Inventory system frontend failing; could someone help?

Alright, so I’ve been puzzling over this for quite a while, and I just cannot figure out the problem. I’m working on a project (in case it wasn’t obvious… :stuck_out_tongue_closed_eyes:), and I can’t seem to get the frontend for my inventory system to function correctly. Here’s a video I made that showcases the issues. I feel like I’d just confuse everyone if I tried to explain it in text.

I know the problem isn’t in my backend programming, as the debug output proves that that is working fine. Here are the scripts that do most of the work:

The backend code, which works fine:

from Util import * #This allows us to use:
#                     random (Module)
#                     math/mathutils (Modules)
#                     bge (GameEngineModule)
#                     globalDict (GlobalDictionary) 
#                     cont (CurrentController) ##Causing issues
#                     scene (CurrentScene)
#                     cam (ActiveCamera)
#                     own (ControllerOwner)
#                   And all my other functions.

##NOTE: Every 'Collectible' object MUST have these Properties:
##  Collectible[Bool]: Defines the object as something that can be collected
##  Class1[Str]: Item basetype; may be 'Tool', 'Material', or 'Misc' (may be made Int later)
##  Class2[Str]: Item subtype, E.G. 'Food', 'Weapon', etc. (may be made Int later)
##  CanDispose[Bool]: Determines whether or not the item can be disposed (may be removed)
##  Desc[Str]: The item's friendly description

cont = bge.logic.getCurrentController()

for i in cont.sensors['Collision'].hitObjectList: #Iterate over every Collectible object
    if i['Collectible'] == True: #Make sure the item is set able to be collected
        try: #Update the dictionary.  This is a weird block of code, but necessary (I /think/) to prevent errors.
            globalDict.update({i.name:{'quant':globalDict[i.name]['quant']+1, 'class':i['Class1'], 'subclass':i['Class2'], 'disposable':i['CanDispose'], 'desc':i['Desc']}})
        except:
            globalDict.update({i.name:{}})
            try:
                globalDict.update({i.name:{'quant':globalDict[i.name]['quant']+1, 'class':i['Class1'], 'subclass':i['Class2'], 'disposable':i['CanDispose'], 'desc':i['Desc']}})
            except:
                globalDict.update({i.name:{'quant':0}})
                globalDict.update({i.name:{'quant':globalDict[i.name]['quant']+1, 'class':i['Class1'], 'subclass':i['Class2'], 'disposable':i['CanDispose'], 'desc':i['Desc']}})
        finally:
            #print(globalDict)
            #print()
            bge.logic.sendMessage("Collected", "Item {0} has been collected".format(i.name), i.name)
            cont.activate(cont.actuators['SaveDict'])
            cont.deactivate(cont.actuators['SaveDict'])

The frontend code, in charge of actually populating the inventory menu:

from Util import *
import ObjRefOffsetHandler2

cont = bge.logic.getCurrentController()
own = cont.owner
scene = bge.logic.getCurrentScene()

for obj in own.children:
    obj.endObject()

for i in globalDict.keys():
    print(i) #For debugging
    if not i.startswith("SETTING"): ##Definitely NOT the best way to do this, but it's all I can think of for now.
        if globalDict[i]['class'] == "Tool" and own['Subset'] == 0:
            scene.addObject(i, "ObjectRef").setParent(own, False)
            ObjRefOffsetHandler2.UpdateOffset()
        elif globalDict[i]['class'] == "Material" and own['Subset'] == 1: 
            scene.addObject(i, "ObjectRef").setParent(own, False)
            ObjRefOffsetHandler2.UpdateOffset()
        
        #ObjRefOffsetHandler2.UpdateOffset()
    print(globalDict) #For debugging
    print()           #

#bge.logic.sendMessage("ResetOffset")
ObjRefOffsetHandler2.ResetOffset()  

Now, maybe I’ve just been staring at it for far too long and missed something obvious, but I absolutely cannot figure out what’s failing.

Oh, and before anyone says anything, I know that actually recreating 3D objects is not the most efficient way to do an inventory menu, but believe it or not, I actually have a good reason for doing it that way (plus it was easy :stuck_out_tongue:).

I’ll attach my source .blend, in case you want to look at / mess with it further. Note that it is only a tech demo, a small piece for a far larger project.

Oh, and if anyone could tell me why packed fonts don’t seem to want to work, that would be appreciated, too. :slightly_smiling_face:

