Short Tutorial: Dictionary based inventory

PART 1

If you want to make a game, you will probably want your player to be able to pick up items.
Storing properties on an item is OK for simple games where you have just a player who can pick up points, or ammo:

player['ammo'] += 1

But when you start trying to model an inventory, this is a pain to try and handle.

One of the best methods for making an inventory is using a dictionary.
***SIMPLE DICTIONARY

my_inventory = {"9mm_ammo":12,"body_armor":1,"medkit":4}

You can wrap this over several lines to make it more readable:

my_inventory = {"9mm_ammo":12,
    "body_armor":1,
    "medkit":4}

You can see that the inventory holds 12 clips of 9mm ammo, a set of body armor and 4 medkits.

An empty dictionary looks like this:

my_inventory = {}

***ADDING ITEMS

If you want to add new items to the dictionary you have to check if it is already in there:

item_string = "teddybear"

if item_string in my_inventory:
    my_inventory[item_string] += 4
else:
    my_inventory[item_string] = 4

Otherwise you’ll get a key error (trying to add to an item that doesn’t exist).

A shorter way of doing this is to use the built in .get() function.

my_inventory[item_string] =  my_inventory.get(item_string,0) + 4 

This checks if item string is there, if not it uses the default setting:

.get(item to check for, default setting)

.get() can be very useful when working with dictionaries.

***REMOVING ITEMS

Removing items could get tricky, as we could end up with -5 9mm ammo if we’re not careful.

So we can use the built in function max() to set a minimum value. This will always choose the highest of two options.

my_inventory[item_string] =  max(0,my_inventory.get(item_string,0) - 4) 

If the result is less than 0, it will be set to zero automatically.
Because we used the .get() method it will even set non existent items to zero.
So if item_string = socks

It will add a zero sized stack of sock object to the inventory.

We could remove the string from the dictionary using del, but we don’t need to. As long as it has a value of zero it won’t be represented in the inventory.

***PREPARING THE INVENTORY FOR DISPLAY

For displaying your dictionary we could break it down in to a list:


display_list = []
for key in my_inventory:
    stack= my_inventory[key]
    for item in range(stack):
        display_list.append(key)

This will produce a list like this:

We need to get a list because we want each object to be displayed individually, not as a stack.

***DISPLAYING THE INVENTORY

You will need to create some inventory items on a hidden layer, and name them the same as your inventory dictionary keys. Next you’ll use scene.addObject() to add those to your scene.

scene = own.scene

y_offset = 0.0

for item in display_list:
    item_box = scene.addObject(item,own,0)
    item_box.worldPosition.y -= y_offset
    y_offset += 1.2

This will add the objects and offset them by 1.2 blender units on the y axis. Like this:


This might not be very useful, as a long inventory will go off the bottom of the screen so we can offset on the x axis too.

scene = own.scene

y_offset = 0.0
x_offset = 0.0

for item in display_list:
    if y_offset > 6.0:
        y_offset = 0.0
        x_offset += 3.5
    
    item_box = scene.addObject(item,own,0)
    item_box.worldPosition.y -= y_offset
    item_box.worldPosition.x += x_offset
    y_offset += 1.2

That’s better:


***Continued below>>>

I’ve created a simple blend to show what I did above:
inventory.blend (483 KB)

There’s lots more we can add to this inventory system, and I’ll be handling that in further posts if anyone is interested.

PART 2

The next thing to think about is how to display the inventory in an overlay scene.
We want to manage the inventory dictionary in the main scene but have it displayed as an overlay.

First you should name your existing scene as “inventory” and add a new scene which you can call “main_scene” or anything you like.

***MODULE MODE

From now we’re going to be using module mode for our scripts. In this case we can define a module just like a function.
(I’ve been highlighting things like dictionaries and functions with links to python tutorials. If you have any questions about the python code, try reading the tutorials there first).

def inventory_manager(cont):
    own = cont.owner

See how we don’t define cont, it’s already passed to the module by Blender.
You need to rename your script to inventory.py Don’t forget to add the .py or it won’t work as a module.

Then add a module controller to your in game object in the main scene and define the module as:

