How Would You Approach Game State for a Narrative-Driven Game?

Hey there!

I know there are about a thousand threads about game state, but from what I’ve seen, the solutions don’t seem to quite fit the structure of my game. I’m making a story-driven game, one that, based on certain decisions you make, changes what happens during your playthrough, which ending you get and what objects and characters load in different scenes.

I’m trying to figure out how to structure my game state architecture. I want the game to clock which path the player is on once making the pivotal decision, and I also want to have the game check how far the player is along the path once they’re on it.

I’ve considered making a pre-load scene, adding a StateManager object and adding a script with a long list of bools or switch-cases, but this seems ungainly for a bigger, more complicated game like mine. I’ve also considered making a number of Scriptable Object bools that can persistent between scenes, or, say, a single GameStateManager Scriptable Object that houses a list of bools I’d use for game state.

Maybe I have a persistent script that changes game state or switches a bool, and then individual scripts in each scene that check which state or bools are activated in OnEnable() and load the scene from there?

Does anyone have examples of what they’ve done, or any advice to help get me started?

(If there’s another thread out there you think answers my question, please send it my way!)

Thanks so much?

Lets ignore saving the game state right now.

And instead I ask: How do all the objects in your scene know what state they’re in currently?

When an event occurs in game that causes the narrative to branch, how are all the objects in scene aware of which branch you’re on? Like is there some narrative tree that every object can check to know which point in the narrative it’s in and they behave based on that? Or is it when an event occurs, objects are toggled to a specific state (enabled, disabled, components added/removed, prefabs spawned, etc).

Now because I don’t know how your game is structured I’m not sure how much this following is going to help. It all hinges on how you setup your game. But I’ll show you how we did it in one of our games.

In our games there are a lot of different things that go on and the states of objects narratively and gameplay structure wise. But I’ll use some of it to give an example… here is a puzzle we call “the shy bathers”:

The puzzle can be in 1 of 5 states based on the last thing you’ve done to it. You can see the 5 states on the right defined in our ‘I_TriggerSequence’ script.

As you interact with the puzzle and successfully complete a state, the I_TriggerSequence is signaled to proceed forward one step. And it signals to that state that it’s entered it.

So here is State3:

So when it gets triggered the ‘I_Trigger’ script fires off and it performs all of those actions. It destroys some gameobjects, enables some others, putting the whole puzzle into the state it needs to be for it. In this case it’s destroying some unused ‘hijack’ (these are script that hijack an interaction point and modify its behaviour), then enables its own hijack (in this case the matron, so when you click on her you get dialogue relative to that state of the puzzle), enables a combinationlock (the puzzle itself), and enables an interactable called ‘e.Message Unsolved’ which is just an interaction point that prompts you if you try to pick up the goal item that it can’t be yet because it’s locked.

You’ll also notice though this ‘Cutscene Event’ script with an trigger/event on it (these are like UnityEvent, but our own custom version, this was all developed before UnityEvent was really a thing. It also does more than just call methods on scripts… it can call method/disable/enable/destroy/trigger all/more). The trigger/event is called “On Skip Index”.

This is called if the Cutscene Manager skips pass this state. This way the state can do any clean up. Effectively it does a quick pass of what should have happened if you went through this state actively.

What happens is that the Cutscene Manager:

This is what saves our game state. It implements an interface called “IPersistentObject”:

    public interface IPersistentObject
    {

        /// <summary>
        /// The earliest load event, this occurs immediately after the scene has loaded.
        /// Awake has been called on all scene native objects, but Start has not been called yet.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="reason"></param>
        void OnSceneLoaded(ScenarioPersistentData data, LoadReason reason);

        /// <summary>
        /// Signals that the object should load its state. This usually only happens when a scene
        /// is loaded. This will occur as part of the Start action if implemented as a Mixin,
        /// otherwise it'll occur after all Starts.
        /// Use 'OnSceneLoaded' if you need to occur before any Start has been called.
        /// </summary>
        /// <param name="data">The data object used for loading.</param>
        /// <param name="reason">The reason the load occurred.</param>
        /// <param name="status">The current LoadStatus of the ScenarioController.</param>
        void OnLoad(ScenarioPersistentData data, LoadReason reason, LoadStatus status);

        /// <summary>
        /// Signals that the object should save its state as some major change is occurring.
        /// Either the player requested to save, or the scene is transitioning.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="reason"></param>
        void OnSave(ScenarioPersistentData data, SaveReason reason);

    }