Thanks in advance for any help!

Source blendfile (PasteAll)

my guess is dictionaries are name based keys, meaning you cant have duplicates.

try using a list and not a dict.
gd["inventory"] = []
gd["inventory"].append(thing)

also, those try blocks seem excessive.
if "thing" in list
dict.get("key", fallback)

^I second. Use a list. Append object names to it and get quantity with built-ins.

inventory = ["Stinky Potion", "Amulet of Impotence", "Sword of Edgelessness"]

#for a dict with {value:occurrences of value within list} for each value
from collections import Counter
quant = Counter(inventory)

#for a single int counting occurrences of a specific value,
quant = inventory.count(objectName)

The rest you can ditch. Why?

Collectible, Class1, Class2, CanDispose & Desc are all the same for every copy of X.
Saving them to the globalDict is wasting memory, you can just look up properties like so

src = scene.objectsInactive[i.name]
if src[propName] ==  some_value: doStuff()

If you need to work with instance-specific values (e.g. condition on a tool degrades with use) then you want a different approach altogether.

EDIT: Tidying up.

While I do thank you both for the advice, at this point, I’m not really interested in optimizing my ‘game’; I’m interested in actually getting my inventory system to work. :stuck_out_tongue_closed_eyes:

@Daedalus_MDW, yes, that is right about dictionaries, but I don’t have duplicates. I have one key for axes, one key for grass, one key for crossbows, and one key for rocks.

@Liebranca, I don’t really get that? Maybe just because I’ve never used the ‘collections’ module.
I know those properties are the same for each instance of X; that’s why I don’t create a key in the global dictionary for every instance.

Oh, and the reason I’m using the global dictionary is because, well, it’s the global dictionary, maintained across blendfiles and scenes. Plus, it’s easy to load from and save to the disk (using the Game actuator), which is important for a game. You don’t (usually) want your inventory to be reset every time you play, right? Also, the API reference suggests using it to store things like inventories.

I believe I have established that the difficulty is due to your placing the inventory buttons off camera. The following code dose not do what you want, but it should illuminate the issue.

from Util import *
import ObjRefOffsetHandler2

cont = bge.logic.getCurrentController()
own = cont.owner
scene = bge.logic.getCurrentScene()

for obj in own.children:
    obj.endObject()

for num, i in enumerate( globalDict.keys() ):
    #print(i)
    if not i.startswith("SETTING"): ##Definitely NOT the best way to do this, but it's all I can think of for now.
        if globalDict[i]['class'] == "Tool" and own['Subset'] == 0:
            obj = scene.addObject(i, "ObjectRef")
            obj.setParent(own, False)
            #print(obj, obj.worldPosition)
            ObjRefOffsetHandler2.UpdateOffset()
            inc = num * 0.3
            obj.worldPosition = -2.5+inc, -3.5, 0
            obj.setParent(own, False)
        elif globalDict[i]['class'] == "Material" and own['Subset'] == 1: 
            obj = scene.addObject(i, "ObjectRef")
            #obj.setParent(own, False)
            #print(obj, obj.worldPosition)
            ObjRefOffsetHandler2.UpdateOffset()
            inc = num * 0.3
            obj.worldPosition = -2.5+inc, -3.5, 0
            obj.setParent(own, False)
        #ObjRefOffsetHandler2.UpdateOffset()
    #print(globalDict)
    #print()
#bge.logic.sendMessage("ResetOffset")
ObjRefOffsetHandler2.ResetOffset()       

I would recommend having multiple emptys and placing the objects on them. ObjectRef0, ObjectRef1, ect… I have not tested this code.

from Util import *

cont = bge.logic.getCurrentController()
own = cont.owner
scene = bge.logic.getCurrentScene()

for obj in own.children:
    obj.endObject()


def makeObj(name):
    refObj = scene['ObjectRef'+str(index)]
    obj = scene.addObject(name)
    obj.worldPosition = refObj.worldPosition
    obj.setParent(own, False)


index = 0
for i in globalDict.keys():
    if not i.startswith("SETTING"): ##Definitely NOT the best way to do this, but it's all I can think of for now.
        if globalDict[i]['class'] == "Tool" and own['Subset'] == 0:
            makeObj(i)
            index += 1
        elif globalDict[i]['class'] == "Material" and own['Subset'] == 1: 
            makeObj(i)
            index += 1  

