Get SpatialOS

Sites

Menu

Inventory systems

This document is going to discuss some of the considerations to be made when designing an inventory system for use with SpatialOS. It will focus mainly on the decisions to be made when designing the schema so as to support expandability and conserve network bandwidth.

This document only deals with the representation of inventories while the player exists in the SpatialOS world. It does not give guidance on how to persist the inventory to an external data store; for guidance on that, see the Third-party service integration page.

Schema representation

The aim is to synchronise a list of items that a player owns, between workers. Therefore the first decision to make is how to represent a single item within SpatialOS. In order to keep bandwidth usage as low as possible, one might choose to use a single integer for representing the index of the item, which references an index within an item dictionary. This dictionary is information that should be available on all workers, either as static data or possibly something obtainable from a backend service.

item-dictionary

"item_dictionary": {
    ...
    306: {
      "name": "Carrot"
    },
    307: {
      "name": "Axe"
    },
    308: {
      "name": "Spade"
    }
    ...
  }

The simplest implementation of an such inventory would be a list of unsigned integers. For this document we will use uint32s.

component InventoryComponent {
    id = 1020;
    list<uint32> inventory = 1;
}

Simple inventory

This allows the synchronisation of as many items as necessary per entity.

Such a component could be used on any entity that requires posession of a number of objects, ranging from players to lootable corpses or chests.

However, this solution poses a number of limitations. The longer the list gets, the more costly it will be to reorder the list, however this is only a concern if it is required for the order of the inventory to be persisted server side. There is also no concept of an empty slot, which would be needed were you to want to display the inventory as a fixed size grid of items which players can rearrange freely.

To get around these issues, you can use a map instead of a list to hold the items, where the key is the slot index in the inventory, and the value is the item dictionary index:

component InventoryComponent {
    id = 1020;
    map<uint32, uint32> inventory = 1;
}

Grid inventory

This roughly doubles the amount of data that will be sent, but means that each item reference also includes its position in the inventory. It is up to you based on how you intend to display inventory in your game as to whether you feel this is a worthwhile tradeoff.

Stacking, enchantments and other metadata

With this simple map based design, there are still limitations.

This map assumes that each slot in the inventory can only hold a single item, and it also assumes that all instances of an item are identical. Often a game will allow players to “stack” certain items, often up to a limit, within each slot. Also, items may have variations, such a durability left, or enchantments/enhancements.

It is therefore necessary to add extra data to the schema to signify these cases. A naive implementation might be to add additional fields for every possible property an item could have. However, due to the limitations of displaying a stack of differing items, games will usually restrict item variation to items which can not stack. So for example, if you were to add a field for stack count and a field for durability, only one of the two could ever be populated for any item.

To get around this inefficiency, it is beneficial to encode multiple pieces of information into a single field, and then decode this on each worker who needs to read the data.

The following example shows how a single “metadata” field can be used to encode either a stack count, or both an enchantment ID and a durability status. The worker will be able to decide what data the metadata field is encoding based on the “stackable” value of the item in the item dictionary.

type ItemPointer {
  
  uint32 item_id = 1;
  uint32 item_metadata = 2;
}

component InventoryComponent {
    id = 1020;
    map<uint32, ItemPointer> inventory = 1;
}

This system can even support multiple pieces of information in the item_metadata field through the use of bit shifts and masks:

Metadata masking

"item_dictionary": {
    ...
    306: {
      "name": "Carrot",
      "stackable": true
    },
    307: {
      "name": "Axe"
      "stackable": false
    },
    308: {
      "name": "Spade"
      "stackable": false
    }
    ...
  },
"enchant_dictionary": {
    ...
    17: {
      "name": "Purple Glow"
    }
    ...
  }

Metadata masking in a grid inventory

Inventory restrictions

Games will sometimes have additional restrictions on an inventory, such as weight limits, or items that take up multiple slots in the inventory.

When designing an inventory with such special cases, it is important to only synchronise the minimal amount of data required to reproduce a full view of the inventory on each client.

For example, when building an ARPG style inventory, where items can take up multiple slots, the size of each item is static data which can be recorded in the item dictionary. It is therefore only necessary to synchronise a single anchor point of each item in the inventory, as the other slots this item covered can be inferred on each worker.

For example, you might always choose to store the item in the top left slot that it occupies, and the item dictionary will be able to tell you the dimensions of any item from its item ID. This way, the inventory shown below, although it fills 8 out of 12 slots, only requires the synchronisation of 4 items and their anchor positions.

The same logic applies to restrictions such as weight limits, or unique items. You only need to synchronise the indexes or the items in the inventory, as you can work out the combined inventory weight and number of unique items by referencing each item index in the item dictionary.

"item_dictionary": {
    ...
    306: {
      "name": "Carrot",
      "dimensions": {"x": 1, "y": 1},
      "weight": 5
    },
    307: {
      "name": "Axe",
      "dimensions": {"x": 2, "y": 2},
      "weight": 38
    },
    308: {
      "name": "Spade",
      "dimensions": {"x": 1, "y": 2},
      "weight": 25
    }
    ...
  }