Basically when a scene is loaded (note that in our game you can back track between scenes… so state doesn’t just have to be saved between playthroughs, but also when traversing the world between different regions of it). So basically when a scene is loaded all objects in the scene that implement this interface have these methods called on them. OnSceneLoaded to signal the scene has loaded and we’re almost ready to start the load of the state process (think of like ‘Awake’, but for save state). The OnLoad occurs just after Start, this is the proper load complete (think of like Start, but for save state). And lastly ‘OnSave’ which signals to everything that it needs to save its data.

Note that each has a “ScenarioPersistentData” object passed in and some enums for status information. This ScenarioPersistentData is a container object that an individual object can stick its state info (it’s what will get serialized and saved to disk).

So in that CutsceneSequenceManager script it had a “UID”, that’s what it uses to identify itself in the ScenarioPersistentData, here you can see its implementation of IPersistentObject:

        #region IPersistentObject Interface

        void IPersistentObject.OnSceneLoaded(ScenarioPersistentData data, LoadReason reason)
        {
            if (!_uid.HasValue) return;

            Token token;
            if (!data.TryGetData<Token>(_uid.ToString(), out token) || token == null) return;

            var seq = this.GetComponent<i_TriggerSequence>();
            if (seq == null) return;

            seq.CurrentIndex = token.SequenceIndex;
        }

        void IPersistentObject.OnLoad(ScenarioPersistentData data, LoadReason reason, LoadStatus status)
        {
            if (status == LoadStatus.SceneLoaded) return; //was handled in early load
            if (!_uid.HasValue) return;

            Token token;
            if (!data.TryGetData<Token>(_uid.ToString(), out token) || token == null) return;

            var seq = this.GetComponent<i_TriggerSequence>();
            if (seq == null) return;

            seq.CurrentIndex = token.SequenceIndex;
        }

        void IPersistentObject.OnSave(ScenarioPersistentData data, SaveReason reason)
        {
            if (!_uid.HasValue) return;

            var seq = this.GetComponent<i_TriggerSequence>();
            if (seq == null) return;
          
            var token = new Token();
            token.SequenceIndex = seq.CurrentIndex;

            data.SetData<Token>(_uid.ToString(), token);
        }

        #endregion

So really all its doing is storeing an “index” into the data container with a key that is its UID. When the game loads this will set its index in the OnSceneLoaded method (before Start). Then on Start it does this:

        protected override void Start()
        {
            base.Start();

            var seq = this.GetComponent<i_TriggerSequence>();
            if (seq.CurrentIndex > 0)
            {
                if (_ignoreSkipIfCompleted && seq.CurrentIndex >= seq.TriggerSequence.Count)
                {
                    //do nothing
                    _onSequenceLoadedCompleted.ActivateTrigger(this, null);
                }
                else
                {
                    int c = Mathf.Min(seq.CurrentIndex, seq.TriggerSequence.Targets.Count);
                    for (int i = 0; i < c; i++)
                    {
                        var ev = ObjUtil.GetAsFromSource<CutsceneEvent>(seq.TriggerSequence.Targets[i].Target);
                        if (ev != null) ev.OnSkipIndex.ActivateTrigger(ev, null);
                    }

                    if(seq.CurrentIndex >= seq.TriggerSequence.Count)
                    {
                        _onSequenceLoadedCompleted.ActivateTrigger(this, null);
                    }
                }
            }
        }

Basically it grabs all of those CutsceneEvents and if the index of that CutsceneEvent is BEFORE the current index, it calls “OnSkipIndex” on it. Allowing that state to perform whatever it should have done if it actively was stepped through. Allowing this puzzle to configure itself to the state it needs to be.

Of course this specifically is a linear thing. No branching in this example.

