Change Trees dynamically at runtime to mimic Smart Objects pattern

leanon00

Member
Hello! I’d like to make AI modular so weapons can bring their own behavior graphs.The main graph holds the common AI logic for all agent archetypes.
A blackboard variable (Subtree Slot) points to the currently equipped weapon’s subtree.
If it’s null, it falls back to a default unarmed attacks subtree, which should also be settable from the blackboard.

When an NPC equips a weapon, that weapon assigns its subtree to the slot; when dropped, the slot clears.

Basically, I want to load a subtree with the weapon logic in another subtree that keeps the shared combat logic.
The combat logic is keept in the main subtree that keeps the subtrees for all the behaviours (combat, wander, idle etc)

I experimented a bit with ISubtreeReference , but from what I understand it’s designed for a list of subtrees that get loaded once with the main graph.

What I’m looking for is something more dynamic.


```
public class SubTreeLoader : ActionNode, ISubtreeReference
{
public SharedVariable<Subtree> subtreeReference;

[Tooltip("The variables that should override the subtree variables.")]
[SharedVariableOverridesListAttribute]
[SerializeField] protected SharedVariableOverride[] m_Variables;
private Subtree[] selectedSubtrees;


public SharedVariableOverride[] SharedVariableOverrides { get => m_Variables; set => m_Variables = value; }

public Subtree[] Subtrees => selectedSubtrees == null ? new Subtree[1] { subtreeReference.Value } : selectedSubtrees;

public void EvaluateSubtrees(IGraphComponent graphComponent)
{
if (selectedSubtrees == null)
selectedSubtrees = new Subtree[1];

selectedSubtrees[0] = subtreeReference.Value;
}
}

```
 
Take a look at the RuntimeBehavior sample. It shows switching out the subtrees at runtime.
 
I am trying to do something different.
The subtrees will live outside of the graph. And can be used/reused for a set of agents

Example:
This will have a reference to a sub tree from the blackboard
public class SubTreeLoader : ActionNode, ISubtreeReference
{
public SharedVariable<Subtree> subtreeReference;

[Tooltip("The variables that should override the subtree variables.")]
[SharedVariableOverridesListAttribute]
[SerializeField] protected SharedVariableOverride[] m_Variables;
private Subtree[] selectedSubtrees;


public SharedVariableOverride[] SharedVariableOverrides { get => m_Variables; set => m_Variables = value; }

public Subtree[] Subtrees => selectedSubtrees == null ? new Subtree[1] { subtreeReference.Value } : selectedSubtrees;

public void EvaluateSubtrees(IGraphComponent graphComponent)
{
if (selectedSubtrees == null)
selectedSubtrees = new Subtree[1];

selectedSubtrees[0] = subtreeReference.Value;
}
}

When the agent picks up a weapon, the weapon will change the subtreeReference to another subtree instance

example:


public class EquipWeaponSubTree : MonoBehaviour
{
public BehaviorTree tree;
public Subtree ref1;
public Subtree ref2;

private Subtree instance1;
private Subtree instance2;


private void Start()
{
instance1 = Instantiate(ref1);
instance2 = Instantiate(ref2);

instance1.Deserialize();
instance2.Deserialize();
}

public void Equip1()
{
bool result = tree.SetVariableValue("EquippedWeapon", instance1, Opsive.GraphDesigner.Runtime.Variables.SharedVariable.SharingScope.GameObject);

Debug.LogWarning($"Equip1 set variable result: {result}");
tree.ReevaluateSubtreeReferences();
}

public void Equip2()
{
bool result = tree.SetVariableValue("EquippedWeapon", instance2, Opsive.GraphDesigner.Runtime.Variables.SharedVariable.SharingScope.GameObject);
Debug.LogWarning($"Equip2 set variable result: {result}");
tree.ReevaluateSubtreeReferences();
}}
 
In that case you don't need to do use ReevaluateSubtreeReferences and you can instead just set the Subtree property on the Behavior Tree component. Also, as of version 2.1.4 when you are pooling and deserializing the subtree you'll need to set the Pooled parameter. I still need to update the documentation on this and will be able to soon. I'll post again after the documentation has been updated.
 
I am using a single BT that already has a sub tree (the combat SubTree with the shared combat logic) configured on the Behavior Tree component.
Inside the combat SubTree I have reference to an optional SubTree Variable that can be set when the agent enters a trigger (for example).

The idea is to reuse the logic as much as possible.

Conceptually very similar to Unreal Engine’s Smart Objects:
1759849572279.png

Fast comparison generated with AI:

1759849871330.png
 
Right now, With the test code I posted above the reference in blackboard is updated but the sub tree with the weapon attack logic is not loaded (it keeps the previous loaded sub tree).
 
Thanks, I see. This actually does look related to reevaluating the subtree reference. When you call BehaviorTree.ReevaluateSubtreeReferences it will trigger the EvaluateSubtrees method. It's here where you can change the functionality. I see that you are doing this, but is the selectedSubtrees[0] pointing to the correct reference?
 
I did some tests and noticed that calling .ReevaluateSubtreeReferences() from my script doesn’t actually trigger the graph reevaluation.

It stops early inside this method:

tree.ReevaluateSubtreeReferences(); > f (!m_Data.ReevaluateSubtreeReferences(this, this, ClearTree))

public bool ReevaluateSubtreeReferences(IGraphComponent graphComponent, IGraph graph, Action onBeforeReevaluationSwap)
{
// The tree must contain tasks.
if (!Application.isPlaying || m_Tasks == null || m_Tasks.Length == 0)
{
// stops here
return false;
}

// Subtree references must exist.
if (m_SubtreeNodesReference == null || m_SubtreeNodesReference.Count == 0)
{
return false;
}

...
}

I am calling It from MonoBehaviour.OnGui() -> testing purposes

My setup is: BehaviourTree component with a SubTree reference
 
m_SubtreeNodesReference should be populated for this to work correctly. Any ISubtreeReference is used - can you see why it's not being added within BehaviorTreeData.Deserialize around line 600?

Code:
m_SubtreeNodesReference.Add(new SubtreeNodesReference()
 
Update.
After the last update, the graph reevaluation is triggered (magic fix 🙃).
Now the problem is that when the subtree is referenced with a shared variable

public SharedVariable<Subtree> subtreeReference;

When EvaluateSubtrees is called, subtreeReference.Value remains the same as before (I changed its value from another script and the blackboard shows the new value)

Tested with a static SubTree reference and it works.

I've made a small package with the logic (only has my scripts and some subtree assets).

It has 4 button that will change the shared variable value or the static sub tree value
The sub trees will Log their identifier (letter: A->pre loaded, B or C -> dynamically loaded)
 
Thanks for the repro. I will need to think about this situation more. The problem is that you are instantiating a subtree within ChangeGraph.Awake and trying to retrieve a GameObject SharedVariable within that subtree. Because the subtree is not bound to any particular GameObject it can't find the variable and assigns it to the default value.
 
Ah no no!
I try to retrieve the variable "SubTree" from the Behavior tree and set its value to one of that instantiated subtrees (they are decoupled from the Behavior tree) so I can load the external graphs.

subtree A (default value), subtree B and subtree C are swappable in LoadTreesDynamically Subtree through the shared variable.

1760285486619.png
 
Back
Top