inventory.inventory_manager


Do the same for the second half of the script, which defines the inventory display.

***GLOBALDICT

We want to access our dictionary from any scene, so we are going to put it in to blender’s bge.logic.globalDict.

bge.logic.globalDict["my_inventory"] = my_inventory

Be careful never to use:

bge.logic.globalDict = my_inventory

As this will overwrite any dictionaries you have saved in globalDict (globalDict is a dictionary, and other dictionaries can be nested inside it).

From your overlay scene you can get the globalDict like this:

my_inventory = bge.logic.globalDict.get("my_inventory")

Notice I’ve used .get() again, this is to make sure that “my_inventory” has been stored correctly and hasn’t been overwritten or deleted by mistake.

I only want the display code to run if “my_inventory” exists, so I use:

if my_inventory:

Then run the code.

EDIT:
This could cause problems if you have an empty inventory, so you could use:

if not "my_inventory" in bge.logic.globalDict: 

Instead, or you could add an empty object to your inventory dictionary when you create it:

my_inventory = {"nothing:0"}

***ADDING THE OVERLAY SCENE

Don’t forget to add your overlay scene, you can use the helpful

bge.logic.addScene("inventory") 

function to do that.

***FINALLY

If you did everything correctly you should have a script that looks a bit like this:

import bge


def inventory_manager(cont):
    own = cont.owner
    
    bge.logic.addScene("inventory")    

    my_inventory = {"9mm_ammo":12,
        "body_armor":1,
        "medkit":4,
        "teddybear":4}
    
    bge.logic.globalDict["my_inventory"] = my_inventory
        
def inventory_display(cont):
    own = cont.owner

    my_inventory = bge.logic.globalDict.get("my_inventory")

    if my_inventory:

        display_list = []
        for key in my_inventory:
            stack= my_inventory[key]
            for item in range(stack):
                display_list.append(key)
                
        scene = own.scene

        y_offset = 0.0
        x_offset = 0.0

        for item in display_list:
            if y_offset > 6.0:
                y_offset = 0.0
                x_offset += 3.5
            
            item_box = scene.addObject(item,own,0)
            item_box.worldPosition.y -= y_offset
            item_box.worldPosition.x += x_offset
            y_offset += 1.2   

And the in game result should look like this:


Here’s a blend file for testing:
inventory.blend (502 KB)

And here’s a blend with some extra developments, you can pick up items and they will be displayed in your inventory. Press “i” to toggle displaying and hiding the inventory.

inventory_final.blend (608 KB)


So what’s the problem in thisi nventory- infinite place in it and no stackable things. If you make that too, than it is very good inventory:D

Stackable objects can be handled by defining a list of stackable objects:

stackable = ["9mm_ammo"]

And checking at each stage of the display code if the item is stackable or not:

if item in stackable:

Now 9mm ammo is stackable, teddy bears are not.

Simple :slight_smile:

As for setting a limit, you have several options.

The easiest one would be to only add new objects if your dictionary was shorter than a certain length:

if len(my_inventory) < 12:

But a more complex way would be to count all the objects in there (only counting stackable objects once):

my_inventory = bge.logic.globalDict.get("my_inventory")
    
inventory_filled = 0
for key in my_inventory:
    stack= my_inventory[key]
    if key in stackable():   
        inventory_filled += 1
    else:
        inventory_filled += stack 

if inventory_filled < 6:
    add_item()

Or you could add further detail by saying that:

 if inventory_filled < 6 or item in stackable:

Then your player could still pack up stackable items even if his inventory is full.

Here’s a demo with stackable objects and an inventory limit:
inventory_final.blend (626 KB)

This should be a very helpful resource for a lot of people. Nice one Smoking!

I kinda implemented something similar, using lists. Was wondering now as well, if it made a difference in this specific case (inventory), whether it’s a list or dictionary you’re going with? Or is it that in a dict, you can assign values to items (stack)? Just to clarify really…

Thanks,

Pete

Good work smoking mirror:)

The benefit of a dictionary is that it’s easy to add things and pull them out. Imagine you are talking to an NPC and he wants 5 apples. With a dictionary it’s really easy to check if you have the right items and then remove them. With a list it’s possible but not quite as easy.

