DesignNotes #1 – The Art of Learning (Inventory System)

The art of learning

Blog posts tagged as #DesignNotes will be used to highlight some of the more interesting tasks involved in developing ThruTheTrees. In this post, I’d like to talk about the inventory system and the logic behind it. I’m hoping that it will be useful for other aspiring developers. The screen shot at the top of this post shows the inventory in it’s current state.

The player has 5 rows of 5 inventory slots, for a total of 25. I made the design choice to limit the number of items the player can hold to force conscious decisions about preparing for their adventures. By limiting the total items a player can hold at a given time, it adds a layer of strategy over what items are essential, while still leaving enough inventory space to bring back rewards from quests. To accommodate video game hoarders such as myself who love to stash every last piece of loot in case it might be useful at some point later in the game, I wanted to make sure that I gave players a way to store all their goods. The classic solution to this problem is to allow the player to craft and place storage chests in their home.

*Note* ThruTheTrees is being developed using GameMaker Studio, and so the rest of this post will be devoted to the techniques I used to create the inventory using that application. It will be heavy on the why behind my decision making, so others may also find my thought process useful (I hope).

Designing the system

The first thing that we need to do is create a persistent object that is used as a global inventory controller. Because we use this object to draw the player’s inventory on-screen, it should be visible, but with no default sprite. We will draw the sprites manually using code when the player input tells us to show the inventory. I called my object oInventory.

In our inventory controller object we want to define all of the variables needed to hold the player’s inventory, keep track of any chests the player has crafted, and also keep track of any items in those chests. We also need the inventory object to actually draw the UI and items on the screen when the player demands it, reading the items from the player’s on-hand inventory, and any items in the chest that the player has open. The inventory object will also control the mouse input from the player to select and move items in between inventory slots and storage chests and perform functions like equipping gear and dropping items on the ground.

Initial Setup

In the create event of our oInventory object, we need to define some controller variables:

global.chest_id = 0; // total number of chests that have been created
global.cur_inv_selected = noone; // current inventory item that is being selected
global.cur_chest_selected = noone; // current storage chest item that is being selected
global.show_inventory = false; // show if the inventory is currently being displayed on-screen
global.show_chest = false; // show if the storage chest inventory is currently being displayed on-screen
chest_index = noone; // current chest being displayed

We also need to define variables to hold the actual item properties in both the player’s inventory, all the storage chests, and the currently selected item. The way that we will accomplish this task is by using a combination of arrays and ds_grid data structures.

We define an array called global.chest[global.chest_id] and set it equal to a series of ds_grids that we create. What we’re doing in this step is creating a variable that will hold the contents of every single storage chest in the game. We can use this variable to load the contents of any chest that the player decides to open. The data stored inside this array is the ds_grid that we create for each separate chest that holds the properties of each item stored inside it.

A ds_grid is a table that has a defined width and height. The width of our ds_grid will be the maximum number of items we want our chest to hold. For ThruTheTrees, I have chosen 16 item slots per chest. The height of our ds_grid will hold the different item properties of each item in the grid. The height should be set to the total number of unique properties that you want your items to have.

For example, if you want each item to have a unique name, a unique sprite, a sprite index, an item count, an item type, an item value, and a variable that remembers if the item is currently equipped, that would be 7 properties, and your ds_grid height would be 7. It would be wise to plan ahead and think about the different properties that your items will need, and consider properties that can be used across a variety of items to save space. We then run a loop to initialize each one of the item slots and create the global variable that will hold our storage chests:

global.chest[global.chest_id] = ds_grid_create(16,7);

for (i=0;i<16;i++) { // Setup 16 Item Slots
    ds_grid_set(global.chest[global.chest_id],i,1,sprNoSprite); // chest_item_sprite
    ds_grid_set(global.chest[global.chest_id],i,2,0); // chest_item_index
    ds_grid_set(global.chest[global.chest_id],i,3,""); // chest_item_name
    ds_grid_set(global.chest[global.chest_id],i,4,0); // chest_item_count
    ds_grid_set(global.chest[global.chest_id],i,5,""); // chest_item_type
    ds_grid_set(global.chest[global.chest_id],i,6,0); // chest_item_value
    ds_grid_set(global.chest[global.chest_id],i,7,false); // chest_item_equipped
}


