Introduction

About

exINV is a general purpose inventory system that provides ready to use assets to manage an item database, multiple grid based inventories, and examples on how to develop a user interface and logic on top of it, including features like crafting and equipment selection.

exINV can be seen as a collection of three modules: Database system, Inventory system and User Interface (UI).

It is important to understand that while the Database system & the Inventory system are generic enough to be used as they are in any project, the UI and its logic may differ greatly from one project to another, depending on the desired result.
The UI and the other modules are, however, totally decoupled. This means that you can safely import all the ex_* scripts into any project and they'll work out of the box, since they do not reference any pre-defined object.
The provided UI instead is a structured and commented example built on top of that; you will need to edit and adapt it to your needs, or simply start from scratch and build your own.

Installation

Open GameMaker: Studio, log in to the Marketplace and navigate to the Library tab. From here you can load the extension resource into your project.
If you are adding the system to an existing project, just add all the resources from the "inventory" folders.
If you want to test the provided example, import everything. It is suggested to start by importing everything into a blank project and unserstand how the system is structured before adding it to your projects.

Getting started

The first thing you want to do is call the initialization script, and load the item database (only once per game run):

ex_init();
ex_db_load("inv_armor.csv","inv_food.csv","inv_potions.csv","inv_weapons.csv");

Database items are stored as external resources in csv format, and multiple files can be loaded at once. Database files require a specificic format (see the Database System section for details) which, among other things, links every item to a unique, string based, identifier (called key), used throughout the system to refer to database items.

You can then start creating inventories and add, remove, count and search items into them, as in the following (pointless) example:

/* creates an inventory with 10 slots. The variable player_inv keeps a reference to the inventory, that is actually a ds_map with all the related data, including max size, item count, and actual item data. */
player_inv = ex_inv_create(10);

/* add a random number of different types arrows (stackable items) into the the empty slots of the inventory */
var fire_arrows_amount = ex_inv_add(player_inv,"weapon_fire_arrow",irandom(128));
var ice_arrows_amount = ex_inv_add(player_inv,"weapon_ice_arrow",irandom(128));

//write the amount inserted to the console
show_debug_message(string(fire_arrows_amount) + " fire arrows have been added to the inventory");
show_debug_message(string(ice_arrows_amount) + " ice arrows have been added to the inventory");

//you can find the amount of a certain item at any time using ex_inv_count
show_debug_message(string(ex_inv_count(player_inv,"weapon_fire_arrow")) + " fire arrows found in the inventory");
show_debug_message(string(ex_inv_count(player_inv,"weapon_ice_arrow")) + " ice arrows found in the inventory");

//you can also return how many slots are occupied by reading the "size" property of the inventory
show_debug_message(player_inv[? "size"]);

Note that the above does not generate anything that the player can see or interact with, it's all in memory and persistent across room changes.

Database system

Introduction

The database stores a collection of all the items in your inventory system alogn with their properties, and allows a fast and easy access to this data.

Data files

Database data is stored in one or more CSV files located in the included files of your project. They can be loaded using ex_db_load(file1,file2,...). Each file can define different properties for the items it contains, but there are some requirements:

  • The first row has to hold column names.
  • The second row has to hold column types, either string or real, defining the type of data in the column.
  • You are required to define a column key of type string, holding a unique (among all files!) identifier per item
  • You are required to define a column stack_size of type real, holding the maximum stack size for the item (1 for non stackable items)

Please refer to the included sample files for an example

Database structure

The database itself is actually a ds_map holding ds_maps for every item, referenced by item key.
Along with the database, a ds_list of all keys is also generated and accessible with ex_db_keys(). This can be useful for example when you need to get a random item, as it's enough to get a random element from all the database entries.

Scripts reference

ex_db_get(key)

Returns a ds_map holding all the data of the item with the specified key.

var item = ex_db_get("weapon_fire_arrow"); //get the fire arrow item data
var item_name = item[? "name"]; //get the name property of the item
ex_db_keys()

