Lifecycle Hooks
Define behavior at different execution phases of a node.
Execution Phases
Every node execution follows this sequence:
First-Time Activation (Only on first tick)
- 1. OnEnabled Called once when the node is first activated (after variable initialization)
Normal Execution (Every tick while running)
- 2. OnEnter Called once when the node starts or re-enters execution
- 3. OnBaseTick Defines the core behavior and returns the node's status (Success/Failure/Running)
- 4. OnTick Side-effect hook for logging, observations, and external systems (no status)
- 5. OnSuccess / OnFailure Called when the node finishes with a specific status (Success or Failure)
- 6. OnExit Called when the node finishes (Success or Failure)
Reset/Cleanup (Only during reset operations)
- 7. OnDisabled Called ONLY when the node is reset (NOT part of normal success/failure flow)
Using Lifecycle Hooks
These lifecycle methods must be called inside a node's definition. Here's an example:
Sequence(() =>
{
OnEnter(() => Debug.Log("Entered Sequence Node!"));
OnExit(() => Debug.Log("Exited Sequence Node!"));
Wait(1f);
// Rest of your nodes...
}); Sequence(() =>
{
OnEnter(() => Debug.Log("Entered Sequence Node!"));
OnExit(() => Debug.Log("Exited Sequence Node!"));
Wait(1f);
// Rest of your nodes...
}); Complete Lifecycle Flow
The complete lifecycle of a node from creation to reset looks like this:
OnEnabled Hook
Called once when the node is first activated (transitions from None to Enabling state). This happens:
- On the first tick of the node
- After variable initializers run
- Before OnEnter is called
- Sets the node's
Activeflag totrue
OnEnabled marks the node as "alive" in the behavior tree and is perfect for one-time setup that persists across re-entries.
When OnEnabled Runs
OnEnabled runs:
- ✅ First tick of the node
- ✅ When tree is first started
- ✅ After
ResetImmediately()orResetGracefully()is called and node is ticked again
OnEnabled does NOT run:
- ❌ During re-entry (when reactive invalidation causes the node to restart)
- ❌ When
Tick(allowReEnter: true)is called on a Done node - ❌ After OnExit when the node completes normally
OnEnabled vs OnEnter
| OnEnabled | OnEnter |
|---|---|
| Called once on first activation | Called every time node enters |
| Runs before OnEnter | Runs after OnEnabled |
| For persistent setup | For per-execution initialization |
| Sets Active = true | Node already active |
| Not called on re-entry | IS called on re-entry |
Use OnEnabled For
- Event subscriptions that should persist across re-entries
- Resource allocation (NavMeshAgent setup, animation controller references)
- System registration (registering with game managers)
- One-time expensive setup that shouldn't repeat on re-entry
OnEnabled and Reactive Trees
In reactive behavior trees, when a node invalidates and re-enters, OnEnabled is NOT called again. Only OnEnter is called. This is intentional - the node is still "active" in the tree, just restarting its execution.
Best Practice
Use OnEnabled for expensive setup operations and resource allocation. Use OnEnter for lightweight per-execution initialization.
OnDisabled Hook
Called only during reset operations - this is the key difference from other lifecycle hooks. OnDisabled is NOT part of the normal success/failure flow.
OnDisabled runs when:
- ✅
ResetImmediately()is called on the node or its parent - ✅
ResetGracefully()is called on the node or its parent - ✅ During reactive invalidation when subsequent nodes need to be reset
- ✅ When the tree is destroyed (e.g.,
OnDestroy()callsTree.ResetImmediately())
OnDisabled does NOT run when:
- ❌ Node completes normally with Success or Failure
- ❌ OnExit is called at the end of execution
- ❌ Node transitions from Running to Done state
- ❌ Reactive re-entry occurs (node restarts via invalidation)
Why OnDisabled is Separate
ClosureBT separates normal completion cleanup (OnExit) from reset/destroy cleanup (OnDisabled) to distinguish between two fundamentally different scenarios:
- OnExit → "I finished my task, clean up this execution"
- OnDisabled → "I'm being removed from the tree, clean up all resources"
This distinction is especially important for reactive trees where nodes may re-enter multiple times without being disabled.
Use OnDisabled For
- Unsubscribing from events registered in OnEnabled
- Releasing allocated resources (object pools, NavMeshAgents)
- Disconnecting from systems (event buses, game managers)
- Final cleanup that should only happen when the node is truly done (reset/destroyed)
Common Mistake
Do not expect OnDisabled to be called when a node completes normally! Use OnExit for normal completion cleanup, and OnDisabled only for reset/destroy scenarios.
Async Support
Both OnEnabled and OnDisabled support async/await with UniTask and receive CancellationToken parameters for proper cleanup:
OnEnabled(async ct =>
{
await LoadResourcesAsync(ct);
Debug.Log("Resources loaded");
});
OnDisabled(async ct =>
{
await SaveStateAsync(ct);
await UnloadResourcesAsync(ct);
Debug.Log("Cleanup complete");
}); OnEnabled(async ct =>
{
await LoadResourcesAsync(ct);
Debug.Log("Resources loaded");
});
OnDisabled(async ct =>
{
await SaveStateAsync(ct);
await UnloadResourcesAsync(ct);
Debug.Log("Cleanup complete");
}); OnEnterExitPair
Combines OnEnter and OnExit with matched actions:
OnEnterExitPair(
onEnter: () => Debug.Log("Starting"),
onExit: () => Debug.Log("Done")
); OnEnterExitPair(
onEnter: () => Debug.Log("Starting"),
onExit: () => Debug.Log("Done")
); The OnExit() will only run if its OnEnter() counterpart has run. During interruptions/cancellations, it's possible not all OnEnters() will run due to cancellation. Use this to ensure that is OnEnter() counterpart has run for its OnExit() pair