Updating Shared Blackboard variables in com.unity.behavior

I was wondering how can I set Blackboard variables in runtime, I was able to reference the BlackboardRuntimeAsset for my game manager but when I update the variables they don’t update until I recompile my code, and if I switched scenes they return to null. And any unused variable even after recompiling if I tried using it it will be null.

For context on how I tried doing it:

  • I created a Blackboard
  • Referenced it in GameManager
  • on Awake I just went though all the variables, located and casted the variables I need (BlackboardVariable<Player>)
  • set the Value on it

if anyone knows how to solve this issue or an alternative solution would be appreciated

Hi Lasovar,

Sorry for the late reply, we’ve been very busy and to be fair, I don’t believe we thought that could be a use case for the Blackboard Asset. I think it’s a valid and nice way of using it actually, so I wanted to make sure to repro and try a few things before going back to you.

The issue

First, we are aware of an issue related to Shared Blackboard Variable lifecycle.

Then, the unexpected behavior probably comes from the fact that your Blackboard Variable (BBV) is not shared. Right now, the variable you are referencing in the code is actually the original BBV that is expected to be duplicated and linked to a specific agent in the scene.

Workaround

Your approach should work by simply making the BBV shared:
image

More details

Blackboard Asset were generally designed to:

  1. Provide a way to user to reused BBVs acros multiple blackboard
  2. Allowing the creation of RunSubgraph (Dynamic) by using BlackboardAsset as reference to decided what BBVs need to be injected to a subgraph (a bit like an interface).
  3. When a graph is referencing a Blackboard asset, it is actually going to duplicate it to have it’s own local version.

I hope this helps understand why accessing BBV from Blackboard Asset wont change the value from graph referencing it.

Later we introduced the concept of Shared Blackboard Variable. The concept being:

  1. BBV can now be marked “Shared” (Right-click on the Variable - a new UI/UX is coming soon)
  2. At runtime, when an Agent is duplicating a blackboard, we convert a BBV marked “Shared” to a Shared Blackboard Variable (SBBV) that will basically points toward the original instance of BBV in the original Blackboard Asset.

To make sure this was working as expected, here is what I’ve tried locally:

  1. Created a new Blackboard Asset along with a new Graph
  2. Added BBV<string> Text (default value = "Unity") to the Blackboard Asset
  3. Referenced the Blackboard Asset to the graph and added a LogVariable (using the Text BBV) to the Start node.
  4. Created a new Script with the following code
public class GameManagerTest : MonoBehaviour
{
    [SerializeField] private RuntimeBlackboardAsset m_BlackboardAsset;
    [SerializeField] private string m_VariableName = "Text";
    [SerializeField] private string m_TargetValue = "Hello World";

    void Awake()
    {
        foreach (var bbv in m_BlackboardAsset.Blackboard.Variables)
        {
            if (bbv.Name == m_VariableName)
            {
                if (bbv is BlackboardVariable<string> stringBBV)
                {
                    stringBBV.Value = m_TargetValue;
                }
            }
        }
    }
}
  1. Added the Graph to a BehaviorAgent along with the GameManagerTest in a scene (+assigning the fields).

Expected output

  1. First run logs Unity - this is the current outcome you are experiencing.
  2. Open the Blackboard Asset and make BBV Text shared (right-click > Shared)
  3. Second run logs Hello World

To conclude

Using BlackboardAsset directly to set global value can be useful and is an extra-use case we haven’t though of. I’ll share the detail with the team and we will try to improve the UX in that area to mitigate the confusion you experienced.

Thank you for you patience and sorry for the inconvenience :bowing_man:

1 Like

I was about to say I already tried what you suggested, and added a lot of code to try to fix it.

But I said just in case let me revert everything and try again maybe I missed something in the first attempt. and works well!

really appreciate the help <3

[SerializeField] private RuntimeBlackboardAsset m_Blackboard;

private void Awake() {
    var player = Instance<PlayerStateMachine>.GetChecked();
    if (!player || !m_Blackboard)
        return;
                        
    foreach (BlackboardVariable blackboardVariable in m_Blackboard.Blackboard.Variables) {
        if (blackboardVariable is not BlackboardVariable<PlayerStateMachine> playerVariable)
            continue;
                
        playerVariable.Value = player;
        break;
    }
}

I tried to hook into the OnValueChanged event for this variable so every time it changes it always set’s itself back to the player instance, it always caused a stack overflow (yes maybe because I am setting on the call back of the call back but I made sure to not update the value when it’s valid) still not sure what the issue was, but just setting it on awake works with no issues now.

Interesting, it is possible that the stack overflow was caused by an issue we are fixing for the next release.
It is possible that when you reverted your change, it also reverted a SharedBlackboardVariable in a RuntimeBlackboardAsset.