Returns a ds_list where all database keys are stored. This can be useful for example for selecting a random item, or determining the size of the database.

var keys = ex_db_keys();
var keys_count = ds_list_size(keys);
var random_item_key = keys[| irandom(keys_count)-1];
ex_db_load(file1,file2,...)

Loads data from one or more CSV files (as described above) into the database.

ex_db_load("inv_armor.csv","inv_food.csv","inv_potions.csv","inv_weapons.csv");

Inventory system

Introduction

Inventories are data structures that reference database items and are, therefore, stored and kept in memory as long as the program is running or the inventory explicitly destroyed.

While inventories are meant to be displayed and interact with the player input, in exINV they are not tied to a visual representation, since that's the UI role. You can create and perform operations on inventories without ever actually displaying them.

The only exception to the above is represented by the script ex_inv_updated, which is called every time an inventory changes, and should include the code required to update the visual representation of the given inventory.

Structure of an inventory

An inventory is a ds_map holding the following data:

max_size: the number of slots of the inventory
size: the amount of occupied slots
items: a ds_grid holding the keys of all the items in the inventory slots, as well as the respective amount per slot. You shouldn't work directly with the the items grid, but you may need to understand its structure for customization purposes. The items grid has the following columns (in order): row number (used to revert sorting operations), item key, amount, base key (for unique items), item data (ds_map).

The following example outputs the number of free slots of an inventory to the console:

var free_slots = inv[? "max_size"] - inv[? "size"];
show_debug_message("The inventory has " + string(free_slots) + " free slots");

Structure of an item

items, referenced in any inventory by their keys, are references to their respective data in the database, which is stored in a ds_map.

It is important to note that every item of the same type (that is, having the same key), references the same data in the database. If you for example change the name of an item by calling item[? "name"] = "something"; you are in fact changing the database, and all items of that kind will get renamed. In order to have items with unique attributes, an item has to be marked as unique.

Other than the properties defined in the database, an item provides the following information in its ds_map:

unique: true or false, keeps track if an item has been marked as unique
base_key: the item key in the database. This is always the same as the item key, unless the item is unique, in which case base_key keeps a reference to the original item key.

The following example gets contents of the 8th slot of the inventory. If the slot is not empty, it prints the item name in the console.

var item = ex_inv_get_item(inv,8);
if(item >= 0) { show_debug_message(item[? "name"]); }
else { show_debug_message("The 8th slot is empty"); }

Unique items

Unique items are essentially base items with independent properties that can be modified on the fly.

To mark an item as unique, you'll have to call ex_inv_item_set_unque on an inventory slot. This will mark all items in that particular slot stack at that moment as unique. After the call you can freely edit the item data ds_map, like: item[? "name"] = "Unique sword" .

Unique items require a new key to be specified, and therefore do not stack with base items, or other unique items, of the same type, unless you specify the same key for multiple unique items.

Scripts reference

ex_inv_add(inv,item,amount)

Inserts the specified amount of items of the specified type in the inventory.
If the item is stackable, and there's already one or more non-full stacks of the item, those are filled first, and the reminder is added to one or more empty slots.

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
Unique items can only be inserted, as a copy, by passing the item reference.

Returns: (real) The actual amount of items inserted

var inserted = ex_inv_add(player_inv,"weapon_fire_arrow",64);
ex_inv_add_slot(inv,item,amount,slot)

Inserts the specified amount of items in the inventory slot. If the item is stackable and there's already an item with the same key in the slot, the item is added to the existing amount. If there's already an item in the slot that's different from the one to be inserted, no action is taken.

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
Unique items can only be inserted, as a copy, by passing the item reference.

Returns: (real) The actual amount of items inserted

var inserted = ex_inv_add_slot(player_inv,"weapon_fire_arrow",64,0);
ex_inv_count(inv,item)

Counts the total number of items of the given type present in the inventory.

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
If item is a key, all items having that base type (including unique items) will be counted.
If item is an item reference, only items having the same primary key will be counted (e.g: if the item references a unique item, only those are counted. Conversely, if it references a base item, only non unique items of that type are counted).