But branching can be handled number of ways. Because we don’t actually represent the entire structure of the game as a massive god object. But instead as small little set pieces (like this puzzle is just a single set piece). A set piece can lead into another set piece.

So take this safe here:

It contains a script called “PersistentStateMachine”.

Basically this just has a UID and index again like the CutsceneSequenceManager did. BUT instead of the index representing a sequence, it just represents an arbitrary state index.

All it does in the Start event is trigger the state that matches its current index:

        protected override void Start()
        {
            base.Start();

            if (_stateTriggers != null && _currentState >= 0 && _currentState < _stateTriggers.Length)
            {
                _stateTriggers[_currentState].ActivateTrigger(this, null);
            }
        }

Anything can tell this thing to change its state, and when it saves it’ll remember which state its in.

So lets say you have a sequence that branches at the end… really what would happen is in the final step of the sequence (say when you solve the bather puzzle you got to pick between 2 choices)… what would happen is there’d be a “PersistentStateMachine” out there that had 3 states:
0 - not used yet
1 - choice 1
2 - choice 2

Once solved the puzzle would switch this persistent state machine to the index it should be in for its state.

When the game loads next time the puzzle just goes through its sequence disabling itself. And then this state machine would load into its state and kick off whatever sequence it needs to do (maybe choice 1 unlocks a door, and choice 2 unlocks a window).

Mechanical branching done.

As for a stronger narrative structure. Like we’re talking branching dialogue trees and the sort (like mass effect or something). This would NOT be a good choice for that.

For that… you’d likely have a “script” (as in like a actual narration script). Maybe in xml or something. The structure of that script would break out all the dialogue trees. Each node in the dialogue tree would get a UID associated with it.

Each character you could talk to would have a persistent token that stores which specific node it’s on (by the uid). When you interacted with that person, it’d just look at where it is in its narrative tree by that uid and just start there.

If there is multiple starting places because context effects them. Then you’d just have a context tree which describes all the possible contexts. And you’d have the dialogue tree node associated with the context tree. These relationships would be saved instead. Then when you interacted you’d calculate which context is significant, and then start in the dialogue tree at the node associated with that context.

1 Like

Am thinking the same thing. I’m making a text driven adventure game. (see signature) The most logical conclusion I came up with is some sort of tree branching strucutre.

I was looking at this over the weekend amongst learning unity ui

delegate void TreeVisitor<T>(T nodeData);

class NTree<T>
{
    private T data;
    private LinkedList<NTree<T>> children;

    public NTree(T data)
    {
         this.data = data;
        children = new LinkedList<NTree<T>>();
    }

    public void AddChild(T data)
    {
        children.AddFirst(new NTree<T>(data));
    }

    public NTree<T> GetChild(int i)
    {
        foreach (NTree<T> n in children)
            if (--i == 0)
                return n;
        return null;
    }

    public void Traverse(NTree<T> node, TreeVisitor<T> visitor)
    {
        visitor(node.data);
        foreach (NTree<T> kid in node.children)
            Traverse(kid, visitor);
    }
}

Thanks @lordofduct for replying with such a thorough answer!

To give you a bit more context on the structure of my game…

After a short, linear prologue, you have a conversation with someone and depending on what you say, you branch into a different path, which has its own unique set of goals and a unique ending. (Also, in each path, scenes have different superficial changes. For instance, on path A, if you walk out onto the street, a character might be sitting on a bench, but in path B, if you walk out onto the street, that same character might be at a bus stop and a different character might be standing by a bench.)

Once you complete a path – each of them is relatively short – you loop back to the beginning of the game where you can choose a different path. After you complete every path, you are led to the true end of the game. Basically it’s a linear game with non-linear elements.

But, it’s very interesting what you have set up. I have a couple questions though:

  1. When you enter a new game state, how does i_TriggerSequence signal to the state object? Does it enable the state object, and if so, does “i_Trigger” fire off the first frame it’s enabled?

This is sort of similar to what I mentioned in my original post: making a persistent script that determines what state the game is in, which other objects or a game state manager within the scene can then refer to upon scene load in their own scripts.

  1. For the safe, how is PersistentStateMachine persistent? Do you just use a DontDestroyOnLoad call in the script? Or is it part of a pre-load scene?

