Managing State in Adventure Games

One of the things that I got stuck on last year while developing AdventureKit was tracking state. After coming back to it again this year I realise I was coming at it from the wrong direction.

Instead of implementing some sort of global state manager that tracks every single entity across all the scenes, I could just let the individual entities keep track of their own state.

This proved to be robust, extendable, and easier to manage. Components and systems could update the state via the entity at appropriate points. This means that just by adding a component to an entity, that component’s values automatically get saved and loaded without any further work on my part.

Saving and Loading

The state struct keeps track of the following properties:

struct ItemState : Codable {
    var useCount : Int = 0
    var position : CGPoint = .zero
    var stateName : String = "default"
    var inventoryItems : [String]? = nil
    var variables : [String : Int] = [:]
}

Each entity then initialises a state manager as soon as it’s created. This class manages the saving and loading and provides an API for updating these values.

Rather than have an external global object set an entity up, each entity can read from this struct and set itself up depending on the values.

A Big Bucket O’ Bits

For AdventureKit, I’m using UserDefaults as the storage mechanism.

There’s a master dictionary where the keys are the IDs of the entities and the values are encoded ItemState structs.

The manager uses a JSONEncoder to encode the structs and the Data object that it creates is then stored in UserDefaults. This is because the ItemState struct stores a CGPoint, a type that cannot be stored directly in a property list.

It can, however, be encoded by the Codable protocol into JSON data and that can be stored as a Data object.

Right now, my games are small enough that using UserDefaults isn’t a problem but I recognise I am pushing the limits of what this API was intended for.

Look at the State of This

The player entity is just another item in a scene and is derived from the same Item class (a subclass of GKEntity) as everything else. It uses the same ItemState struct to store its state and only becomes a playable character thanks to special abilities that it gets from additional components.

One of these additional components is an inventory.

Saving the Inventory

The second breakthrough I had with tracking state was realising that all the inventory items that can possibly exist in the entire game should be stored in a separate inventory file which is loaded with every scene.

Each inventory item has a unique identifier. Saving an inventory simply means saving an array of these identifiers, which happens every time an item is added to or removed from that inventory.

Loading an inventory involves reading back these strings and looking up the relevant details from the inventory file (which contains things like the sprite name and what the player says when it they look at the item). It then uses this data to initialise another Item entity and adds this to the character’s inventory.

For my games at the moment, only the player needs an inventory component. However, I could add one to any other character (or even have multiple player characters like in Day of the Tentacle or the Blackwell games) and AdventureKit would start saving and loading the inventory for those characters automatically.

Location

Similarly, adding a move component to a character or item is all that’s required to start saving its position. Every time this character or item finishes moving somewhere within a scene, the component writes that final location to its entity’s state struct.

When that character or item is next loaded, the entity first checks the state struct for its position before falling back on a default position.

Object and Non-Player Character States

Every item and NPC can have multiple states. Each state can have different attributes (e.g. what the player says when they look at the item/NPC, or how they respond when a player interacts with them).

The solution I came up with here was to make each state a dictionary in the game data JSON, where each key was the state name:

"item.garbage" : {
    "name" : "Garbage",
    "position": [ 871, 20],
    "states" :  {
        "default" : {
            "image" : "Item-Garbage", 
            "look": [
                "It's an old box that's falling apart."
            ],
            "facingDirection": "east"
        },
        "state.kicked" : {
            "image" : "Item-Garbage-Open", 
            "facingDirection" : "west",
            "look": [
                "Hmm. There's something written on the inside."
            ]
        }
    }
}

These state dictionaries contain all the information for how that item or character responds to the player. Any external system can then change an item or character’s state by setting a single property on the Item entity.

Changing this property instantly changes the state of that entity, including its appearance. It also updates the state name in the ItemState struct.

When a scene is next loaded, the entity checks the state manager to see which of its named states it should load.

Removed Objects

Removing an item (e.g. when a player picks it up) from a scene is handled differently. The entity still exists in the scene, but its state is set to state.removed. It has no graphical representation in the scene and cannot be interacted with.

This allows a removed entity to be saved in the same way as any other—as a string representing the currently selected state—and means that there’s no further tracking required to keep tabs on what’s been removed from a given scene.

Saving the Current Scene

The only other thing that I need to keep track of is the last scene the player was in.

This needs to be stored separately to the entity state dictionaries as it’s required when the game is set up, not just the scene, as it tells AdventureKit which scene to load initially.

Each room has a unique name. When the player enters a new scene, the scene name is written to its own key in UserDefaults.

Issues

This system has so far proven to be surprisingly robust and adaptable, but it is not without its issues:

Right now, every entity has its own state manager. This is fine as it’s currently only reading to and from UserDefaults.

Apple has optimised the hell out of this system and my understanding is that the underlying disk writes are batched, so lots of frequent writes to UserDefaults do not necessarily mean there’s a corresponding number of writes to the file system.

If in future I need to move the storage so that I am interacting with the file system directly then I’d probably want to centralise this. Having each item in a scene read and write from disk for every tiny change might be too much.

However, the amount of data I’m saving for the size of game I’m planning is small enough that UserDefaults is a reasonable choice.

For the moment, at least.