Returns: (real) The number if items matching key

var fire_arrows_count = ex_inv_count(player_inv,"weapon_fire_arrow");
ex_inv_create(slots)

Creates a new empty inventory with the given number of slots

Returns: (real) The newly created inventory

equipment_inv = ex_inv_create(8);
ex_inv_destroy(inv)

Destroys the given inventory along with its data.

ex_inv_destroy(equipment_inv);
ex_inv_find(inv,item,ex_find_type)

Returns the index of the slot holding the given item.

ex_find_type determines the search operation to apply, and is either one of the following enum values:

ex_find_types.first : search for the first slot with an item having the given key
ex_find_types.last : search for the last slot with an item having the given key
ex_find_types.low : search for the slot with the lowest stack amount of items having the given key
ex_find_types.high : search for the slot with the highest stack amount of items having the given key

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
If item is a key, all items having that base type (including unique items) will be considered.
If item is an item reference, only items having the same primary key will be counted (e.g: if the item references a unique item, only those are considered. Conversely, if it references a base item, only non unique items of that type are considered).

Returns: (real) The index of the slot, or -1 if no item with the given key is found.

var first_slot_holding_carrots = ex_inv_find(player_inv,"food_carrot",ex_find_types.first);
ex_inv_get_amount(inv,index)

Returns amount of items in the slot with the given index.

Returns: (real)

ex_inv_get_amount(equipment_inv,0);
ex_inv_get_item(inv,index)

Returns the item in the slot with the given index. Empty slots will return -1.

Returns: (string)

ex_inv_get_item(equipment_inv,0);
ex_inv_get_key(inv,index)

Returns key of the item at the given slot index

Returns: (string)

ex_inv_get_key(equipment_inv,0);
ex_inv_item_set_unique(inv,slot,new_key);

Marks an item stack in a specific slot as unique, allowing its properties to be changed. See this chapter introduction for more info on how unique items.

new_key is the new unique key assigned to that item. Items with the same key will be able to stack.

Returns: (boolean) true if successful, false is slot is empty

//set the item in slot 0 of player_inv as unique, and changes its name
ex_inv_item_set_unique(player_inv,0,"unique_"+item[? "key"]);
item[? "name"] = "My precious";
ex_inv_read(string)

Creates and returns a new inventory from a string generated previously by ex_inv_write, loading all its contents.

Returns: (inventory)

save inventory into an ini file
ini_write_string("Player", "inventory", ex_inv_write(inv));

destroy the inventory and load it again from the ini file
ex_inv_destroy(inv);
inv = ex_inv_read(ini_read_string("Player", "inventory", ""));
ex_inv_remove(inv,item,amount)

Removes the specified amount of items from the inventory, independently from their slot position.

If the given amount is -1 or exceeds the total number of items with key, all those items are removed from the inventory.

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
Unique items can be removed explicitly by passing their item reference.

Returns: (real) The actual amount of items removed

var gold_removed = ex_inv_remove(player_inv,"item_gold",1200);
ex_inv_remove_slot(inv,amount,slot)

Like ex_inv_remove, this function removes the specified amount of items with the given slot, but is limited to the specified slot, and doesn't require to specify an item.

If the given amount is -1 or exceeds the total number of items with key, the slot is emptied.

Returns: (real) The actual amount of items removed from the slot

var items_removed = ex_inv_remove_slot(player_inv,32,2);
ex_inv_set_slot(inv,item,amount,slot)

Replaces the item currently in the specified slot with the given item and amount.

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
Unique items can only be inserted, as a copy, by passing the item reference.

This is useful to force and overwrite an item into a specific slot independently of what's already there.

Returns: (real) The actual amount of items inserted in the slot

var item_inserted = ex_inv_set_slot(equipment_inv,"weapon_flame_sword",1,0);
ex_inv_sort(inv,ascending)

Reorganizes the inventory sorting all slot contents (empty ones included) based on item key. Ascending (either true or false) determines the sort order

