State Machines -- Blackboards

One of the challenges in working with state machines is that often we will have to pass along critical information between the states.
Often this is done in the form of custom constructors. For example, in the Third Person course, when we want to execute an Attack combo, we start a new Attack state and pass the index of the next attack into the custom constructor

public PlayerAttackState(PlayerStateMachine machine, int index) :base(machine)
{
    attackIndex = index;
}

While this is convenient for a small amount of data, sometimes there is a need for more persistent data. In AI, there is a concept called a Blackboard, which is usually associated with Behavior Trees. Behavior Trees are actually a specialized form of State Machine.

I realized the need for a Blackboard when I started reworking the states to be Scriptable Objects instead of simple classes. I made this choice because I want to be able to customize AI behavior by assigning custom states to various categories of states. For instance, a soldier might have a non-combat state of patrolling along pre-defined coordinates, a spider might select random locations along the NavMesh, and listen for vibrations as players move through certain areas, a wolf might hunt innocent rabbits when not attacking the player, and an innocent rabbit may simply lay in wait in the cave ready to destroy unsuspecting knights with their huge teeth and visiousness I warned you, but did you listen to me? Oh, no, you knew it all, didn’t you? Oh, it’s just a harmless little bunny, isn’t it? Well, it’s always the same. I always tell them.

The problem with ScriptableObjects is that they cannot retain any state… well, they can, but since ScriptableObjects are a shared resource, setting the current attack means that all charactes using that attack will have the same attack.

Having had experience with Behavior Trees, the Blackboard immediately came to mind. For our purposes, the Blackboard remains a component on the State Machine. It doesn’t have to be a MonoBehaviour, but in this case, I made it one. It would probably be more efficent for the StateMachine to simply create a Blackboard in the Awake() method…

The Blackboard starts with a Dictionary<string, object>

using System.Collections.Generic;
using UnityEngine;

namespace RPG2.States
{
    public class Blackboard : MonoBehaviour
    {
        private Dictionary<string, object> blackboard = new Dictionary<string, object>();

The Dictionary is our actual Blackboard, the rest of the class is just the wrapper for this Dictionary. It’s initialized at runtime, so there is no need for an Awake() method to initialize it. It’s ready to go at the start of the game.
The key is a string, so if I want to store anything in the Blackboard, I need a Key (string), and to pass the actual information I want to store. Since I’m using an object, this could be quite literally anything. It could be an int, a bool, another string, a Vector3, an object reference, or an entire MP4 of the greatest movie ever made, Monty Python and the Holy Grail.

Now this Dictionary is not public, so we’re going to need to make some accessors…

First, created a special accessor so that you can literally assign a value to the Blackboard by treating the blackboard itself as if it were a Dictionary…
in other words, I want to be able to say

stateMachine.Blackboard["PatrolPoint"] = 1;

Here’s how we pull that bit of magic off:

        public object this[string key]
        {
            get => !blackboard.ContainsKey(key) ? null : blackboard[key];
            set => blackboard[key] = value;
        }

This is a special property that allows you to create a bracket accessor. This is actually exactly how Array, List, and Dictionary expose the brackets to you.
With the Get, if the key does not exist, instead of balking and saying no such key, it simply returns null… That’s ok, because that is not the safest way to get information from the Blackboard, but the Setter, THAT is important, because this is exactly how we want to set it. The setter simply drops whatever is on the right hand side of the assignment into the Dictionary.

The next method is a bit of magic… We have no way of knowing exactly what the object is (well, there are ways, but they are cumbersome)… But fortunately, we can actually test to see if an object is a certain type, and we have generics, so it’s possible to have a class T and test to see if the object is a T… so the way to GET a value from the Blackboard is this TryGet method (as well as the public ContainsKey() method just to round things out

       public bool ContainsKey(string key) => blackboard.ContainsKey(key);

        public T TryGetValue<T>(string key, T fallback)
        {
            if (!ContainsKey(key)) return fallback;
            if (blackboard[key] is T t)
            {
                return t;
            }
            return fallback;
        }

This generic method returns a T, and if the key does not exist or there is another reason that the contents are not T, returns a fallback.
First, it’s tested for the key… if no key, return the fallback value.
Then it’s tested for equivalency. If the value is the type (say I assigned an int, and I’m looking for an int), then that value is returned. Otherwise, the fallback is returned (if, for example, I pushed an int, but then tried to read it back as a string)

That’s the entire class…
So in my Patrol class, I store PatrolIndex onto the Blackboard…

stateMachine.Blackboard[PatrolKey] = currentIndex; //Edited

Then when I want to retrieve the index,

int index = stateMachine.Blackboard.TryGetValue(PatrolKey, -1); //Edited

Now if the state gets a -1, then it knows there is no patrol index, and that it needs to actually do about the business of locating the nearest patrol point. But if the index is NOT -1, then it knows it needs to tell the NavMeshAgent that the character should be heading for the patrol point of index.

Here’s the completed class.

Blackboard.cs
using System.Collections.Generic;
using UnityEngine;

namespace RPG2.States
{
    public class Blackboard : MonoBehaviour
    {
        private Dictionary<string, object> blackboard = new Dictionary<string, object>();

        public object this[string key]
        {
            get => !blackboard.ContainsKey(key) ? null : blackboard[key];
            set => blackboard[key] = value;
        }

        public bool ContainsKey(string key) => blackboard.ContainsKey(key);

        public T TryGetValue<T>(string key, T fallback)
        {
            if (!ContainsKey(key)) return fallback;
            if (blackboard[key] is T t)
            {
                return t;
            }
            return fallback;
        }
    }
}

That’s it. Now with a reference to the Blackboard in the StateMachine, your states can store any information they need to.

Probably not too exciting for everybody, but thought I’d share this anyways, as it’s something I’m finding useful, and it’s a remarkably compact piece of code that carries quite a bit of functionality.

2 Likes

You named your blackboard BehaviourTree in the state machine which feels a little odd, but you know what you’re doing so I’ll leave it there.

I like the blackboard as a MonoBehaviour simply because it then allows us to have a game object in the scene that can keep global ‘world-related’ information that can be accessed by all other state machines.

No I typed this just before going to bed, and while the Blackboard class was straight from my project, the example useage I typed at 12:00 in the morning… indicating that sometimes I don’t know what I’m doing. :stuck_out_tongue:

It’s still possible to have a global Blackboard as well as StateMachine level blackboards without being a MonoBehavior (one static, one instance Dictionary, and two methods) but probably not ideal. I may play with this later.

Ok, I’ve played around with this a bit, and made a few modifications. This new version of Blackboard.cs uses an inner class for the functionality, allowing us to have a Static Blackboard as well (it’s not an instance per se, it contains a static instance of the inner Board class.

using System.Collections.Generic;
using UnityEngine;

namespace RPG2.States
{
    public class Blackboard : MonoBehaviour
    {
        private Board blackboard = new Board();
        public static Board Shared { get; } = new Board();

        public object this[string key]
        {
            get => blackboard[key];
            set => blackboard[key] = value;
        }

        public bool ContainsKey(string key) => blackboard.ContainsKey(key);

        public T TryGetValue<T>(string key, T fallback)
        {
            return blackboard.TryGetValue(key, fallback);
        }

        public class Board
        {
            private readonly Dictionary<string, object> blackboard = new Dictionary<string, object>();

            public object this[string key]
            {
                get => !blackboard.ContainsKey(key) ? null : blackboard[key];
                set => blackboard[key] = value;
            }

            public bool ContainsKey(string key) => blackboard.ContainsKey(key);

            public T TryGetValue<T>(string key, T fallback)
            {
                if (!ContainsKey(key)) return fallback;
                if (blackboard[key] is T t)
                {
                    return t;
                }
                return fallback;
            }
        }
        
    }
}

Now this still retains the ability to access the blackboard attached to the statemachine with

stateMachine.Blackboard["myindex"]

but it also allows us to access a shared blackboard with

Blackboard.Shared["myIndex"]

Privacy & Terms