The down side is that a dictionary is unsorted so it doesn’t preserve any data about the order or position of items in your backpack.

How do I test which item I am hovering with mouse? I want to make mouse pickup…

Thanks for taking the time to post this tutorial. I’ve always used planes with items UVmapped, and Text properties to show inventories.
'bout time I learned how to do it the right way. :slight_smile:
I’ll have a look at this later.

I’ll be adding more to the tutorial later to cover using or dropping things from the inventory screen and also combining 2 items to make a third like in a point and click adventure puzzle. (Used in my own project)

I would also use this feature smoking,

will you support breaking items back down? I was thinking of having a ‘blue print’ in the inventory, that clicking a part then clicking the print, would add it to the stack if the item was on the list of parts needed.

Breaking down = gives you all parts + blue print

Are you going to also be able to assemble compound rigid bodies in your own game?

OK! Sounds great, but I’ve got a problem. Did you check that .blend I sent you? I really can’t implement that in that game. Maybe you should make some sub-tutorials for different methods on how to implement it in different games…

As promised I’ll be adding some updates to the system today.
It’s getting a little complex, so I hope you can continue to follow along.

The first thing to say about working with an inventory is that because we will later be using scripts to get the mouse position on screen so we can move things around, it probably best to get mouse info with scripts rather than with logic bricks. So two useful script functions I use are:


def mouse_hit_ray(camera,mouse_position,property):
    screen_vect = camera.getScreenVect(*mouse_position)      
    target_position = camera.worldPosition.copy() - screen_vect                
    target_ray = camera.rayCast( target_position, camera, 300.0, property, 0, 1, 0)
        
    return target_ray

def mouse_triggered(button): 
    mouse = bge.logic.mouse
    tapped = bge.logic.KX_INPUT_JUST_ACTIVATED 
        
    if mouse.events[button] == tapped:
        return True
    
    return False

Mouse_hit_ray will work as a “mouse over any” function, you could use the same logic brick if you wanted. Mouse triggered works the same as a mouse button sensor, you could use the same if you want. Why don’t I?

Well, mouse over any is a little bit of a CPU hog. If you’ve got a lot of objects it can degrade performance. If you use the mouse_triggered function, and then only run mouse over when the mouse_triggered function is True, we can save some performance. The inventory view updater is going to be running on a zero pulse always sensor anyway, but most of the time it won’t actually be doing any calculations so logic stays at near zero.

***NEW GLOBALDICT ADDITIONS

Before we get started we need two more additions to the global dict. In the inventory manager function we add:

if not "my_inventory" in bge.logic.globalDict:  
    ...
    bge.logic.globalDict['dropping_item'] = []
    bge.logic.globalDict['inventory_update'] = True

‘dropping_item’ is the name of the item that’s being dropped from the inventory, you could use a message sensor instead if you wanted to make this with logic bricks. ‘inventory_update’ is the property which tells the inventory display script to update the display. We are no longer going to update the display continuously, but only when needed. We want it to update the display right away because we might have just loaded a game with an existing inventory.

***THE MOUSEOVER

In the inventory_display function we want to see if the player is clicking on one of the inventory items. For now we will be using right click to dump items from the inventory, later we will want to use left click to combine items.

For now the script looks like this:

right_button = mouse_triggered(bge.events.RIGHTMOUSE)
                
if right_button:
    mouse_position = bge.logic.mouse.position       
    camera = scene.active_camera          
    item_over = mouse_hit_ray(camera,mouse_position,"inventory_item")[0]
                      
    if item_over:  
        dropping_item = str(item_over.name)    
        
        if dropping_item != "nothing":    
            bge.logic.globalDict['dropping_item'].append(dropping_item)

If the right button is being pressed, we check for mouse over. If true we send a message to the main inventory manager in the main scene to get them to remove that item from the inventory. We don’t want to draw the inventory this time, because the item hasn’t be removed yet, so we next use:

elif bge.logic.globalDict['inventory_update'] or inventory_key.positive:
    bge.logic.globalDict['inventory_update'] = False

Now the inventory will only redraw if it receives an update message from the manager, or if we turn off or on the inventory screen.

***DROPPING THE ITEM

Back in the inventory manager function we need to get the position of a dropper item. This is just an empty object attached to the player which is used to drop items.


To get it for use in the script we just use a list comprehension- [**]:

own['dropper'] = [ob for ob in own.children if ob.get("dropper_ob")][0]

Note that if you didn’t set up the dropper’s properties correctly or the dropper is not parented correctly you’ll get an error. This is because we are getting the [0] first item from a list, but the list might not exist, so there’s a problem in that case.

All being well you shouldn’t get any errors. Make sure the dropper is scaled to 1 and is far enough away from the player so dropped items won’t get picked up as soon as they are dropped.

Next we needs some dropping code:

if bge.logic.globalDict['dropping_item'] != []:
    
    for drop_item in bge.logic.globalDict['dropping_item']:
        item_management(drop_item,adding=False)  
        
        drop_item_ob = drop_item + "_item" 
        dropping_thing = own.scene.addObject(drop_item_ob,own['dropper'],0)
        dropping_thing.applyForce([0.0,230.0,0.0],True)           
         
    bge.logic.globalDict['dropping_item'] = []
    bge.logic.globalDict['inventory_update'] = True

As I said, you could use a message system here, sending and receiving messages instead of embedding them in the globaldict, it’s up to you how you want to handle it. I might do a version of the system which uses only logic bricks one day…

You might ask why I use a “for” loop on the dropped item list, since it will usually be only one item which is dropped at a time. well, there may be times when you want to send a drop item request to the player for example to make them drop all their gear when they die so it can be picked up by someone else on the server. Or if the player is given a bunch of quest type loot by an automated script as a reward but already has a full inventory. In that case you need to drop all that extra loot else it will either disappear or cause the inventory to overfill.

*applyForce is optional, but I think you probably want the objects to drop as if thrown down, rather than just falling to earth.

***MISC CHANGES TO THE SCRIPTS

I’ve made some other changes which are difficult to list here. you can see them in the example blend:
inventory_dropping.blend (622 KB)

Sometimes as you add new features to a script it becomes necessary to reorder the if/else statements or change the logic brick set up a little. Don’t be afraid to make structural changes if the functional changes aren’t working.

For the next part it gets even more complicated, but if you’ve followed me so far I don’t think you’ll fall apart now.

***COMBINING ITEMS

For combination of items, or crafting we can make a crafting recipe:

combinations = {"ham":["cheese","sandwich"]}

This is a simple recipe, it means if you have ham, and some cheese you can make a sandwich (bread is assumed :stuck_out_tongue: )
Really you should make recipes in the following way:

combinations = {"ham":["bread","ham_sandwich"],
    "cheese":["bread","cheese_sandwich"],
    "ham":["cheese_sandwich","ham_and_cheese_sandwich"],
    "cheese":["ham_sandwich","ham_and_cheese_sandwich"]}

So that by combining two items we can get a midpoint item, or by combining crafting materials we can get a crafting component. And then by further combination we can reach the final combination to get a fully crafted item.

A more suitable crafting type example might be:

sticks + flints = arrow shafts
feathers + arrow shafts = arrows

Arrow shafts are a crafting component and are useless on their own, as are sticks, flints and feathers.

***ACTIVE ITEM

You will need a new object in your inventory overlay scene. It’s a copy of one of your inventory item representations, it doesn’t matter what it looks like (it’s going to be invisible when not representing one of the inventory items) but it needs to be the same scale as the inventory items. Place it anywhere in the active layer and give it a “mouse_focus_item” property. Set it to no collision.

You can get this item at any time by using:

if "mouse_item" not in own:
    own['mouse_item'] = [ob for ob in scene.objects if ob.get("mouse_focus_item")][0]
    own['mouse_item'].visible = False
    own['active_item'] = ""

We want to make it invisible at first. own[‘active_item’] is going to be the string which tells your script which item you’re handling.