ex_inv_sort(toolbar_inv,false);
ex_inv_switch(inv1,index1,inv2,index2)

Switches the contents of two slots from the same or different inventories.

ex_inv_switch(player_inv,0,toolbar_inv,0);
ex_inv_test(inv,item,amount)

This function works exactly like ex_inv_add(), but doesn't actually insert the item, it only returns the amount of items that could potentially be inserted.

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
Unique items can only be tested by passing the item reference.

Returns: (real) The amount of items that can be inserted with the same call to ex_inv_add(), but doesn't actually insert them.

var amount = ex_inv_test(player_inv,"weapon_fire_arrow",64);
ex_inv_test_slot(inv,item,amount,slot)

This function works exactly like ex_inv_add_slot(), but doesn't actually insert the item, it only returns the amount of the specified item that can potentially be inserted by passing the same arguments to ex_inv_add_slot().

Item can either be an item reference (as returned from ex_inv_get_item) or an item key.
Unique items can only be tested by passing the item reference.

Returns: (real) The amount of items that can be inserted with the same call to ex_inv_add_slot(), but doesn't actually insert them.

var amount = ex_inv_test_slot(player_inv,"weapon_fire_arrow",64,0);
ex_inv_updated(inv,affected_slots)

This function is called automatically when an inventory changes in some way. The purpose of this script is to perform a callback to the UI whenever an inventory is updated by an add / remove / sort / etc. operation.

The developer is supposed to adapt this function to its needs depending on how the UI is programmed.

inv is the inventory affected by the update, and affected_slots is a ds_list of slot indexes in inv that have been affected by the update.

ex_inv_write(inv)

Returns a (JSON formatted) string containing all inventory data, that can be turned into an inventory again using ex_inv_read().

Returns: (string)

save inventory into an ini file
ini_write_string("Player", "inventory", ex_inv_write(inv));

destroy the inventory and load it again from the ini file
ex_inv_destroy(inv);
inv = ex_inv_read(ini_read_string("Player", "inventory", ""));

User Interface

Introduction

The user interface determines how inventories are displayed to the user, and all the required logic that handles how the inventory behaves when the user interacts with them.

As such, if the inventory and database system are generally project agnostic, the UI is instead an opinionated resource, and differs from game to game. exINV shows a way to program an UI for performing some common operations like crafting, player inventory, toolbar and equipment, but it's up to the programmer to refine or rewrite this part based on his needs.

It is important to understand that the inventory itself and the UI are totally decoupled, meaning that there is no reference to objects or instances inside ex_inv scripts, with the notable exception of ex_inv_updated . This is the script that ties UI and Inventories, and is in charge of notifying the UI that something changed inside the inventories.

The following is a description of how the UI system works in the provided example, but it is in no way the only or perfect way to do it.

Inventory controller

obj_inv_controller is in charge of the player inventory management as a whole. It creates and keeps a reference of all the inventories related to the player, and performs general purpose actions like saving, resetting and randomizing the inventory, etc.

Panels & Slots

Panels are a visual representation of an inventory, and hold a number of slot instances linked with it.

Every inventory requiring a visual representation defines its own panel object, having obj_inv_panel as parent. A crafting inventory for example could be associated to a obj_inv_crafting panel, while a chest to obj_inv_chest panel, defining their own specific behaviour (if required).
Note that obj_inv_panel is never instantiated directly.

A panel defines the following properties:

inv: The inventory referenced by the panel
slots: An array of slot instances that matches the size of the slots in inv

The panel works as a controller for the slots, meaning that all the logic for that specific inventory happens in the panel, not the slots, that are used only to show the items and catch the user interaction.
For this reason, a panel has a number of user events that get called when its slots are clicked or updated.

Panels can be created and destroyed freely without affecting the inventory. The provided scripts inv_panel_show() and inv_panel_hide() do in fact destroy and recreate the panel every time respectively. This means that a hidden panel is not just visually hidden, but doesn't exist at all as long as inv_panel_show() is called again, a good way to increase performance when managing a lot inventories.