Having looked at your code I would like to inform you of a two subtle considerations with BgePython. Namely, aliasing and scope.

print('This will only print once.')
def someFunc():
  print('This will print many times.')

pos = obj.worldPosition # returns a *copy* of the position vector not the vector itself.
pos[0] += 1 # dose not change the objects position.
print(pos[0] == obj.worldPosition) # prints False

The first time I came across the aliasing problem, it took me about three solid days of debugging, before I came across the deepcopy function.

Okay, I haven’t thoroughly looked through your post yet, but one thing I thought I’d mention is that writing to a variable initialized from an object’s position vector actually does move the object. Here’s a quick test that I made in about 30 seconds that shows this.
untitled.blend (490.3 KB)

I can confirm that post = own.worldPosition; post.x += 1 works but that post = own.worldPosition.x; post += 1 dose not. I was basing my assumption on prior experience and did not test it. My reason for mentioning aliasing at all was to point out that it can occasionally become confusing. In fact, if the aforementioned code did not work than the object assumably would not have moved at all. A very different error, so I suppose I was obviously wrong. My bad.
The short of my answer was that I believe the objects are being moved off screen. If you just move them less, then that would probably fix it.

Edit:
Ok, ok, I don’t really grok your code, so its hard to know what it should be, but I think I found a typo.

def ResetOffset():
    pos[0] = -2.5
    pos[0] = -3.5

Should probably be.

def ResetOffset():
    pos[0] = -2.5
    pos[1] = -3.5
1 Like

Alright, thanks! And thanks for catching that error! It could have made for some headaches in the future. I fixed it. :slightly_smiling_face:

While doing a little more testing, I noticed that now, the first time you bring up the inventory menu, everything works fine, but on subsequent attempts, it doesn’t. Looking in the console, I get RuntimeError: Vector write index, user has become invalid, raised from ResetOffset(). I Googled that exact string in quotes, and got no results. I assume it means that something is happening to ObjectRef (like getting deleted), but I don’t know why. However it’s happening, it’s probably the problem…

You’re trying to access an attribute of an object that has been deleted. Make a copy of the vector,

pos = obj.worldPosition.copy()

But won’t using copy() only return a duplicate of the vector that has no connection to the original? If so, that won’t work, since I need to be able to modify the worldPosition. Hm, that gives me an idea, actually…

Edit: Okay, so replacing pos with the more direct scene.objects['ObjectRef'].worldPosition raises a different error: SystemError: Blender Game Engine data has been freed, cannot use this python variable
Now I really don’t know what to do. Do I need to re-add an instance of ObjectRef every time the inventory menu scene is loaded?

Ah, alright. You want to move the object around.

The problem is your reference points to an object that’s been deleted. Either stop throwing the whole scene in the garbage can everytime the player closes a menu or spawn a new object.

pos = list(obj.worldPosition) to save to the globalDict, since complex python objects cant be stored there.

the error about data being freed means the object you are trying to access has been deleted (end object).

Aliasing in global scope can cause unpredictable side effects that are hard to debug. You are only deriving scene and pos the first time the module is run. Try this instead. It will rederive them every time the functions are run.

from Util import *


def UpdateOffset():
    scene = bge.logic.getCurrentScene()
    pos = scene.objects['ObjectRef'].worldPosition
    
    if not pos[0] >= 2.5:
        pos[0] += 1
    elif pos[0] >= 2.5 and own.worldPosition[1] == -3.5:
        pos[0] = -2.5
        pos[1] -= 1
    elif pos[0] == 2.5 and own.worldPosition[1] == -4.5:
        pass #Not quite sure what to do in this situation yet...


def ResetOffset():
    scene = bge.logic.getCurrentScene()
    pos = scene.objects['ObjectRef'].worldPosition
    
    pos[0] = -2.5
    pos[1] = -3.5

I made a tutorial regarding the issue on the wiki. The aforementioned code should work without reading the tutorial, incase it’s found to be tldr.
http://upbge.wikidot.com/global-local-tutorial

I remember reading an image explaining the differences between script and module controllers. It was many years ago, but I seem to be thinking it was posted by @Monster? I would like to add it to the wiki. Dose any one happen to know where it could be found?

Great! Thanks a lot, @Cortu! Although I had found another solution (that completely got rid of ObjectRef), this one is far better. :grinning:
Thank you for your help, everyone (even if it wasn’t particularly helpful is some cases :wink:).