Quick Start
Get up and running with ClosureBT in just a few minutes with this hands-on introduction.
Conceptual Learning
If you are new to behavior trees, it's recommended to read Behavior Trees for AI: How They Work first. These docs focus more on ClosureBT usage rather than behavior tree fundamentals.
Getting Started
For the best experience with ClosureBT, start by importing the static AI class:
using static ClosureBT.BT; using static ClosureBT.BT;
This gives you direct access to all ClosureBT nodes and functions without the AI. prefix.
Quick Overview
Here's the basic setup for using ClosureBT
using static ClosureBT.BT;
public class YourNPC : MonoBehaviour
{
public Node AI; // This will expose a "Open Node Graph" button in the inspector
private void Awake() => AI = Sequence("NPC AI", () =>
{
Do(() => Debug.Log("Hello World"));
Wait(1);
});
private void Update() => AI.Tick();
private void OnDestroy() => AI.ResetImmediately();
} using static ClosureBT.BT;
public class YourNPC : MonoBehaviour
{
public Node AI; // This will expose a "Open Node Graph" button in the inspector
private void Awake() => AI = Sequence("NPC AI", () =>
{
Do(() => Debug.Log("Hello World"));
Wait(1);
});
private void Update() => AI.Tick();
private void OnDestroy() => AI.ResetImmediately();
} Cleanup Tip
You should probably callResetImmediately() on your nodes in OnDestroy to ensure proper cleanup.
Sequence - The AND Logic
Sequences are like A() && B() && C() in normal code. They run children in order and succeed only if all children succeed.
Node Chase(Action lifecycle = null) => Sequence("Chase", () =>
{
Condition("Target Too Far", () => Vector3.Distance(transform.position, target.position) > 5f);
Leaf("Move Towards", () =>
{
OnEnter(() => Debug.Log("Begun to chase target!"));
OnBaseTick(() =>
{
var distance = Vector3.Distance(transform.position, target.position);
if (distance > 3f)
{
transform.position = Vector3.MoveTowards(transform.position, target.position, speed * Time.deltaTime);
return Status.Running;
}
else
return Status.Success;
});
});
// The second parameter allows us to attach lifecycle
Wait(1f, () =>
{
OnSuccess(() => Debug.Log("Okay we're close enough now!"));
});
lifecycle?.Invoke();
}); Node Chase(Action lifecycle = null) => Sequence("Chase", () =>
{
Condition("Target Too Far", () => Vector3.Distance(transform.position, target.position) > 5f);
Leaf("Move Towards", () =>
{
OnEnter(() => Debug.Log("Begun to chase target!"));
OnBaseTick(() =>
{
var distance = Vector3.Distance(transform.position, target.position);
if (distance > 3f)
{
transform.position = Vector3.MoveTowards(transform.position, target.position, speed * Time.deltaTime);
return Status.Running;
}
else
return Status.Success;
});
});
// The second parameter allows us to attach lifecycle
Wait(1f, () =>
{
OnSuccess(() => Debug.Log("Okay we're close enough now!"));
});
lifecycle?.Invoke();
}); Key Takeaways
- • Lifecycle methods like OnEnter and OnExit attach functionality to nodes
- • Most functions have overloads where the first parameter is the node name
- • Use a lifecycle parameter in custom nodes to add more functionality easily
Selector - The OR Logic
Selectors are like A() || B() || C() in normal code. They try children sequentially until one succeeds.
Node SomeAbstractChoices() => Selector("Choices!!!", () =>
{
D.Condition(() => Foo == true);
Sequence(() =>
{
Wait(1, () => OnSuccess(() => Debug.Log("Cool!")));
Chase(() =>
{
OnSuccess(() => Debug.Log("We reached the target"));
OnFailure(() => Debug.Log("We gave up chasing the target"));
});
});
D.Condition(() => Bar == true);
Wait("Pretend to do something", 1);
// A final node that will run if Foo and Bar are both false
Wait("Final", 1);
}); Node SomeAbstractChoices() => Selector("Choices!!!", () =>
{
D.Condition(() => Foo == true);
Sequence(() =>
{
Wait(1, () => OnSuccess(() => Debug.Log("Cool!")));
Chase(() =>
{
OnSuccess(() => Debug.Log("We reached the target"));
OnFailure(() => Debug.Log("We gave up chasing the target"));
});
});
D.Condition(() => Bar == true);
Wait("Pretend to do something", 1);
// A final node that will run if Foo and Bar are both false
Wait("Final", 1);
}); In terms of standard code, this might look like:
void SomeAbstractChoices()
{
if (Foo == true)
{
if (Wait(...) && Chase())
return Status.Success;
}
else if (Bar == true)
{
if (Wait(...))
return Status.Success;
}
else
{
if (Wait(...))
return Status.Success;
}
return Status.Failure;
} void SomeAbstractChoices()
{
if (Foo == true)
{
if (Wait(...) && Chase())
return Status.Success;
}
else if (Bar == true)
{
if (Wait(...))
return Status.Success;
}
else
{
if (Wait(...))
return Status.Success;
}
return Status.Failure;
} The real convenience comes from lifecycle methods that allow us to add non instaneous entry and exit methods.
Why Lifecycle Methods Matter
Lifecycle methods like OnEnter, OnExit, OnTick, OnSuccess, and OnFailure make it incredibly easy to initialize and cleanup state without cluttering your behavior tree logic. You can:
- • Attach setup code to any node with OnEnter
- • Ensure cleanup always happens with OnExit
- • React to specific outcomes with OnSuccess and OnFailure
- • Do all this without modifying the core node structure
- • Supports async operations
Next: Nodes
Learn about nodes, the fundamental building blocks of behavior trees.
Read about Nodes →