I’m wondering if it makes sense to have a persistent script that contains a switch statement, with a case number for each branching path, and then a persistent script featuring a class for each path with a list of bools that determine state for that path. And then, I could have a StateManager object for each scene check the case number and which bools are set to true to determine how to load the scene. Does that make any sense?

(Also, FYI, I’m using Yarn Spinner for my dialogue system, which uses its own set of variables that can manage state and which dialogue should be said when, so I should be good on that front. At least I have been so far. Fingers crossed it continues that way.)

Thanks again!

Damn that yard spinner looks like a gem

Hahah yeah, @unit_dev123 , for the amount of writing there is in my game, Yarn Spinner’s been a pretty elegant solution so far. I haven’t heavily integrated it into my game yet, because I haven’t really built out a lot of the major conversations, but it’s been nice for the bit I’ve used it so far.

As far as that Tree Node structure goes, is the idea that you’re creating a list of game state nodes, and then “Traverse” would move from node to node? Is it as simple as that?

So i_TriggerSequence uses what we originally called a ‘Trigger’, but then changed the name to ‘SPEvent’ after UnityEvent came out:
https://github.com/lordofduct/spacepuppy-unity-framework-3.0/blob/master/SpacepuppyUnityFramework/Events/SPEvent.cs

This ‘SPEvent’ predates UnityEvent by a bit and is our own custom thing. But basically it works JUST LIKE UnityEvent with one difference…

5537038--569314--upload_2020-2-29_17-58-59.png

So UnityEvent can only call a function on a script.

Thing is when we designed ours my artist/designer/partner doesn’t like code at all. And rather was like “hey, could I have it where I have a thing that points at a GameObject, and on some event that thing tells that script to ‘activate’”. Basically instead of calling a method of your choice on that… it instead grabs ALL scripts off the target component that implement “ITriggerable”, loops over them, and calls “ActivateTrigger” on it.

These ITriggerable scripts always have the naming “I_WhatItDose”. Like:
5537038--569317--upload_2020-2-29_18-1-30.png
I_DisplayMessageText

5537038--569320--upload_2020-2-29_18-1-52.png
I_TriggerIfInInventory

5537038--569323--upload_2020-2-29_18-2-27.png
I_PlayAnimation

You may be following the naming convention here… “I Play Animation”… like I vs You. He’s an artist, he likes things like this.

I just create a script that does a general purpose thing and he can put them together to create situations.

So with I_Trigger, it’s basically like a daisy chain. Like so say some event occurs and it triggers GameObject A which plays an anim, a sound effect, etc… and it also has an i_Trigger that triggers on to another object. Note that i_Trigger has a ‘delay’ and other options on it to do timing and stuff.

Persistent does not mean persist between scenes in this case. It means the state of the object persists always. It’s a “savable object” basically. It persists in the “save file”.

And I explained in the previous post how that is saved. PersistentStateMachine saves the same exact way CutsceneSequenceManager saves. The only difference is how they behave on Start. CutsceneSequenceManager treats the states as sequence, and calls ‘skip’ on the previous ones. So if you start on index 4, 0-3 have skip called, and 4 is triggered.

Where as PersistentStateMachine only triggers the state that is the current state. If the index is 4, ONLY 4 is triggered, all others are ignored.

This is PersistentStateMachine. But instead of a switch statement… it’s an editor specific drag and drop interface.
5537038--569326--upload_2020-2-29_18-8-23.png

State0, State1, State2 is the switch statement… the one that’s triggered is the one that ‘Default State’ is set to. It does this on Start.

Now I’ll back up.

So in regards to your scenes. If the scenes go in sequence like this and branch. Here’s what I’d do.

FIRST - write out the general outline of your narrative with its branches. Note when doing so organize what scene your in for each one.

Now organize out what parts of the narrative are in each scene.