arpg inventory

Inventory manipulation

To prevent opportunities for cheating, write access for the inventory component should be maintained server side. This means that any manipulation of the inventory, whether adding/removing items or just moving them between slots, must all be processed on the server in response to communication from the client.

Due to the nature of network communication, any such request will incur latency, which can be preferable to hide from the player. Therefore it is often beneficial to visualise a change as soon as the player makes it client side, and allow the server to send a validation response as to whether the action was actually performed. Once the validation is received, the client can either undo the visualisation of the change, or leave it be, depending on the reponse.

All pending changes should be maintained in a chronological list, so that if any change fails validation, then the client can revert to the component’s current state, and then reapply the visualisation of all pending actions. The need for this will be most obvious in a system where players can drag items between slots. Without the client side prediction, players would always see the dragged item snap bag to its original position, until the round trip has been completed and the server has applied the new position of the item.

Other considerations

The inventory concept can be extended even further:

Multiple bags

So far, implementations have all assumed a single bag, of a fixed size known by the client and server. An alternative approach might involve multiple bags, of variable size and quantity.

In order to support such a system, you could extend the key of the map to contain both a slot index and a bag identifier. Additionally, a list of which bags the player has and their slot count might need to be synchronised.

type ItemPointer {
    uint32 item_id = 1;
    uint32 item_metadata = 2;
}

type Bag {
    map<uint32, ItemPointer> inventory = 1;
    uint32 bag_size = 2;
}

component InventoryComponent {
    id = 1020;
    list<Bag> bags = 1;
}

Equipment

Often games will allow players to equip certain items on their character. These could have a visual effect, and also affect gameplay stats. As the number of available equipment “slots” is defined in advance, you can use a set number of fields to define what items are in these slots. As the field will always exist, it is required to have a way to know if the equipment slot is empty or filled. This can be accomplished with an “Option” field.

The solution shown here assumes that when the item is equipped, it no longer exists in the bag. If you instead wanted to show the item in both the bag and in the equipment slot, you could change the Option to an Option so as to reference the stored item.

type ItemPointer {
    uint32 item_id = 1;
    uint32 item_metadata = 2;
}

type Bag {
    map<uint32, ItemPointer> inventory = 1;
    uint32 bag_size = 2;
}

type SlotPointer {
    uint32 bag_index = 1;
    uint32 slot_index = 2;
}

component InventoryComponent {
    id = 1020;
    list<Bag> bags = 1;
}

component EquipmentComponent {
    id = 1021;

    Option<ItemPointer> head_slot = 1;
    Option<ItemPointer> chest_slot = 2;
    Option<ItemPointer> legs_slot = 3;
}

Hotbars

Some games also implement a hotbar on the UI, where players are provided a quick access button to items in their inventory. Sometimes these bars can also include a reference to spells or abilities, but those will not be covered in this doc.

If you assume that a hotbar is only a reference to an item in the inventory, then this data doesn’t necessarily have to be synchronised to the server at all. However you might want to ensure the player is able to access their hotbar setup from any computer, and would therefore want to persist this information with the rest of their player data.

Some games will treat the hotbar as a special set of inventory slots, which can hold items in the same way as a bag, just exposing them in a different way in the GUI. Other games such as MMOs however, will often simply place a reference to an item on the hotbar. For example, a player might drag a weapon from their bags onto a slot, allowing for a quick weapon change, and also drag a stack of dynamite on to a separate slot for easy access.

You will want to be sure that when a player hits the weapon button, the exact weapon they dragged to that slot is the one they equip, while stackable items such as dynamite, you might want to automatically start using other dynamite stacks when the first one runs out. It is therefore necessary to synchronise both the slot of the item they dragged onto the hotbar, as a “SlotPointer” and also what item the hotbar is referring to, as an “ItemPointer”.

type ItemPointer { 
    uint32 item_id = 1;
    uint32 item_metadata = 2;
}

type Bag {
    map<uint32, ItemPointer> inventory = 1;
    uint32 bag_size = 2;
}

type SlotPointer {
    uint32 bag_index = 1;
    uint32 slot_index = 2;
}

type HotbarPointer {
    Option<SlotPointer> referenced_slot = 1;
    ItemPointer referenced_item = 2;
}

component InventoryComponent {
    id = 1020;
    list<Bag> bags = 1;
}

component Hotbar {
    id = 1021;
    map<uint32, HotbarPointer> hotbar_slots = 1;
}

When any changes occur in the inventory, it will be necessary to also update any affected references. For example, if the player moves the references sword between inventory slots, any hotbar slots referencing the old slot should be redirected to the new slot. As for the stackable dynamite item example, whenever the referenced stack becomes empty/removed, you should redirect the hotbar to the next available stack of the same item ID. When there are no available stacks, the Option can have no value, to show that no more dynamite is available.

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums