Yield
Defer node creation until runtime to enable recursive task decomposition, dynamic behavior switching, and state machines.
Introduction
Yield defers node creation until runtime, enabling patterns that aren't possible with static trees. This includes recursive task decomposition, dynamic behavior subtree swapping, and state machines that adapt to game state.
Unlike static composites where all children are created upfront, yielded nodes are created and managed during execution. This deferred execution allows nodes to call themselves recursively, which is essential for goal-oriented planning systems.
Yield enables creating reusable behavior functions that compose together. Complex behaviors can emerge from simple, testable components.
YieldSimpleCached
YieldSimpleCached() executes a dynamically created node. The node is created once, cached, and reused across ticks. Perfect for recursion and parameterized behaviors.
Recursive Task Decomposition
Yield enables recursive task decomposition where nodes can call themselves to resolve complex dependencies. This allows high-level goals to be automatically broken down into smaller steps.
Here's a crafting system where acquiring an item might require crafting it, which requires acquiring ingredients, which may themselves need to be crafted:
Node AcquireItem(Func<string> getItemID) => Selector(() =>
{
var itemID = Variable(getItemID);
// Already have it?
Condition(() => Inventory.Contains(itemID.Value));
// Can collect it from world?
D.Condition(() => ItemExists(itemID.Value));
YieldSimpleCached(() => CollectItem(itemID));
// Need to craft it? Recursively acquire ingredients
D.Condition(() => IsCraftable(itemID.Value));
YieldSimpleCached(() => CraftItem(itemID));
});
Node CraftItem(Func<string> getItemID) => Sequence(() =>
{
var recipe = Variable(() => GetRecipe(getItemID()));
// Acquire each ingredient recursively!
D.ForEach(() => recipe.Value.Ingredients, out var item);
YieldSimpleCached(() => AcquireItem(item));
Wait(() => recipe.Value.CraftTime);
}); Node AcquireItem(Func<string> getItemID) => Selector(() =>
{
var itemID = Variable(getItemID);
// Already have it?
Condition(() => Inventory.Contains(itemID.Value));
// Can collect it from world?
D.Condition(() => ItemExists(itemID.Value));
YieldSimpleCached(() => CollectItem(itemID));
// Need to craft it? Recursively acquire ingredients
D.Condition(() => IsCraftable(itemID.Value));
YieldSimpleCached(() => CraftItem(itemID));
});
Node CraftItem(Func<string> getItemID) => Sequence(() =>
{
var recipe = Variable(() => GetRecipe(getItemID()));
// Acquire each ingredient recursively!
D.ForEach(() => recipe.Value.Ingredients, out var item);
YieldSimpleCached(() => AcquireItem(item));
Wait(() => recipe.Value.CraftTime);
}); The tree handles arbitrarily deep dependency chains. For example, acquiring a sword would resolve to needing iron ore and wood, which might require crafting from iron nuggets, which requires mining—all executed in the correct order from a single high-level goal.
This works because AcquireItem calls CraftItem, which calls AcquireItem again for each ingredient. This recursive pattern is only possible because YieldSimpleCached defers node creation until runtime.
YieldDynamic
YieldDynamic() allows swapping entire behavior subtrees based on conditions. The function is called every tick to determine the active node, enabling state machines and dynamic behavior switching.
This provides full control over what node yields. Useful for scenarios like enemies that switch between patrol, chase, and attack behaviors, or any system that needs to change its behavior tree at runtime.
State Machine Example
Here's an enemy AI that switches between patrol, chase, and attack behaviors based on game state:
enum State { Patrol, Chase, Attack }
YieldDynamic("Enemy AI", controller =>
{
controller
.WithResetYieldedNodeOnNodeChange()
.WithResetYieldedNodeOnSelfExit();
Node patrolNode = null;
Node chaseNode = null;
Node attackNode = null;
return _ => currentState switch
{
State.Patrol => patrolNode ??= PatrolBehavior(),
State.Chase => chaseNode ??= ChaseBehavior(),
State.Attack => attackNode ??= AttackBehavior(),
_ => IdleBehavior()
};
}); enum State { Patrol, Chase, Attack }
YieldDynamic("Enemy AI", controller =>
{
controller
.WithResetYieldedNodeOnNodeChange()
.WithResetYieldedNodeOnSelfExit();
Node patrolNode = null;
Node chaseNode = null;
Node attackNode = null;
return _ => currentState switch
{
State.Patrol => patrolNode ??= PatrolBehavior(),
State.Chase => chaseNode ??= ChaseBehavior(),
State.Attack => attackNode ??= AttackBehavior(),
_ => IdleBehavior()
};
}); Node Caching
It's very important to not load the node eagerly!
You'll most likely want to cache your nodes somehow, otherwise you'll be making a new node per tick.
YieldController Configuration
Configure behavior using fluent API:
WithResetYieldedNodeOnNodeChange()- Reset old node when switching (recommended)WithResetYieldedNodeOnSelfExit()- Reset node when yield exitsWithConsumeTickOnStateChange()- Control immediate vs next-tick transitions
Choosing the Right Yield Type
Both yield types defer node creation, but they're optimized for different use cases:
| Feature | YieldSimpleCached | YieldDynamic |
|---|---|---|
| Nodes managed | Single node | Multiple nodes |
| Evaluation | Once (cached) | Every tick |
| Configuration | None (automatic) | YieldController API |
| Best for | Recursion, simple yields | State machines, behavior switching |
| Boilerplate | Minimal | More verbose |
Use YieldSimpleCached when you need to call a single behavior function—especially for recursion and parameterized nodes. Use YieldDynamic when you need to switch between multiple different behaviors based on runtime conditions.
Summary
Yield enables several important patterns by deferring node creation until runtime:
- Recursive task decomposition — Nodes can call themselves to break down complex goals into smaller steps
- Dynamic behavior switching — Swap entire subtrees based on runtime conditions
- Modular composition — Create reusable behavior functions that compose together
- State machines — Implement adaptive AI that responds to changing game state
Performance Tips
- Always cache nodes: Use the
node ??= CreateNode()pattern to avoid recreating nodes every tick - Watch recursion depth: Very deep hierarchies can impact performance—consider iterative alternatives for extreme cases
- Choose the right tool: YieldSimpleCached for single behaviors and recursion, YieldDynamic for state machines and behavior switching
Next: Reactive Variables
Learn about Functional Reactive Programming and composable transformation pipelines.
Read about Reactive Variables →