Starting a new behavior tree from inside a custom Action can break the new tree

nanodeath

New member
If you have a BD Action that starts a new behavior tree on the current object, you have to be careful about what you return from that Action.

Look at this snippet of source:

C#:
if (status != TaskStatus.Running) {
    // pop the task immediately if the task is instant. If the task is not instant then wait for the next update
    if (task.IsInstant) {
        PopTask(behaviorTree, taskIndex, stackIndex, ref status, true);
    } else {
        behaviorTree.nonInstantTaskStatus[stackIndex] = status;
    }
}

This happens in BehaviorManager#RunTask immediately after the Action's OnUpdate method is called. It doesn't check for the (admittedly unlikely) event that the behavior tree changed; so unless you return `TaskStatus.Running`, PopTask will be called on the new behavior tree, which causes weird behavior (like Conditional nodes getting skipped). The workaround here is to make sure to return TaskStatus.Running in the task that switches behavior trees.

I'm not sure what exactly the right answer here is; more documentation, or logging a warning if the tree changed but we didn't return TaskStatus.Running, or...just making it work.

For me, I'm going to look into whether I can actually use BD's `StartBehaviorTree` which might be a safer way to do what I'm doing rather than reassigning the behavior in my own code.
 
How are you changing out the behavior tree? If you are switching external behavior trees you should not be changing the tree within the same task that is being switched out. I can add this to the documentation as I think it's easy to workaround this.
 
It's as you say -- I'm basically doing GetComponent<BehaviorTree>().ExternalBehavior = newBehavior from within an Action. If the answer is "don't do that", that's fine, it's just not obvious to me that isn't something I should do.

That said, I've changed it so that I've created essentially my own version of StartBehaviorTree and it's working much better:

C#:
public class TakeJob : Action {
    private ExternalBehavior originalBehavior;
    private Pawn pawn;
    private Task task;
    private BehaviorTree newBehavior;

    public override void OnStart() {
        pawn = GetComponent<Pawn>();
        task = pawn.jobQueue.RequestJob(pawn);
        
        newBehavior = gameObject.AddComponent<BehaviorTree>();
        newBehavior.BehaviorName = task.name;
        var args = task.parameters;
        if (args != null) {
            foreach (var (key, value) in args) {
                newBehavior.SetVariableValue(key, value);
            }
        }
        newBehavior.ExternalBehavior = task.externalBehavior;
        newBehavior.OnBehaviorEnd += AllDone;
    }

    public override TaskStatus OnUpdate() {
        return newBehavior.ExecutionStatus;
    }

    private void AllDone(Behavior behavior) {
        newBehavior.OnBehaviorEnd -= AllDone;
        Object.Destroy(newBehavior);
        newBehavior = null;
    }
}
 
Top