So like say you have a bus stop scene and there’s 3+ different sequences that may occur here.

  1. Sally is sitting on the bench and talks about the sunset, Fred is by the garbage can rummaging through it for his lost headphones, the Dog is sniffing the hotdog stand
  2. Sally is laying on the bench reading a book, Fred is by the hotdog stand buying a hot dog for he and the dog, the dog waits patiently by his side
  3. Sally is not there, Fred is asking the hot dog stand guy if he’s seen a girl (Sally), the dog is sniffing the garbage can

Now what I’d do is create a bus stop scene.

Then I’d create an empty GameObject in the base of that scene for EACH scenario. In them I’d place everything that makes that scene in that state (models for Sally/Fred/Dog/Hotdog stand, the dialogue they have, the interactables, etc etc etc).

Once done each of these root gameobjects will be converted into a Prefab and stored in asset folder labeled “BusSceneScenarios” or something like that.

Next I’d have a single gameobject in the scene with a single script on it. Something like:

public class ScenarioSpawner : MonoBehaviour
{

    [SerializeField]
    private string _sceneId; //unique name for the scene
    [SerializeField]
    private GameObject[] _scenarioPrefabs;
    [SerializeField]
    private int _defaultIndex; //this is the scenario index that spawns if the Start lookup fails
 
    void Start()
    {
        //pseudocode generic interface for a service provider that you can retrieve managers/services from.
        //you may prefer a singleton design, I just prefer a service provider design
        var gamestate = Services.Get<GameStateManager>();
        int index = gamestate.GetSceneScenarioIndex(_sceneId);
        if(index < 0 || index >= _scenarioPrefabs.Length) index = _defaultIndex;
        if(index < 0 || index >= _scenarioPrefabs.Length || _scenarioPrefabs[index] == null) return; //default index was out of range
     
        Instantiate(_scenarioPrefabs[index]); //TODO - do you want to position/rotate the prefab??? probably...
    }

}

This GameStateManager is what would store all the scene’s scenario state that gets saved.

Something like:

public class GameStateManager : ISaveableObject
{

    private Dictionary<string, int> _scenarioStateIndices = new Dictionary<string, int>();
 
    public int GetSceneScenarioIndex(string sceneId)
    {
        int result;
        if(!_scenarioStateIndices.TryGetValue(sceneId, out result)) result = -1;
        return result;
    }
 
    public void Save(SaveTokenContainer data)
    {
        data.Add("*GameStateManager*", new SaveToken() {
            Data = _scenarioStateIndices.ToArray()
        });
    }
 
    public void Load(SaveTokenContainer data)
    {
        var token = data.Get<SaveToken>("*GameStateManager*");
        _scenarioStateIndices.Clear();
        if(token.Data != null)
        {
            foreach(var pair in token.Data)
            {
                _scenarioStateIndices[pair.Key] = pair.Value;
            }
        }
    }
 
 
 
 
    [System.Serializable]
    public struct SaveToken
    {
        public KeyValuePair<string, int> Data;
    }

}

Note SaveTokenContainer is just a psuedo-code generic container that anything can get stuck into by string->object pair. Basically a glorified dictionary that you have some serialization engine that will serialize it for you.

Which one that is is up to you. How that all works is all up to you.

Note my pseudo-code approach here assumes that the SaveTokenContainer accepts ANY serializable type and that the engine will resolve it for you (so not like how unity’s JsonUtility works which explicitly needs to know the types… rather instead something more like the System.Serialization approach like BinaryFormatter or Newtonsoft with its type describers turned on).

Pretty much although am fairly new to this. My idea was you would have an N-ary tree that captures the story state. It is a linear dialogue game, with discrete inputs. Much akin to a ‘choose your own adventure’ story.

So the juggling of the logic would only really depend on which node you are in the tree. If you know where you are in the tree, you can do a pre-order traversal to figure out which states / decision matrices have been triggered.

That’s the concept, haven’t yet tried it in practice though.

My other idea was to somehow store the data in a heavily nested json format, my concern with this is performance and traversal, but it might not be an issue if you only keep going forward.

Thanks for all your help! Yeah, I think setting up each scenario as its own object is an interesting idea. I think I’m going to set up a test project and some scenes and test it out. Fingers crossed I make progress!