2
\$\begingroup\$

I'm reading about behavior trees, and one of the common recommendations I've seen is to maintain a reference to your running action so that you don't have to traverse the entire tree during every game loop. Without traversing the entire tree every time, how do I check if an action should end early?

For example, suppose I have the following tree and the AI encounters an enemy that's closer than 10m, so he starts to aim his weapon. But then while he's aiming, his HP drops below 50%. I would want the agent to stop the aiming/firing sequence and move to the "use medkit" action.

enter image description here

The only way that I can think of to accomplish that sort of behavior is to evaluate every node in the active branch -- starting from the root node -- during every game loop. But based on what I've read, evaluating the entire active branch every game loop is discouraged.

Is there a way to be able to abort an action if a higher-level node is no longer valid, while not spending a lot of processing power evaluating the entire branch every game loop?

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

That is not how you do do decisions in a behavior tree.

If I understand correctly, your tree has this logic:

while(true)
{
    if (HP > 50%)
    {
        if (Visible enemy closer than 10m)
        {
            Aim Weapon
            Fire Weapon
        }
        else
        {
            Move
        }
    }
    else
    {
        Use Medkit
    }
}

This is how you should express it:

selector
{
    sequence
    {   
        HP > 50%
        selector
        {
            sequence
            {
                Visible enemy closer than 10m
                Aim Weapon
                Fire Weapon
            }
            Move
        }
    }
    Use Medkit
}

Let go over it, starting from the most inner part:

            sequence
            {
                Visible enemy closer than 10m
                Aim Weapon
                Fire Weapon
            }

This is a sequence, it will succeed if all its tasks succeed. Fail as soon as one fails. That means that if Visible enemy closer than 10m fails, you don't run Aim Weapon and Fire Weapon. Similarly, if Aim Weapon fails, you don't run Fire Weapon.

Next is this:

        selector
        {
            sequence
            {
                Visible enemy closer than 10m
                Aim Weapon
                Fire Weapon
            }
            Move
        }

This is a selector, it will fail if all its tasks fail. Succeed as soon as one succeeds. That means that if the sequences we reviewed before succeeds you don't run Move. That means that Move only runs when Visible enemy closer than 10m (or any other task of the sequence) fails.

Next up:

    sequence
    {   
        HP > 50%
        selector
        {
            sequence
            {
                Visible enemy closer than 10m
                Aim Weapon
                Fire Weapon
            }
            Move
        }
    }

Hopefully, you can see how this works… If HP > 50% fails, you don't go into the selector.

Finally:

selector
{
    sequence
    {   
        HP > 50%
        selector
        {
            sequence
            {
                Visible enemy closer than 10m
                Aim Weapon
                Fire Weapon
            }
            Move
        }
    }
    Use Medkit
}

As you can see, if the prior sequence succeeds (which implies that HP > 50% succeeds), then you don't run Use Medkit. In fact, Use Medkit would only run in scenarios where HP > 50% failed.


For example, suppose I have the following tree and the AI encounters an enemy that's closer than 10m, so he starts to aim his weapon. But then while he's aiming, his HP drops below 50%. I would want the agent to stop the aiming/firing sequence and move to the "use medkit" action.

You would have to add a task that fails the sequence, for example:

selector
{
    sequence
    {   
        HP > 50%
        selector
        {
            sequence
            {
                Visible enemy closer than 10m
                HP > 50%
                Aim Weapon
                Fire Weapon
            }
            Move
        }
    }
    Use Medkit
}

Now, the agent checks its health before aiming, and if did drop below 50%, it will move… Wait, that is not what you want.

We can of course do this:

selector
{
    sequence
    {   
        HP > 50%
        selector
        {
            sequence
            {
                Visible enemy closer than 10m
                HP > 50%
                Aim Weapon
                Fire Weapon
            }
            sequence
            {
                HP > 50%
                Move
            }
        }
    }
    Use Medkit
}

And we don't need to check health above, do we?

selector
{
    selector
    {
        sequence
        {
            Visible enemy closer than 10m
            HP > 50%
            Aim Weapon
            Fire Weapon
        }
        sequence
        {
            HP > 50%
            Move
        }
    }
    sequence
    {
        HP < 50%
        Use Medkit
    }
}

No need for nested selectors, either:

selector
{
    sequence
    {
        Visible enemy closer than 10m
        HP > 50%
        Aim Weapon
        Fire Weapon
    }
    sequence
    {
        HP > 50%
        Move
    }
    sequence
    {
        HP < 50%
        Use Medkit
    }
}

This is an useful pattern: make a selector of sequences, where each sequence lists its preconditions.

Right, but, we don't want to repeat ourselves…

Take advantage of the order in the selector:

selector
{
    sequence
    {
        HP < 50%
        Use Medkit
    }
    sequence
    {
        Visible enemy closer than 10m
        Aim Weapon
        Fire Weapon
    }
    Move
}

As you can see, this agent will only Use Medkit when HP < 50% succeeds. And if that succeeds, it will not aim or move. In other words, it only aims or move given that HP < 50% failed.


Right, but what if the health fell between aiming and firing?

There is no concept of aborting a tasks in behavior trees. Instead a task could translate to any code you want to run, and that code may fail. Thus, you could package the preconditions in the task.


It is a design principle of behavior trees to make tasks fast. If aiming takes time don't have the tree handle it.

We already have an idea of state: HP. Let us go further. We will have a target enemy.

selector
{
    sequence
    {
        HP < 50%
        Use Medkit
    }
    sequence
    {
        Has selected enemy
        Aim at enemy
    }
    sequence
    {
        Has aimed enemy
        Fire weapon
    }
    sequence
    {
        Find visible enemy closer than 10m
        Set selected enemy
    }
    Move
}

Perhaps aiming does not have to exist in the tree. Instead, if there is an enemy selected, an animation subsystem would handle aiming at it (and might even do it while moving).

However, imagine aiming happens asynchronously… Or perhaps imagine it happens beyond the control of the tree, just like losing HP happens beyond the control of the tree. However, once the agent has aimed an enemy, the tree should decide to fire the weapon.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you. That's a great answer, and I appreciate you taking the time to write something so detailed. \$\endgroup\$
    – Ben Rubin
    Commented Jun 7, 2020 at 12:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .