Nodes
Nodes are the fundamental building blocks of behavior trees in ClosureBT.
Overview
Every node in ClosureBT has three possible statuses during execution:
- RunningThe node is currently executing and hasn't finished yet.
- SuccessThe node completed successfully.
- FailureThe node failed to complete.
Core Concepts: Reactive Pattern & Invalidation
Two important core features that build on top of nodes are the Reactive Pattern and Invalidation checks. These are essential tools for building responsive systems that react to changing conditions.
The Reactive Pattern allows nodes to automatically detect when conditions change and reset their execution, making it easy to build behaviors that respond immediately to state changes without complex manual state management.
Creating Nodes
Nodes are created using static methods from ClosureBT. The simplest way to create a node is with a name and a setup action:
// Create from an existing node
Node MySequence() => Selector("Foo", () =>
{
Wait(1);
// ...
});
// A custom leaf node
Node Baz() => Leaf("Baz", () =>
{
OnBaseTick(() =>
{
return SomeCalculation() ? Status.Success : Status.Running;
});
}); // Create from an existing node
Node MySequence() => Selector("Foo", () =>
{
Wait(1);
// ...
});
// A custom leaf node
Node Baz() => Leaf("Baz", () =>
{
OnBaseTick(() =>
{
return SomeCalculation() ? Status.Success : Status.Running;
});
}); Resetting Nodes
You can reset a node to its initial state at any time:
// Reset immediately (synchronous)
AI.ResetImmediately();
// Reset gracefully (may take multiple ticks for async cleanup)
AI.ResetGracefully();
// Common use: clean up when GameObject is destroyed
private void OnDestroy() => AI.ResetImmediately(); // Reset immediately (synchronous)
AI.ResetImmediately();
// Reset gracefully (may take multiple ticks for async cleanup)
AI.ResetGracefully();
// Common use: clean up when GameObject is destroyed
private void OnDestroy() => AI.ResetImmediately(); Composite Nodes
Composite nodes are special nodes that contain child nodes. They define how their children are executed:
Node Root() => Selector("Root", () =>
{
// Children added implicitly in order they appear
Sequence("Patrol", () =>
{
MoveAroundPoints(() => PatrolPoints, () =>
{
OnEnter(() => Debug.Log("Began Patrolling"));
OnExit(() => Debug.Log("Stopped Patrolling")
});
Wait(2f);
});
Attack();
}); Node Root() => Selector("Root", () =>
{
// Children added implicitly in order they appear
Sequence("Patrol", () =>
{
MoveAroundPoints(() => PatrolPoints, () =>
{
OnEnter(() => Debug.Log("Began Patrolling"));
OnExit(() => Debug.Log("Stopped Patrolling")
});
Wait(2f);
});
Attack();
}); Parameters and Return Values
Nodes can have parameters and return values by passing them around as Func<T>
Node Patrol(Func<List<T>> points, out Func<GameObject> getTargetSeen)
{
VariableType<GameObject> targetSeen; // Variables must be made inside a node!
targetSeen = () => targetSeen.Value;
return Leaf("Patrol", () =>
{
targetSeen = new();
OnBaseTick(() =>
{
// Patrol logic here
// If target seen, set _targetSeen and return Success
if (Sight.HasTargetInSight(out var target))
targetSeen.Value = target;
return targetSeen.Value ? Status.Success : Status.Running;
});
});
} Node Patrol(Func<List<T>> points, out Func<GameObject> getTargetSeen)
{
VariableType<GameObject> targetSeen; // Variables must be made inside a node!
targetSeen = () => targetSeen.Value;
return Leaf("Patrol", () =>
{
targetSeen = new();
OnBaseTick(() =>
{
// Patrol logic here
// If target seen, set _targetSeen and return Success
if (Sight.HasTargetInSight(out var target))
targetSeen.Value = target;
return targetSeen.Value ? Status.Success : Status.Running;
});
});
} Now it can be used like this
Sequence(() =>
{
Patrol(() => PatrolPoints, out var seenTarget);
Attack(seenTarget);
}); Sequence(() =>
{
Patrol(() => PatrolPoints, out var seenTarget);
Attack(seenTarget);
});