Blend Trees¶
Namespace: Brine2D.Animation
Blend trees drive clip selection continuously from runtime parameter values rather than discrete transitions. They are ideal for movement animations (walk/run based on speed), directional facing (eight-way movement), and any situation where a float value should smoothly pick between clips.
Both trees sit on AnimatorComponent and are evaluated by AnimationSystem before the animator is ticked each frame, so a clip change takes effect with no one-frame lag.
Priority vs. the state machine
When both a blend tree and a state machine are active, AnimationSystem evaluates the blend
tree first, then the state machine. If the state machine fires a transition on the same frame,
the state machine wins. Set only the tree or machine you want active at any given time, or use
IsEnabled on either to switch between them.
1D Blend Tree¶
AnimationBlendSelector1D maps a single float value to the nearest registered threshold and plays the corresponding clip.
Setup¶
var sel = new AnimationBlendSelector1D(animComp.Animator);
animComp.BlendSelector1D = sel;
sel.AddNode(0f, "idle");
sel.AddNode(1f, "walk");
sel.AddNode(4f, "run");
Value selects the clip whose threshold is nearest. Nodes are kept sorted by threshold.
Setting the Value¶
// Setting Value evaluates immediately
sel.Value = velocity.Length();
Speed Interpolation¶
When adjacent nodes carry a speed override, SpriteAnimator.Speed is linearly interpolated between them as Value moves between their thresholds.
sel.AddNode(0f, "walk", speed: 0.5f);
sel.AddNode(4f, "run", speed: 2.0f);
// At Value = 2.0, Speed ≈ 1.25
Key Properties¶
| Property | Default | Purpose |
|---|---|---|
Value |
0 |
Current blend parameter; triggers evaluation on set |
CrossFadeDuration |
0 |
Cross-fade seconds on clip change; 0 = hard cut |
RespectNonLoopingClips |
true |
Yield to non-looping clips started outside the tree |
AllowZeroSpeed |
false |
Allow a speed node value of exactly 0 |
IsEnabled |
true |
When false, tree evaluation is skipped |
NodeCount |
— | Number of registered nodes |
ActiveClip |
— | Name of the currently selected clip |
Events¶
sel.OnClipChanged += (prev, next) =>
{
// prev may be null on the first evaluation
};
Node Management¶
sel.AddNode(6f, "sprint", speed: 3f);
sel.RemoveNode("sprint");
sel.ClearNodes();
Validation¶
var issues = sel.ValidateNodes();
foreach (var msg in issues)
Logger.LogWarning(msg);
2D Blend Tree¶
AnimationBlendSelector2D maps a 2D float coordinate to the nearest registered node using Euclidean distance (nearest-neighbor / Voronoi). Ideal for eight-way directional movement.
Setup¶
var sel = new AnimationBlendSelector2D(animComp.Animator);
animComp.BlendSelector2D = sel;
// Eight-way directional movement
sel.AddNode( 0f, 1f, "walk_up");
sel.AddNode( 0f, -1f, "walk_down");
sel.AddNode(-1f, 0f, "walk_left");
sel.AddNode( 1f, 0f, "walk_right");
sel.AddNode( 0.7f, 0.7f, "walk_up_right");
sel.AddNode(-0.7f, 0.7f, "walk_up_left");
sel.AddNode( 0.7f, -0.7f, "walk_down_right");
sel.AddNode(-0.7f, -0.7f, "walk_down_left");
Setting the Value¶
// SetValue evaluates immediately
sel.SetValue(velocity.X, velocity.Y);
// Read back
float x = sel.X;
float y = sel.Y;
Speed Interpolation¶
When the two nearest nodes both carry a speed override, SpriteAnimator.Speed is interpolated between them by proximity.
sel.AddNode(0f, 0f, "idle", speed: 0f);
sel.AddNode(1f, 0f, "walk", speed: 1f);
sel.AddNode(4f, 0f, "run", speed: 2f);
sel.AllowZeroSpeed = true; // allow the idle node's speed = 0
Key Properties¶
| Property | Default | Purpose |
|---|---|---|
X, Y |
0 |
Current 2D blend parameter (read-only; set via SetValue) |
CrossFadeDuration |
0 |
Cross-fade seconds on clip change |
RespectNonLoopingClips |
true |
Yield to non-looping clips started outside the tree |
AllowZeroSpeed |
false |
Allow a speed node value of exactly 0 |
IsEnabled |
true |
When false, tree evaluation is skipped |
NodeCount |
— | Number of registered nodes |
ActiveClip |
— | Name of the currently selected clip |
Events¶
sel.OnClipChanged += (prev, next) => { };
Node Management¶
sel.AddNode(2f, 1f, "walk_up_right");
sel.RemoveNode("walk_up_right"); // by clip name
sel.RemoveNode(2f, 1f); // by position
sel.ClearNodes();
Validation¶
var issues = sel.ValidateNodes();
foreach (var msg in issues)
Logger.LogWarning(msg);
Blend Trees on Layers¶
Each AnimationLayer also supports a blend tree independently of the base animator:
var layer = animComp.AddLayer("legs", priority: 1);
var sel = new AnimationBlendSelector1D(layer.Animator);
layer.BlendSelector1D = sel;
sel.AddNode(0f, "idle_legs");
sel.AddNode(2f, "walk_legs");
sel.AddNode(5f, "run_legs");
See Layers for full layer setup.
Switching Between 1D and 2D¶
When both BlendSelector1D and BlendSelector2D are set on the same AnimatorComponent or layer, only BlendSelector1D is evaluated. Set the unused property to null to switch:
animComp.BlendSelector1D = null; // switch to 2D
animComp.BlendSelector2D = sel2D;
animComp.BlendSelector2D = null; // switch to 1D
animComp.BlendSelector1D = sel1D;
Interaction with the State Machine¶
Blend trees and the state machine are not mutually exclusive. A common pattern:
- Use the blend tree to drive directional walk/run clips.
- Use the state machine to handle one-shot overrides (attack, hurt, jump) with on-complete transitions back to the default state.
// Blend tree drives walk/run direction
animComp.BlendSelector2D = dirSel;
// State machine handles overrides
sm.AddAnyTriggerTransition("attack", p, "attackPressed", canInterrupt: false);
sm.AddOnCompleteTransition("attack", "idle");
sm.SetDefaultState("idle");
When the state machine fires "attack", the animator stops being driven by the blend tree for that clip's duration (because RespectNonLoopingClips is true by default). When "attack" completes and the state machine returns to "idle", the blend tree resumes control.