This kind of setup allows for complex, panel specific behaviour and graphics. For example, the panel obj_inv_equipment defines, in its slot changed callback event, the code to compute the armor rating of the player, that cycles through all its slots and checks the defense value of each armor piece.

Bonus: try to call inv_panel_show() on the same inventory, at different positions to see what happens :)

Panels events

As described above, panels have a number of events (defined as user events in the object) that may or may not be used to program a specific behaviour. Here's a list of the ones defined in the example:

after create (event_user_0): Called right after the panel is created. Slot instances are created in this event.
inventory changed (event_user_1): called after the inventory changes in some way. This is used in obj_inv_equipment to update the armor rating whenever something happens to the inventory, or in the crafting inventory to test if there's a matching recipe.
slot left pressed (event_user_2): called when a slot is left clicked. In this event, you can use other to refer to the specific slot
slot right pressed (event_user_3): called when a slot is right clicked. In this event, you can use other to refer to the specific slot
slot updated (event_user_4): called when a slot is updated. In this event, you can use other to refer to the specific slot
panel cleanup (event_user_15): called when the panel instance is destroyed or the room changes

Extending panels events

Panel events are meant to be extended by the developer. If, for example, you want something to happen when the middle mouse button is pressed on a slot, you'll have to:

  • 1. add the event name to the panel_events enum in the game start event of obj_inv_controller, assigning the event user number you want to associate it to. Something like slot_middle_pressed = 5
  • 2. Add a middle mouse pressed event to obj_inv_slot that just calls the event for the panel: with(panel) {event_user(panel_events.slot_middle_pressed)}
  • 3. Add an event user 5 to obj_inv_panel, and add the code that should be executed when this event gets called. If the code is not common to all or most of the panels, leave it blank and implement it in the specific panels that need it.

Slot instances

Slot instances are tied to a specific panel and a specific inventory. They are used to diplay the item held in a certain slot of an inventory, as well as detect user interaction (left click, right click, etc.).
Slot instances hold the following properties:

item: Reference to the database ds_map holding the item properties (-1 if empty)
key: Key of the item currently in the slot ("" if empty)
amount: Number of items in the stack
rel_x: x position of the slot relative to the panel
rel_y: y position of the slot relative to the panel
panel: Rerefence to the panel holding the slot instance
inv: Reference to the inventory
index: Index of the slot in the inventory

It is important to understand that slots are totally dependent on inventory data, changing for example the item, amount or key variables of the slot instance will do absolutely nothing to the inventory itself.
Slots are automatically updated whenever the inventory changes, and this is thanks to the ex_inv_updated script (see reference) that is in charge to notify the UI that something must be updated.

Crafting

Crafting has probably the most complex behaviour of the included example, it's therefore useful to explain in a few paragraphs how that part works and its limits.

First, at game start, obj_inv_controller defines a list of crafting recipes. A crafting recipe is a ds_map holding: recipe result, amount of resulting item per recipe, a ds_list of 1 to 4 required ingredients (item keys). This list is compared to the items in the crafting table when checking if a recipe is valid.

Then a panel of type obj_inv_crafting is created, with a total of 5 slots. The first 4 slots are dedicated to ingredients, while the last holds the resulting item (if any). The panel specifies a different logic for the last slot, prohibiting anything from being inserted there and consuming ingredients when the player actually get something out of it.

obj_inv_crafting also generates and keeps an updated ds_list of item keys based on the 4 ingredients slots, which is then sorted and compared to all recipe ingredient lists whenever an item is inserted or removed from the ingredient slots. By sorting both recipe and ingredients lists, we ensure that the order of the items plays no role in determining if a recipe is valid.

Keep in mind that this approach is relatively simple, but also limited in scope. It does not consider, in contrast for example with minecraft, the number of ingredient items in the same slot, nor the position in the crafting grid

Credits

Contact me on the YoYoGames forums or by email at simoneguerra<at>ekalia.com