We now need to create the player's inventory, using another loop set to the total number of inventory slots the player has, in my case 25. If you want to create a system with an expandable inventory, you simply swap the 25 in the for loop below with a variable like "inv_max_items" that you define with whatever you like. We should initialize array variables to hold the properties of each inventory slot item.

// Create Player Inventory
for (i=0;i<25;i++) {
    inv_item_sprite[i] = sprNoSprite; // ITEM SPRITE
    inv_item_index[i] = 0; // ITEM SPRITE INDEX
    inv_item_name[i] = ""; // ITEM NAME
    inv_item_count[i] = 0; // ITEM COUNT
    inv_item_type[i] = ""; // PICKAXE, WOODAXE, HOE, SHOVEL, CANDLE, WATERCAN, BOOMERANG, FISHPOLE
    inv_item_value[i] = 0; // ITEM VALUE
    inv_item_equipped[i] = false; // WHETHER OR NOT THE ITEM IS EQUIPPED
}


And do the same thing for a temp inventory that we use to display the currently open chest:

// Create Temp Chest Inventory for Displaying on-screen
for (i=0;i<16;i++) {
    chest_item_sprite[i] = sprNoSprite;
    chest_item_index[i] = 0;
    chest_item_name[i] = "";
    chest_item_count[i] = 0;
    chest_item_type[i] = "";
    chest_item_value[i] = 0;
    chest_item_equipped[i] = false;
}


The last things to initialize in the create event are variables for the currently selected item, a temp item that gets used when 2 items in the inventory need to be swapped, and variables to control when to draw the item following the mouse cursor when the player picks it up.

// Create Chest System
last_equipped = noone;
draw_mouse = false;
mouse_item_sprite = noone; // sprite image to draw following the mouse cursor
mouse_item_index = noone; // inventory slot where the item is located
mouse_item_sprite_index = noone; // sprite index to draw
mouse_picked_up = false; // whether or not an item has been picked up
mouse_item_chest = false; // TRUE if the item we're selecting is in a chest, FALSE if the item we're selecting is in the player's inventory

// Currently Selected Item
selected_item_sprite = sprNoSprite;
selected_item_index = 0;
selected_item_name = "";
selected_item_count = 0;
selected_item_type = "";
selected_item_value = 0;
selected_item_equipped = false;

// Temporary Selected Item
_inv_item_sprite = sprNoSprite; // ITEM SPRITE
_inv_item_index = 0; // ITEM SPRITE INDEX
_inv_item_name = ""; // ITEM NAME
_inv_item_count = 0; // ITEM COUNT
_inv_item_type = ""; // PICKAXE, WOODAXE, HOE, SHOVEL, CANDLE, WATERCAN, BOOMERANG, FISHPOLE
_inv_item_value = 0; // ITEM VALUE
_inv_item_equipped = false; // WHETHER OR NOT THE ITEM IS EQUIPPED


That's one helluva setup, but we're done. Now we can move on to controlling, manipulating, and drawing the inventory system.

Checking for Input

In the begin_step event, we want to check for player input on whether or not to display the inventory. If the player opens the inventory or a chest, we should set the global.show_inventory or global.show_chest variables = true, and if the inventory is already open and the player wants to close it, we should set those variables = false. When the inventory is closed, it is a good idea to clear whatever the last selected item was, so that when the player opens the inventory again they won't have any items in their hand. We can write a script called inv_reset_mouse_item() that can be called to reset the selected item variables.

Additionally, we need a chest object that can be placed in the game world and interacted with by the player, which we name oChest. We'll need to make sure that we read the contents of each individual chest and display them on-screen when selected. We'll use another script named inv_write_chest() to make sure that we save the changes to our chest inventories when the player closes out the inventory screen. If you want your game to play sound FX when the player opens or closes the inventory screen, we can add that here as well.

/// Check for Input in the Begin_Step Event
// Inventory
if (keyboard_check_pressed(ord('I'))) {
    if (global.show_inventory == true) {
        global.show_inventory = false;
        inv_reset_mouse_item();        
        if (global.show_chest == true) {
            inv_write_chest();
            global.show_chest = false;
            audio_play_sound(snd_chest_close,5,false);  
            with (oChest) {
                closed = true;
            }                  
        }    
    } else {
        global.show_inventory = true;
        audio_play_sound(snd_click1,5,false);
    }
}


/// inv_reset_mouse_item()
mouse_picked_up = false;
mouse_item_sprite = noone;
mouse_item_index = noone;
mouse_item_sprite_index = noone;
mouse_item_chest = false; // Not picked up from a chest
draw_mouse = false;
audio_play_sound(snd_click2, 5, false);
reset_selected_item();


/// reset_selected_item()
// Resets Currently Selected Item
selected_item_sprite = sprNoSprite;
selected_item_index = 0;
selected_item_name = "";
selected_item_count = 0;
selected_item_type = "";
selected_item_value = 0;
selected_item_equipped = false;


/// inv_write_chest()
for (i=0;i<16;i++) { // Write 16 Chest Item Slots
    ds_grid_set(global.chest[oInventory.chest_index],i,1,oInventory.chest_item_sprite[i]); // chest_item_sprite
    ds_grid_set(global.chest[oInventory.chest_index],i,2,oInventory.chest_item_index[i]); // chest_item_index
    ds_grid_set(global.chest[oInventory.chest_index],i,3,oInventory.chest_item_name[i]); // chest_item_name
    ds_grid_set(global.chest[oInventory.chest_index],i,4,oInventory.chest_item_count[i]); // chest_item_count
    ds_grid_set(global.chest[oInventory.chest_index],i,5,oInventory.chest_item_type[i]); // chest_item_type
    ds_grid_set(global.chest[oInventory.chest_index],i,6,oInventory.chest_item_value[i]); // chest_item_value
    ds_grid_set(global.chest[oInventory.chest_index],i,7,oInventory.chest_item_equipped[i]); // chest_item_equipped
}

Moving and Equipping Items

We're done with the begin_step event, but we also need a normal step event to check if the display inventory = true, get the currently selected item properties, and perform any inventory functions such as equipping items, moving and re-arranging items, dropping items, etc. The best thing to do is create scripts that control each one of these different checks. This will make your project much easier to debug and pinpoint issues with your code.

/// Check Inventory
if (global.show_inventory == true) {
    inv_get_selected_item(); // Get the selected inventory item stats
    
    // EQUIP Items using the RIGHT MOUSE BUTTON
    if (mouse_check_button_pressed(mb_right)) {
        inv_equip_item();
    }
    // MOVE Items using the LEFT MOUSE BUTTON
    if (mouse_check_button_pressed(mb_left)) {
        inv_move_item();
    }
}


/// inv_get_selected_item()
// Get Selected Inventory Item Stats
if (global.cur_inv_selected != noone) {    
    selected_item_sprite = inv_item_sprite[global.cur_inv_selected];
    selected_item_index = inv_item_index[global.cur_inv_selected];
    selected_item_name = inv_item_name[global.cur_inv_selected];
    selected_item_count = inv_item_count[global.cur_inv_selected];
    selected_item_type = inv_item_type[global.cur_inv_selected];
    selected_item_value = inv_item_value[global.cur_inv_selected];
    selected_item_equipped = inv_item_equipped[global.cur_inv_selected];
    
} else if (global.cur_chest_selected != noone) { /// Item Cursor in Chest Slot
    selected_item_sprite = inv_item_sprite[global.cur_chest_selected];
    selected_item_index = inv_item_index[global.cur_chest_selected];
    selected_item_name = inv_item_name[global.cur_chest_selected];
    selected_item_count = inv_item_count[global.cur_chest_selected];
    selected_item_type = inv_item_type[global.cur_chest_selected];
    selected_item_value = inv_item_value[global.cur_chest_selected];
    selected_item_equipped = inv_item_equipped[global.cur_chest_selected];
}


In the inv_equip_item() script, we simply check to make sure that the inventory slot selected is not empty, and then check the item type selected, and update the necessary variables as needed. The global.cur_inv_selected variable will get updated in the draw_gui event.

/// inv_equip_item()
// Equip ITEM
if (global.cur_inv_selected != noone and global.cur_inv_selected != last_equipped) {

    // EQUIP TOOL
    if (inv_item_sprite[global.cur_inv_selected] == spr_tool) {
        oPlayer.tool = inv_item_type[global.cur_inv_selected];
        oPlayer.tool_name = inv_item_name[global.cur_inv_selected];
        oPlayer.tool_value = inv_item_value[global.cur_inv_selected];
        last_equipped = global.cur_inv_selected;
        
        for (i=0;i<25;i++) {
            if (inv_item_sprite[i] == spr_tool) {
                inv_item_equipped[i] = false;
            }
        }
        inv_item_equipped[global.cur_inv_selected] = true;
        
        audio_play_sound(snd_equip_item, 5, false);


Moving objects around the player's inventory and swapping items in-and-out of chests can involve some reasonably complicated logic. I'll try and take you through the process I used to keep track of my inventory. It's crucial for you as a designer to think about how the player will interact with the UI that you create. In ThruTheTrees, when a player presses the left mouse button on an item, the item will be "picked up" and drawn following under the mouse cursor until the player presses the left mouse button again. If the button is pressed on an empty slot in the player's inventory or a storage chest, the item will be placed in that slot. If the slot already has an item in it, the two items will swap item slots. If an item gets moved from the player's inventory to a storage chest, we will also need to unequip the item if it was equipped by the player.

Here is some example code for moving objects in the inventory. The first step is to check if the player already has an item in-hand. If not, "pick up" the item by copying it's location to the variable mouse_item_index and the item's sprite to mouse_item_sprite and mouse_item_sprite_index. You can then use the mouse_item_index variable to reference any of the information you'd like to call for that item, which you do for tasks such as drawing the item stats on-screen in the inventory. The mouse_item_chest variable is used to tell the controller if the item that was picked up came from a chest or not.

if (global.cur_inv_selected != noone) { // Item Cursor in Inventory Slot
    if (mouse_picked_up == false) { // Nothing in hand
        //if (inv_item_sprite[global.cur_inv_selected] != sprNoSprite) {
        if (selected_item_sprite != sprNoSprite) {
            mouse_picked_up = true; // Pick up the Item
            draw_mouse = true; // Draw the item in hand
            mouse_item_index = global.cur_inv_selected;
            mouse_item_sprite = selected_item_sprite;                
            mouse_item_sprite_index = selected_item_index;
            mouse_item_chest = false; // Not picked up from a chest
            audio_play_sound(snd_click1, 5, false); 
        }
    // IF already have an item in hand
    } else if (mouse_picked_up ==  true) { // Item in hand              
        inv_set_temp_item() // Set Selected Item to a Temporary Slot
                                            
        // Swap Item Slots
        //if (inv_item_sprite[global.cur_inv_selected] != sprNoSprite) {
        if (selected_item_sprite != sprNoSprite) {
            if (mouse_item_index == global.cur_inv_selected) { // Check to see if the same item is selected
                inv_reset_temp_item();
                inv_reset_mouse_item();                
            } else { 
                inv_item_new_slot() // Move the item to the new inventory slot                                          
                inv_reset_mouse_item();                                                   
            }
        // Place Item in Empty Slot                 
        } else if (inv_item_sprite[global.cur_inv_selected] == sprNoSprite) {
            inv_item_new_slot(); // Move the item to the new inventory slot                   
            inv_reset_mouse_item();                      
        }
    }       
}


The bottom half of the code above shows how to handle the input when the player has already "picked up" and item. We need to set the item in the newly selected slot to a temporary item slot, then copy over the new item, and put the old item in the old slot.

/// inv_set_temp_item()
// Set the selected inventory slot item to a temp variable
if (inv_item_sprite[global.cur_inv_selected] != sprNoSprite) {
    _inv_item_sprite = inv_item_sprite[global.cur_inv_selected];
    _inv_item_index = inv_item_index[global.cur_inv_selected];
    _inv_item_name = inv_item_name[global.cur_inv_selected];
    _inv_item_count = inv_item_count[global.cur_inv_selected];
    _inv_item_type = inv_item_type[global.cur_inv_selected];
    _inv_item_value = inv_item_value[global.cur_inv_selected];
    _inv_item_equipped = inv_item_equipped[global.cur_inv_selected];     
}


/// inv_reset_temp_item
// Reset Temporary Selected Item
_inv_item_sprite = sprNoSprite; // ITEM SPRITE
_inv_item_index = 0; // ITEM SPRITE INDEX
_inv_item_name = ""; // ITEM NAME
_inv_item_count = 0; // ITEM COUNT
_inv_item_type = ""; // PICKAXE, WOODAXE, HOE, SHOVEL, CANDLE, WATERCAN, BOOMERANG, FISHPOLE
_inv_item_value = 0; // ITEM VALUE
_inv_item_equipped = false; // WHETHER OR NOT THE ITEM IS EQUIPPED 


The inv_item_new_slot() script checks to see where the "picked up" item came from (chest or player's inventory) and then swaps it to the new item slot as needed:

/// inv_item_new_slot()
if (mouse_item_chest == false) { // Item came from the player's inventory                       
    inv_item_sprite[global.cur_inv_selected] = inv_item_sprite[mouse_item_index];
    inv_item_index[global.cur_inv_selected] = inv_item_index[mouse_item_index];
    inv_item_name[global.cur_inv_selected] = inv_item_name[mouse_item_index];
    inv_item_count[global.cur_inv_selected] = inv_item_count[mouse_item_index];
    inv_item_type[global.cur_inv_selected] = inv_item_type[mouse_item_index];
    inv_item_value[global.cur_inv_selected] = inv_item_value[mouse_item_index];
    inv_item_equipped[global.cur_inv_selected] = inv_item_equipped[mouse_item_index]; 

    if (inv_item_sprite[global.cur_inv_selected] == sprNoSprite) {     
        clear_inventory_slot();
    }
    
    // Swap the old item slot
    inv_item_sprite[mouse_item_index] = _inv_item_sprite;
    inv_item_index[mouse_item_index] = _inv_item_index;
    inv_item_name[mouse_item_index] = _inv_item_name;
    inv_item_count[mouse_item_index] = _inv_item_count; 
    inv_item_type[mouse_item_index] = _inv_item_type;        
    inv_item_value[mouse_item_index] = _inv_item_value;    
    inv_item_equipped[mouse_item_index] = _inv_item_equipped; 
    
    inv_reset_temp_item(); 
} else if (mouse_item_chest == true) { // Item came from a chest
   // Do the same stuff you just did above, but for a chest instead
}


/// clear_inventory_slot()
// Clear Inventory Slot
inv_item_sprite[mouse_item_index] = sprNoSprite;
inv_item_index[mouse_item_index] = 0;
inv_item_name[mouse_item_index] = "";
inv_item_count[mouse_item_index] = 0; 
inv_item_type[mouse_item_index] = "";   
inv_item_value[mouse_item_index] = 0;    
inv_item_equipped[mouse_item_index] = false;

mouse_picked_up = false;
draw_mouse = false;
mouse_item_sprite = noone;
mouse_item_index = noone;
mouse_item_sprite_index = noone;
audio_play_sound(snd_click2, 5, false);  

Drawing it On-Screen

The final task of our inventory control object is to draw the items on screen when the player interacts with the game. We use the draw_gui event to overlay the inventory for easier visibility. Draw a sprite that represents the background image for each item slot called spr_inventory_box. What we do is loop through the screen coordinates and draw a sprite for each one of our inventory or chest item slots. We use a variable called _inv_selected to keep track of the currently selected item for player input. The way we calculate what item the player is currently selecting is by using device_mouse_x_to_gui and device_mouse_y_to_gui, which returns the x or y position of the mouse relative to the GUI layer when pressed.

/// Draw Item Slot Backgrounds
if (global.show_inventory == true) {
    // Draw Inventory Boxes
    for (i=0;i<5;i++) {
        for (j=0;j<5;j++) {            
            
            // Get Current Inventory Item
            if (j==0) {
                var _inv_selected = i;                                                
            } else if (j==1) {
                var _inv_selected = i + 5;
            } else if (j==2) {
                var _inv_selected = i + 10;
            } else if (j==3) {
                var _inv_selected = i + 15;
            } else if (j==4) {
                var _inv_selected = i + 20;
            } 
              
            // Draw Item Background
            draw_sprite_ext(spr_inventory_box,0,(i*64),(j*64),1,1,0,c_white,1);     
            
            // Draw Item Sprite
            draw_sprite_ext(inv_item_sprite[_inv_selected],inv_item_index[_inv_selected],(i*64),(j*64),1,1,0,c_white,1);  
                  
            // Draw Cursor Selection            
            if (device_mouse_x_to_gui(0) > (i*64) and device_mouse_x_to_gui(0) < (i*64)+64) {
                if (device_mouse_y_to_gui(0) > (j*64) and device_mouse_y_to_gui(0) < (j*64)+64) {                    
                    if (j==0) {
                        global.cur_inv_selected = i;                                                
                    } else if (j==1) {
                        global.cur_inv_selected = i + 5;
                    } else if (j==2) {
                        global.cur_inv_selected = i + 10;
                    } else if (j==3) {
                        global.cur_inv_selected = i + 15;
                    } else if (j==4) {
                        global.cur_inv_selected = i + 20;
                    }
                }  
            // Check if cursor is outside the inventory box              
            } else if (device_mouse_x_to_gui(0) < (i*64) or device_mouse_x_to_gui(0) > (5*64)) {
                global.cur_inv_selected = noone;
            } else if (device_mouse_y_to_gui(0) < (i*64) or device_mouse_y_to_gui(0) > (5*64)) {
                global.cur_inv_selected = noone;
            }        
        }
    }
}

Cleaning Up

One other thing to mention is to make sure to destroy the ds_grid when the game ends to avoid memory leaks. Put this code in your game_end event in the oInventory object:

/// Destroy ds_grid
ds_grid_destroy(global.chest[global.chest_id]);

Conclusion

This post is not meant to be a 100% step-by-step guide. The code provided above is very close to being a completed inventory system, but there are still more things to address. We didn't fully flesh out code for swapping inventory items into a storage chest, that will be a challenge left up to you to learn and complete. If you follow the example code I'm sure you'll find it fairly simple to do. Be sure to calculate the global.chest_id of each new chest that the player creates and initialize the item slots for that new chest when it is created. We also need to consider the input when a player opens a chest, for example by pressing the left mouse button on the chest sprite. We do a check to open the inventory, and then read the contents of the chest from the global.chest[chest_index] array and insert the item properties into the oInventory controller object:

if (closed == false) {
    // Open the Inventory
    if (global.show_inventory == false) {
        global.show_inventory = true;
    }
    global.show_chest = true;                    
    mouse_picked_up = false;
    mouse_item_sprite = noone;
    mouse_item_index = noone;
    mouse_item_sprite_index = noone;
    draw_mouse = false;        
    oInventory.chest_index = chest_id; // Set the chest_id                       
    with (oInventory) { // Fill the chest inventory slots
        for (i=0;i<16;i++) {
            chest_item_sprite[i] = ds_grid_get(global.chest[chest_index],i,1);
            chest_item_index[i] = ds_grid_get(global.chest[chest_index],i,2);
            chest_item_name[i] = ds_grid_get(global.chest[chest_index],i,3);
            chest_item_count[i] = ds_grid_get(global.chest[chest_index],i,4);
            chest_item_type[i] = ds_grid_get(global.chest[chest_index],i,5);
            chest_item_value[i] = ds_grid_get(global.chest[chest_index],i,6);
            chest_item_equipped[i] = ds_grid_get(global.chest[chest_index],i,7);
        }                        
    }                 
}


Leave a Reply

Your email address will not be published. Required fields are marked *