Get started with Behavior Trees
Decorators are a way to alter the behavior of an instance of a class. Unlike a composite, a decorator only ever has one child. That child could be an action node, a condition node or even a composite itself.
Decorator Pattern is a design pattern in object-oriented programming. You can read more about it here.
There are many different decorators that are useful. This tutorial only needs two, the Inverter
and the Timer
.
Before we get started with each of these, we will need a Decorator
class to inherit from. Create a new abstract class in /WUG/Scripts/Behaviors called Decorator
, which will inherit from Node
. Add the following code:
public abstract class Decorator : Node
{
public Decorator(string displayName, Node node)
{
Name = displayName;
ChildNodes.Add(node);
}
}
The main difference between this and the Composite
script, is that the Decorator
will only accept a single child, rather than an array.
In the Understanding Behavior Trees section we touched a bit on the Inverter
decorator earlier in this tutorial. The concept behind it is simple, it will flip the result of its child node before passing it up the tree:
NodeStatus.Success
will now be NodeStatus.Failure
.NodeStatus.Failure
will now be NodeStatus.Success
.NodeStatus.Running
, as this means the child is still processing its logic.Let’s get it added to the project:
Decorator
. Add the following code for the constructor, OnRun
and OnReset
methods: public class Inverter : Decorator
{
public Inverter(string displayName, Node childNode) : base(displayName, childNode) { }
protected override void OnReset() { }
protected override NodeStatus OnRun()
{
//Confirm that a valid child node was passed in the constructor
if (ChildNodes.Count == 0 || ChildNodes[0] == null)
{
return NodeStatus.Failure;
}
//Run the child node
NodeStatus originalStatus = (ChildNodes[0] as Node).Run();
//Check the status of the child node and invert if it is Failure or Success
switch (originalStatus)
{
case NodeStatus.Failure:
return NodeStatus.Success;
case NodeStatus.Success:
return NodeStatus.Failure;
}
// Otherwise, it's still running or returning an Unknown code
return originalStatus;
}
}
The OnRun()
method will first confirm it has a valid child node. If it does, it’ll run it and check the status. If the child returned NodeStatus.Success
or NodeStatus.Failure
, the status will be flipped and returned otherwise it returns the original.
The goal of Timer
is to continue to run the child node until the timer expires. The example below would cause the AI to make lock picks for two seconds. in the example below the AI must wait two seconds before making a lock pick and being able to pick the lock of the door.
Create a new class in the Decorators folder called Timer, which will inherit from Decorator
. Add the following code for the global variables and constructor:
public class Timer : Decorator
{
private float m_StartTime;
private bool m_UseFixedTime;
private float m_TimeToWait;
public Timer(float timeToWait, Node childNode, bool useFixedTime = false) : base($"Timer for {timeToWait}", childNode)
{
m_UseFixedTime = useFixedTime;
m_TimeToWait = timeToWait;
}
}
The constructor for Timer
takes two new parameters:
Next, add the OnRun
and OnReset
methods:
protected override void OnReset() { }
protected override NodeStatus OnRun()
{
//Confirm that a valid child node was passed in the constructor
if (ChildNodes.Count == 0 || ChildNodes[0] == null)
{
return NodeStatus.Failure;
}
// Run the child node and calculate the elapsed
NodeStatus originalStatus = (ChildNodes[0] as Node).Run();
//If this is the first eval, then the start time needs to be set up.
if (EvaluationCount == 0)
{
StatusReason = $"Starting timer for {m_TimeToWait}. Child node status is: {originalStatus}";
m_StartTime = m_UseFixedTime ? Time.fixedTime : Time.time;
}
//Calculate how much time has passed
float elapsedTime = Time.fixedTime - m_StartTime;
//If more time has passed than we wanted, it's time to stop
if (elapsedTime > m_TimeToWait)
{
StatusReason = $"Timer complete - Child node status is: { originalStatus}";
return NodeStatus.Success;
}
//Otherwise, keep running
StatusReason = $"Timer is {elapsedTime} out of {m_TimeToWait}. Child node status is: {originalStatus}";
return NodeStatus.Running;
}
OnRun
will setup the timer when the node runs for the first time. Each subsequent run will confirm that the timer has not expired. Every time OnRun
is called, it will run the child node.
In this section you learned about decorators and how they can be useful for changing the behavior of an instance of a node. There are many more decorators that can be particularly useful, such as:
Behavior Tree Visualizer has examples of these and more in the Standard Behavior Tree Nodes sample project. Go here for more details.