How to use a Data Blackboard?

WARNING - DO NOT USE THE CODE BELOW, READ TO THE END OF THE THREAD FIRST

Last few days I’ve written a behaviour tree system with the new viewGraph tools and almost everything is set up except for the variables.
Seems like blackboards are the way to go, but I’ve no idea how to use them.

Just in case you don’t know: Behaviour Trees only exist once in memory, so all the data of a tree is stored in the agent who is executing it, this data is called “Context” - so when the tree gets executed it looks like this tree.Execute(myContext);

Context, Blackboards? Where is the connection? Local data is not the only data needed, for example: I need to know where the player is located, this is global data, then if the enemy fortress/base is alerted, I call this “group data”, because it only affects a group of enemies (so if one enemy fortress is alerted, enemies on the other side of the game world are not alerted, only the ones of this specific base).

Currently my blackboard looks like this:

public class DataEntitiy
{
    object storedValue;

    public T Get<T>() {
        T result = (T)storedValue;
        return result;
    }
    public object Get() {
        return storedValue;
    }
    public void Set(object value) {
        this.storedValue = value;
    }
}

public class DataBlackboard
{
    protected Dictionary<string, DataEntitiy> data = new Dictionary<string, DataEntitiy>();

   public DataEntitiy this[string key] {
        get {
            DataEntitiy result;
            if (data.TryGetValue(key, out result)) {
                return result;
            }
            result = new DataEntitiy();
            data[key] = result;
            return result;
        }
    }

// Other GET and SET methods
}

Using it like this: myBlackboard["PlayerPosition"].Get<Vector3>();

Context has 4 of these Blackboards

  • BehaviourTree-Node specific data (e.g. a timer, only relevant for the node executing)
  • local data (data about the executing agent e.g. agentHealth)
  • group data (data of one enemy fortress e.g. lastTimePlayerWasSeen, isAlerted, NrOfAliveAgents)
  • global data (e.g. playerPosition, currentDaytime)

Now it just feels wrong to set the data in an update loop, especially if I use Update() to set the Data like PlayerPosition and also Update() to execute the behaviourTree and therefor read the same data - agents could miss shots because they have the wrong information, which was set in the previous frame.
Overall I am not happy setting the data like this, when it may not be needed at all … is this even the correct approach or am I doing something wrong here?

Note: Some VariableNames are created in the Editor, so I can’t just write them all down in code and access them directly like context.agentRigidbody.position;, also it would break the decoupling and I may want to reuse the behaviour trees in another project.

I am sure the experts will have something to say on this. I myself am quite new to C#, Unity and game development. But I have been programming in other languages for ages.

I wonder if perhaps you could use delegates to compute the information on demand if and when it is needed. The idea would be to store a computation in the dictionary that knows how to get the required information from the world. The computation would only need to be stored once and could be executed in the framea where the corresponding information is needed. Perhaps like this:

public class InfoStore
{
    public delegate object DataProvider();

    private class Data
    {
        internal DataProvider Provider;
        internal object Value;
        internal int LastUpdated;
    }

    private Dictionary<string, Data> dict = new Dictionary<string, Data>();

    public void Add(string key, DataProvider provider)
    {
        Data data = new Data()
        {
            Provider = provider,
            Value = null,
            LastUpdated = Time.frameCount - 1

        };
        dict.Add(key, data);
    }

    public object Get(string key)
    {
        if (!dict.TryGetValue(key, out Data data) {
            throw new InvalidOperationException("Cannot get data for non-existing key " + key);
        }
        // Is the data up to date?
        if (data.LastUpdated != Time.frameCount)
        {
            data.Value = data.Provider.Invoke();
            data.LastUpdated = Time.frameCount;
        }
        return data.Value;
    }
}

(Add generics as needed.) You could then populate the InfoStore instance with something like

info.Add("PlayerPosition", () => thePlayer.transform.position);

Depending on your needs there could also be variants that pass additional parameters to Get that would be passed on to the delegate call.

Don’t make it that generic. Don’t make a blackboard for “any game”. Make one for your game.

Look, you will know what data your blackboard needs to contain. Don’t have a Dictionary<string, object> that you cast to Vector3 just to store the player’s position. Store the player’s position in a variable named playerPosition.

That’s a lot more readable, and actually quite a bit faster (you’re boxing the position a lot in the current implementation).

Also don’t be too worried about what the “correct” implementation of a BT is. Have your own implementation, and let it just have the data it needs, rather than wrapping it in a super-generic context object. That’ll just make your code very cumbersome to use.

3 Likes

Yes this works :slight_smile: I actually ended up using Func in my DataEntity class, whenever the Data is requested and if “useFunc” is TRUE, so it can be a storage OR a method pointer, which will return the desired result and it works fine ^^

Problem here is that the user (thats me in this case xD) can create variables in the Editor, outside of Visual Studio, so there has to be this string/object based solution for now. I don’t want to hop into visual studio every time I need need a new variable. Because one node can write to a variable, that another node can read. GoToPosition is probably the best example. If I search for various positions, e.g. patrol position, healing spot, ammo position, and then decide where to go in the tree, these are all variables, which do not exist in code - thats why I need a solution that works for both cases.

But unless I run into perfomance issues, I’d consider this case closed. ^^ Thanks for the help :slight_smile:

Well, here we are, 9 months later, on the day where our second gameplay-test-phase should start.

I had the idea to use a string/object dictionary from someone who wrote this in C++, which probably works fine but accessing variables, hundreds of calls per frame, via string in C# causes massive Garbage. Every time a string is passed into the Get() method or into the dictionary[ ] indexer, the string is actually copied - and a string is basically a List, so it is saved as reference type on the heap memory, so the garbage collection has to clean all that clutter.

The 30 enemies in our game create so much garbage per frame, almost 10 milliseconds, and it’s only 30 enemies with rather simple behaviour - the amount and the complexity of their behaviour will increase, so I basically have to rewrite the whole system.

To anyone stumbling over this - do not use string/object dictionaries for things you call a lot of times every frame

So, back to refactoring, just thought I’d warn whoever comes by. ^^

1 Like

I don’t think a Dictionary<string, whatever> should allocate on the getter. It’s just calling GetHashCode on your key. Passing a string instance around should also not copy it.

strings are immutable, but that only means that methods that modify them return a copy. Any use that does not change it should be safe.

Here’s a test script:

using System.Collections.Generic;
using UnityEngine;

public class TestScript : MonoBehaviour {
    public string s;
    public int i;
    public int i2;
    private Dictionary<string, int> dict;

    void Start() {
        dict = new Dictionary<string, int> { {s, 5} };
    }

    void Update() {
        dict[s] = Mathf.RoundToInt(Time.time);
        dict["foo"] = Mathf.RoundToInt(Time.fixedTime);

        i = dict[s];
        i2 = dict["foo"];
    }
}

The profiler results are:

So your allocations are probably not coming from the accessor.

2 Likes

Ok, thats interresting - but I don’t have any new() or delegate calls in the System, strings are the only thing that is passed around - maybe it has to do with the scriptableObject.name calls?

Maybe I don’t have to rewrite the whole System? We’ll see, further investigation is needed.

ScriptableObject.name allocates.

This is because the name lives in the c++ engine, and has to be copied over to the C# runtime every time you get it.

In general, do not get the names of things. If you’re using the ScriptableObjects as keys in a Dictionary, use them directly instead of using their names, as their HashCode is just their instanceID, which is stored in a field, so that’s both faster than using the strings to begin with, and does not allocate.

Yes, thanks a lot for the info, I will try this - more or less, maybe I cache the name-strings in a seperate dictionary and access them by the ScriptableObject-Reference because right now I have both cases, some where the blackboard variable is referenced via a ScriptableObejct in the BehaviourTree Inspector and some where they are references directly inside the code (for local variables of one node or lazy writing, when I just want the variable somewhere without having to assign the scriptableObject in the inspector, their names are unlikely to change).

I tracked it down - the result is … well, unexpected, after about 8 years of game dev, how could I never know about this?
Ok, so it took me about 4h to deconstruct the method, bit by bit, out-commenting everything and it turns out that assigning a struct to an object field, creates garbage, massive garbage to be more precise xD

Here is the script I used for testing:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GarbageTest : MonoBehaviour
{
    Vector2 _var1;
    object _var2;

    [SerializeField] bool _startTest;

    void Update()
    {
        if (_startTest)
        {
            _startTest = false;
            DoTest();
        }
    }

    void DoTest()
    {
        TestMethod1();
        TestMethod2();
    }

    void TestMethod1()
    {
        for (int i = 0; i < 100000; i++)
        {
            _var1 = Vector2.zero;
        }
    }
    void TestMethod2()
    {
        for (int i = 0; i < 100000; i++)
        {
            _var2 = Vector2.zero;
        }
    }
}

And here is the screenshot of the profiler, set to deep profile

you can test it yourself, if you don’t beliefe this, I think this is quite crazy …
As you can see, the GC Alloc Field says:
Method1 = 0 B
Method2 = 2.3 MB

we just created 2.3 Megabyte of garbage, with only 100.000 calls.

Setting the iteration count to 1, returns 74 B Garbage for TestMethod2(), quite a bit for a Vector2 which should only have 8 Bytes and create no Garbage at all …

This basically breaks the whole system, I’ve absolutely no idea how to avoid this.
30 Agents call this method, getting and setting variables about 2.500 times a frame, creating ~20 Kilobyte Garbage per frame.
Thats pretty bad.

Does anyone have any ideas how to deal with this?

This is caused by Boxing (Boxing and Unboxing - C# | Microsoft Learn). Essentially, C# is creating a new wrapper object to allow your struct to be assigned to a variable of type object. This wrapper has to be garbage collected like any other object.

Potential solutions are:

  • Create your own reusable wrappers
  • Implement specialised Dictionaries just for the value types you use the most.
2 Likes

Finally solved it by making DataEntitiy generic → DataEntitiy<T> and deriving from a class with the same name, just without the generic argument.
No more garbage on a per Frame basis - well that was quite a ride. (hadn’t time to fix it until now, since all the other changes I made, already stabilized the framerate - there were quite a few performance leaks in the code)

Thanks @bobisgod234 for the keyword “boxing” ^^

Can you share your solution @John_Leorid ?

Sure:

public class DataBlackboard
{
    protected Dictionary<string, DataEntitiy> data =
        new Dictionary<string, DataEntitiy>();


    public DataEntitiy<T> Get<T>(string key)
    {
        DataEntitiy<T> result;
        if (data.TryGetValue(key, out DataEntitiy resultRaw))
        {
            return resultRaw as DataEntitiy<T>;
        }
        result = GenericPool.Get<DataEntitiy<T>>();
        data[key] = result;
        return result;
    }
}
public abstract class DataEntitiy
{
    public abstract void ReturnToPool();
    public abstract DataEntitiy GetCopy();
}
public class DataEntitiy<T> : DataEntitiy
{
    T _storedValue;

    public T Value
    {
        get => _storedValue;
        set
        {
            _storedValue = value;
            OnSetEvent?.Invoke(this);
        }
    }

    public delegate void onSet(DataEntitiy<T> entity);
    /// <summary>
    /// returns the entity when set, so Get<T>() can
    /// be used to get the prefered type
    /// </summary>
    public event onSet OnSetEvent;

    public DataEntitiy() { }
    public DataEntitiy(T value)
    {
        _storedValue = value;
    }

    public Type GetTypeOfValue()
    {
        return _storedValue.GetType();
    }

    void ResetValue()
    {
        _storedValue = default;
    }
    public override void ReturnToPool()
    {
        ResetValue();
        OnSetEvent = null;
        GenericPool.Return(this);
    }

    public override DataEntitiy GetCopy()
    {
        DataEntitiy<T> copy = GenericPool.Get<DataEntitiy<T>>();
        copy._storedValue = _storedValue;
        copy.OnSetEvent = OnSetEvent;
        return copy;
    }
}
public static class GenericPool
{
    private static Dictionary<Type, object> _pool = new Dictionary<Type, object>();
    public static T Get<T>()
    {
        if (_pool.TryGetValue(typeof(T), out object value))
        {
            Stack<T> pooledObjects = value as Stack<T>;
            if (pooledObjects.Count > 0)
            {
                return pooledObjects.Pop();
            }
        }
        return Activator.CreateInstance<T>();
    }
    public static void Return<T>(T obj)
    {
        if (obj == null)
        {
            return;
        }
        if (_pool.TryGetValue(typeof(T), out object value))
        {
            Stack<T> pooledObjects = value as Stack<T>;
            pooledObjects.Push(obj);
        }
        else
        {
            Stack<T> pooledObjects = new Stack<T>();
            pooledObjects.Push(obj);
            _pool.Add(typeof(T), pooledObjects);
        }
    }
    /// <summary>
    /// only required when fast play mode options are active
    /// </summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void DomainReset()
    {
        if (_pool != null) _pool.Clear();
    }
}
2 Likes

This is really helpful! I think I got a better understanding of it but there’s still some parts that I don’t get.
Could you explain why you’re using a “GenericPool” and what that does in the context of the rest of the code?

Thanks!

To avoid garbage. Further up this thread I talk about boxing - a valid way to avoid it is by using a custom wrapper. And I have to pool the wrapper to avoid generating garbage.

The real DataBlackboard class has a remove method and there I return the classes to the pool. Given the code here, it wouldn’t matter if I would create the DataEntities with new(), as all of them are saved in the dictionary anyway.

1 Like