Extracted from the upcoming new “Known Issue” documentation page:

Stack-overflow issue with SharedBlackboardVariable

A stack overflow occurs at runtime (or playmode) when the graph of a BehaviorGraphAgent attempts to access or set SharedBlackboardVariable.

Causes

Incorrect serialization of the SharedBlackboardVariable in a RuntimeBlackboardAsset.

Resolution:

Upgrade to Unity Behavior version 1.0.3 or later.

Additionally, check your original Blackboards to ensure no SharedBlackboardVariable is serialized in the RuntimeBlackboardAsset, as this can lead to issues later.

Yeah, it seams not only when setting the SharedBlackboardVariable it’s also when getting it.
when I try to log the variable now it throws a stack overflow error and restarts the tree…

I am really loving the workflow switched most of my old behavior trees to this… Hope it gets fixed soon🙏
though for now I guess I will just work around it till its fixed, I would also appreciate if you update me when it gets fixed :slight_smile:

thanks for your help

1 Like

hey @MorganHoarauUnity - I tried your code with a List variable type and there were 2 issues:

  1. It shows “type mismatch” even thought I am definitely setting adding a GameObject. I also tried this without a List and just a single GameObject variable and it gave the same error.

  2. It saves the new value in the Blackboard scriptable object asset in the project, which become “missing” references because they no longer exist after exiting playmode.

Here is my code:

foreach (var bbv in GameBlackboardAsset.Blackboard.Variables) {
    if (bbv.Name == "Players") {
        if (bbv is BlackboardVariable<List<GameObject>> variable) {
            variable.Value.Add(networkPlayerObject.gameObject);
        }
    }
}

Thanks.

Hey @Lasovar,

Sorry I forgot to get you updated, 1.0.3 has been released and should provide the fix for the issue.

Please make sure to visit the Troubleshooting page for more information about potential issue you might encounter.

1 Like

Hi @fendercodes

Sorry for the confusion, I will assume that this BlackboardVariable is shared, but what you described here is currently the expected behavior. Does that causes you other underlying issue?

I suspect that the type mismatch is caused by the generic serialization, but you should be able to click the “Type mismatch” field to ping the target object in the scene. I’ll make sure we have an internal ticket to try to improve that issue.

This also expected behavior, this is because the RuntimeBlackboardAsset is caching its original value OnPlaymodeEnter and reset it’s runtime value to this cache value OnPlaymodeExit. This is done in order to conserve any default value assigned to a BlackboardVariable. I hope this makes sense.

This is something we might document in case we have several user being confused about this mechanism, so if anyone else experience this confusion, please let us know so we can improve our documentation.

Also, please feel free to report a bug if you find other issues (bug or UI/UX), this way you will be notified when the issue is solved.

Sorry for the inconvenience and thank you for your patience :slightly_smiling_face:

Thanks for your hard work on Behaviour! It’s great, keep it up.

This also expected behavior, this is because the RuntimeBlackboardAsset is caching its original value OnPlaymodeEnter and reset it’s runtime value to this cache value OnPlaymodeExit. This is done in order to conserve any default value assigned to a BlackboardVariable. I hope this makes sense.

So in my Blackboard asset the default value for the list is that there are 0 child values:

After I enter playmode, add a single element to the list at runtime, then exit playmode, it doesn’t clear the list back to the state before playmode. It just has a missing reference.

For projects using version control this can cause a bit of a headache because it marks the file as having changes after each time we go into playmode.

Oh that is actually after entering playmode :hushed:

That does looks like a bug then. Can you report a bug please? We still have a few issues around shared blackboard and this might be something new.

Just to be sure, are you using 1.0.3?

Yeah I am on 1.0.3. Here is the script I am using to edit the value, just in case I am doing that wrong.

        var bbv = GameBlackboardAsset.Blackboard.Variables.Find(v => v.Name.Equals("Players"));
        if (bbv is BlackboardVariable<List<GameObject>> variable) {
            List<GameObject> gameObjects = new List<GameObject>();
            variable.Value = default;
            foreach (var networkObject in m_SpawnedCharacters.Values) {
                gameObjects.Add(networkObject.gameObject);
            }
            variable.Value = gameObjects;
        }

I just logged the bug: IN-87437

1 Like

Thanks for reporting the bug, will look into it!

Just to simplify, you could use the generic BlackboardReference.GetVariableValue that will do both the name and type check for you. (or GetVariable if you are interested into binding the OnValueChanged event)

1 Like

@MorganHoarauUnity

Thanks for the tip, that will definitely clean up the code. However, when I reference from the asset in the inspector, it shows a variables dropdown too. What is the purpose of that? It’s not even showing the variables that are actually in the Source (“Game” asset in the screenshot). I just want to reference an existing blackboard asset and change its values at runtime is all.

Note: This is all running on my global GameController and not on a single agent so there is no access to the blackboard reference via the behaviour agent.

Hi fendercodes,
I can see you are trying to use the Blackboard asset as a standalone solution.

From the provided screenshot it seems like you are referencing a BlackboardReference in your class. This class is used by the behavior agents to duplicate the source blackboard (using Blackboard.GenerateInstanceData which is internal) allowing each agent to work with their own instance of blackboard. What you are seeing in the inspector is the Blackboard object (that would store the duplicated data) and the “Source” is the RuntimeBlackboardAsset that would be use as a reference.

Unfortunately the way you are trying to use the asset is just not allowed at the moment because it is only meant to be use by BehaviorGraphAgent.

Very early on we saw the potential for such a use case and nobody in the team liked that we made the whole blackboard API internal. But as we could not finish and validate the API for such use case in the release timeline, we had to prevent user from using a potentially temporary (and half-baked) API, or we would had to stick with it until 2.0.

Currently a workaround could be to add a Behavior(Graph)Agent on your GameController with an stub graph that has the blackboard you want as reference and work with the BlackboardReference from it. Additionally, using the Shared functionality on BlackboardVariables, you could then make all your agents through use the same data.

public class DataManager : MonoBehaviour
{
    [SerializeField]
    private BehaviorGraphAgent m_Agent;

    void Start()
    {
        m_Agent.BlackboardReference.SetVariableValue("DataManager", this);
        m_Agent.BlackboardReference.GetVariable("Player", out var player);
        // ...
    }
}

I hope this help.
Sorry for the inconvenience :bowing_man:

Thanks for digging into this deeper. The way I have it at the moment “works” for the most part. I reference the RuntimeBlackboardAsset like this:

public RuntimeBlackboardAsset GameBlackboardAsset;

Then can update its values at runtime like this:

    void UpdateBlackboardPlayers() {
        var bbv = GameBlackboardAsset.Blackboard.Variables.Find(v => v.Name.Equals("Players"));
        if (bbv is BlackboardVariable<List<GameObject>> variable) {
            variable.Value.Clear();
            foreach (var networkObject in m_SpawnedCharacters.Values) {
                variable.Value.Add(networkObject.gameObject);
            }
        }
    }

I’m doing it this way because I want to be able to set a list of game objects at runtime that can then be looped over inside of the graph (its a multiplayer game where the AI checks for each player in turn).

Everything works, except that initial bug I logged above where changes made to the RuntimeBlackboardAsset for lists are persisting after exiting playmode. Does your proposed workaround avoid that issue?

For reference, this is the EachModifier I wrote, which runs the child branch for each element in a List of game objects. Perhaps useful for other people. (It would be cooler if it could handle any element type and not just GameObject)

using System;
using System.Collections.Generic;
using Unity.Behavior;
using UnityEngine;
using Modifier = Unity.Behavior.Modifier;
using Unity.Properties;

[Serializable, GeneratePropertyBag]
[NodeDescription(
    name: "Each",
    story: "Sets [Element] to each item in [List]",
    description: "Runs branch once for each item in the GameObject List",
    category: "Flow",
    id: "04805d44fda619a98df1d245ff057636"
)]
public partial class EachModifier : Modifier {
    [SerializeReference] public BlackboardVariable<List<GameObject>> List;
    [SerializeReference] public BlackboardVariable<GameObject> Element;

    internal int m_CurrentIndex;

    protected override Status OnStart() {
        m_CurrentIndex = 0;

        if (Child == null || List == null) {
            return Status.Failure;
        }

        if (List.Value.Count == 0) {
            return Status.Success;
        }

        Element.Value = List.Value[m_CurrentIndex];

        var childStatus = StartNode(Child);
        if (childStatus == Status.Failure || childStatus == Status.Success) {
            // Exit the loop if there was just a single element in the list
            if (List.Value.Count == 1) {
                return childStatus;
            }

            return Status.Running;
        }

        return Status.Waiting;
    }

    protected override Status OnUpdate() {
        // Check child status and increase index when its finished
        Status childStatus = Child.CurrentStatus;
        if (childStatus == Status.Failure || childStatus == Status.Success) {
            ++m_CurrentIndex;

            // Exit the loop when we reach the end of the list
            if (m_CurrentIndex >= List.Value.Count) {
                return childStatus;
            }

            Element.Value = List.Value[m_CurrentIndex];

            var newChildStatus = StartNode(Child);
            if (newChildStatus == Status.Failure || newChildStatus == Status.Success) {
                return Status.Running;
            }
        }

        return Status.Waiting;
    }
}