We will be replacing the mesh of this object with whatever inventory item you’ve currently grabbed in order to combine with another. So we need all the inventory items on the second layer to have the same mesh name as they have as an object name. If you don’t want to do that manually, it’s pretty boring to retype all those mesh names now, you can use a simple bpy script:

import bpy

for ob in bpy.context.selected_objects:
        
    name = ob.name
        
    ob_data = ob.data
    
    ob_data.name = str(name)

Just select all the inventory items and run the script from the text window.

***SELECT AN ITEM

To select an item we use our mouse over script again and a left click check this time:

left_button = mouse_triggered(bge.events.LEFTMOUSE)

If you’ve clicked on an item you want to set that as your active object.

if left_button:                             
    item_over = mouse_hit_ray(camera,mouse_position,"inventory_item")[0]
                      
    if item_over:  
        active_item = str(item_over.name)              
        own['active_item'] = active_item

Now, if you have a selected object we need to make it appear under the mouse cursor:

def mouse_copy(camera,mouse_position):
    screen_vect = camera.getScreenVect(*mouse_position)   
    screen_vect.length = 32.0
    
    target_position = camera.worldPosition.copy() - screen_vect  
    
    return target_position

You may have to fiddle with screen_vect.length so that the selected item object looks the right size on screen.

if own['active_item']:
    own['mouse_item'].worldPosition = mouse_copy(camera,mouse_position)
    own['mouse_item'].visible = True
    own['mouse_item'].replaceMesh(own['active_item'])

***TRY TO COMBINE ITEMS

Next, if you have an active item ready for combination we just need to check if you click on another item and if the combination is valid.

if left_button:                             
    item_over = mouse_hit_ray(camera,mouse_position,"inventory_item")[0]
                      
    if item_over:  
        
        new_item = "" 
        remove_items = []                       
        
        combine_item = str(item_over.name)  

Hopefully we now have an active item and a combine_item, to try and combine it with. Here we are going to do something clever.
We don’t want to write a combination recipe for each of the following:

A + B = C
B + A = C

Because that would be duplication of effort and with a crafting recipe book with hundreds of items in it, you would go mad writing every possible combination, so we make a list of possible combinations and check each variation:

combinations = {"ham":["cheese","sandwich"]}

variations = [[own['active_item'],combine_item],[combine_item,own['active_item']]]                        

for ingredients in variations:

    if ingredients[0] in combinations:
        combination = combinations[ingredients[0]]   
        if combination[0] == ingredients[1]:
            new_item = combination[1]
            remove_items = ingredients

So ham and cheese makes a sandwich and so does cheese and ham. Genius! :slight_smile:

Anyway, if your combination has been successful we just need to remove the old objects from the inventory and add a new one:

if new_item:
    item_management(new_item)   
    for remove_item in remove_items:                            
        item_management(remove_item,adding=False)
    
        
    own['active_item'] = ""
    own['mouse_item'].visible = False    
    bge.logic.globalDict['inventory_update'] = True    

We are going to do the globaldict changes right here in the inventory display overlay rather than sending them to the main manager as we did before. That’s optional, but I thought it’s OK since we are not going to be dropping any items in to the main scene this time. If you were using messages to communicate between the main scene and the overlay you’d have to send messages back to the main scene to do the changes.

***CRAFTED!

There’s more to it than that of course! You will have to reorder your script somewhat to get it to work well. We only want to be able to drop items if you’re not trying to combine them, otherwise we could drop an item we are trying to combine! We also want to clear the active item if we click on anything other than another inventory item. We also need to clear the active item if inventory update is true, as that means you probably just picked up another item, which could make things go buggy if you keep the active item set.

The full set of changes are in this demo:
inventory_combinations.blend (727 KB)

Pick up the cheese and ham and then use the left mouse button to combine them.

Next time I’ll show how to use an active object from the inventory with something in the main scene, like opening a door with a key, or throwing a spear at an enemy.

OK! Looks like this will be very useful for me! Are there gone be more tutorials?

Maybe you could make a video tutorial on this, but use an ID object system that allows you to save a .bgeconf file with data(because it doesn’t allow to save strings). It is hard for me to use text tutorials! I usually need video tutorials:/