Unigine 2.22 Animation Overhaul: State Machines, Layer Blending, and the New Skeletal Workflow
In a nutshell
Explore Unigine 2.22's animation tooling overhaul—new state machines, layer blending, and skeletal workflows with practical code examples and migration tips for game developers.
The Animation Workflow That Made Unigine Developers Leave the Engine
Every Unigine developer has hit the same wall: the renderer pushes gorgeous PBR scenes at 120fps, but authoring a simple idle-to-run blend takes an hour of scripting hacks and manual bone tweaking. That discrepancy between visual fidelity and animation tooling has been Unigine's open secret for years, and it's the reason most small teams evaluating Unigine eventually walk away.
With Unigine 2.22, the team finally addressed this gap head-on. The release introduces a visual animation state machine editor, additive layer blending with per-bone masks, an improved FBX import pipeline with avatar skeleton mapping, and a new procedural motion warping API. For teams using Unigine for simulation, architectural visualization, or industrial applications who've been cobbling together animation logic in code for years — this changes the day-to-day workflow significantly.
This guide walks through what actually changed, how to migrate from the old scripting-heavy approach to the new state machine tools, and where the edge cases bite hardest.
What Changed in Unigine 2.22's Animation System
The Old Way: Script-Driven Animation Logic
Before 2.22, triggering an animation transition in Unigine meant writing the logic yourself:
// UnigineScript — old pattern, pre-2.22
AnimLayer idle_layer = new AnimLayer();
idle_layer.SetAnimation("idle.anim");
idle_layer.Loop = true;
AnimLayer run_layer = new AnimLayer();
run_layer.SetAnimation("run.anim");
run_layer.Loop = true;
void update(float speed) {
float blend = clamp(speed / 6.0, 0.0, 1.0);
if (blend > 0.01f) {
idle_layer.SetWeight(1.0 - blend);
run_layer.SetWeight(blend);
} else {
idle_layer.SetWeight(1.0);
run_layer.SetWeight(0.0);
}
}
This works for a two-state system, but scaling it to crouch, jump, aim, sprint, and 15 attack variations becomes unmaintainable fast. Every team ends up with a hand-rolled state machine in script, and every team ends up fighting transition pops and blending glitches.
The 2.22 Approach: Declarative State Machines
Unigine 2.22 introduces the AnimationGraph system — a node-based state machine editor embedded in the Unigine Editor, paired with a scripting API for runtime control. States map to animation clips or blend spaces. Transitions define conditions and durations. The engine handles interpolation, interrupt blending, and layer composition internally.
The key improvements:
- Visual state machine editor — define states, transitions, and conditions in the editor with a graph preview
- Layer stacking with bone masks — blend upper-body aim over lower-body locomotion using named bone groups
- Additive animation support — apply lean, head look, and recoil as offsets rather than full pose replacements
- Improved FBX import — avatar skeleton mapping that preserves bone hierarchies across character variants
- Motion warping — root motion can be redirected and blended at runtime for climbing, vaulting, and cover transitions
Setting Up the New Animation Pipeline Step by Step
Step 1: Define Your Avatar Skeleton
The avatar skeleton is the linchpin of the 2.22 workflow. It's a named bone template that maps skeletal meshes from DCC tools (Blender, Maya, 3ds Max) to a canonical skeleton the animation state machine references.
If you skip this step — and many developers learning the system do — animations will play on the wrong bones or not play at all. The engine won't throw an error; it silently uses the first bone it finds with a matching name, which causes bizarre rendering artifacts.
In the Editor's Skeleton Asset panel, define your avatar:
// Avatar definition in Unigine's data format
avatar {
name = "humanoid_standard";
bones {
root = "Hips";
left_arm = "LeftArm";
right_arm = "RightArm";
left_leg = "LeftLeg";
right_leg = "RightLeg";
spine = "Spine";
head = "Head";
}
}
Every character asset in your project shares this avatar definition. When you import a new humanoid, you map its bone names to the avatar names once, and every animation clips from every character works with every other character's animations. The typical setup time for this mapping is 10-15 minutes per character, versus the old approach of manually naming anchor points per animation clip, which took 30-60 minutes per character and broke whenever you retargeted.
Step 2: Build the State Machine in the Editor
Open the AnimationGraph editor from Unigine 2.22's asset browser. For a basic character controller, you'll want at minimum:
- Idle state — looping idle animation, entry state
- Locomotion blend space — 2D blend of walk/jog/run based on speed and direction parameters
- Jump state — non-looping jump start and airborne transitions
- Land state — staged recovery with interruptible transition back to locomotion
Each state references an animation clip or blend space. Transitions connect states with condition parameters — floats, bools, or triggers you set from code at runtime.
[Idle] --(speed > 0.1)--> [Locomotion]
[Locomotion] --(is_jumping == true)--> [Jump]
[Jump] --(on_ground == true)--> [Land]
[Land] --(land_finished == true)--> [Idle]
[Damage_Taken] --(hit_received == true)--> [Flinch]
[Flinch] --(flinch_finished == true)--> [Idle]
Step 3: Drive It from Code
The scripting side is simplified dramatically. Instead of manually computing blend weights, you push parameter values each frame:
// UnigineScript — driving the 2.22 AnimationGraph
ObjectMeshSkinned character_node;
AnimationGraph anim_graph;
int init() {
character_node = node_cast(engine.editor.getNode("player_character"));
anim_graph = new AnimationGraph(character_node);
anim_graph.Load("animations/player_graph.animgraph");
return 1;
}
int update() {
float speed = length(character_node.getPositionVelocity());
bool is_jumping = !character_node.isOnGround();
anim_graph.SetFloat("speed", speed);
anim_graph.SetFloat("direction", character_node.getTurnAngle());
anim_graph.SetBool("is_jumping", is_jumping);
anim_graph.SetBool("on_ground", character_node.isOnGround());
if (received_damage) {
anim_graph.Trigger("hit_received");
received_damage = false;
}
anim_graph.Update();
return 1;
}
The old version of this code was 120-180 lines of manual blend management. The new version is under 30 lines because the state machine, blending, and transition logic live in the graph asset.
Step 4: Configure Layer Blending for Upper/Lower Body
For a character that needs to aim while running, you need two animation layers blended with bone masks. In the 2.22 AnimationGraph editor:
Layer 0 (base): Locomotion state machine — affects hips, legs, spine core
Layer 1 (upper body override): Aim blend space — affects spine, arms, head
Layer 2 (additive): Recoil animation — additive offset on right arm and spine
The bone masks use the avatar bone groups you defined in Step 1. The mask blend weight defines how much the override layer replaces the base layer for those bones. A weight of 1.0 means total replacement; 0.7 means partial blending (useful for blending aim angle influence on the upper spine).
This layering structure is the core reason animation moved to a state machine system rather than staying script-driven. Manual mask blending breaks catastrophically when you add a third or fourth layer. The new system handles it by enforcing an evaluation order and compositing layers sequentially before skinning.
Procedural Animation: Motion Warping and IK
What Motion Warping Solves
Motion warping redirects root motion at runtime. The canonical example: your character's vault animation has root motion that moves the capsule 2 meters forward, but the obstacle is 1.5 meters away. Without motion warping, the character either floats over the gap or clips into the wall. With motion warping, the root motion target is set to the obstacle edge, and the animation trajectory bends to match.
In Unigine 2.22:
// Setting the motion warp target during a vault
Vector3 vault_edge = getVaultEdge(ground_check.point, obstacle.normal);
// The AnimationGraph exposes a warp target parameter
anim_graph.SetWarpTarget("vault_end_point", vault_edge);
anim_graph.Trigger("start_vault");
This is significant for Unigine's industrial and simulation customers as well. Training simulators with humanoid characters need characters to interact with environment geometry accurately. Motion warping provides that without per-animation hand-tuning of foot placement.
IK Integration for Runtime Posing
Unigine 2.22 exposes an IK solver that runs after animation layer evaluation. The two most common uses:
Foot IK — cast rays from each foot joint downward, adjust leg bend to match ground slope. Prevent the "feet floating 5cm above uneven terrain" look that plagues most Unigine projects.
Aim IK — track the camera or crosshair direction by rotating the spine chain and clamping head rotation. Essential for any third-person shooter built in Unigine.
// Foot IK setup — called each frame after anim_graph.Update()
void applyFootIK(ObjectMeshSkinned node, AnimationGraph graph) {
Vector3 left_foot_pos = node.getBoneWorldPosition("LeftFoot");
Vector3 right_foot_pos = node.getBoneWorldPosition("RightFoot");
float left_ground = castRayGround(left_foot_pos); // returns Y offset
float right_ground = castRayGround(right_foot_pos);
// Smoothly offset the pelvis to the lower foot position
float pelvis_offset = min(left_ground, right_ground);
graph.SetFootIKPelvisOffset(pelvis_offset);
graph.SetFootIKTarget("LeftFoot", left_ground);
graph.SetFootIKTarget("RightFoot", right_ground);
}
You'll want to pass ground normal data into the foot IK as well — if the feet are on a 30-degree slope, the ankle rotation needs to match. Skip that and feet will be positioned correctly on Y but rotated flat, looking worse than no IK at all.
The FBX Import Overhaul: What to Watch For
Avatar Mapping During Import
The improved FBX importer in 2.22 resolves historical pain points around bone hierarchy mismatches. When you drop an FBX into the asset browser, it now offers:
- Auto-detection of bone naming conventions (Humanoid, Mixamo, custom)
- Avatar assignment — tag the imported mesh with your project's avatar skeleton
- Bone rotation fix — compensates for Blender's Z-up versus Unigine's coordinate system (this caused head-bone 90-degree rotations in the old importer roughly every other import)
- Animation clip extraction — automatically splits multi-take FBX files into individual clips
The coordinate system fix alone eliminates what used to be a 2-hour debugging session per character. In pre-2.22 Unigine, you'd import a Blender humanoid and spend the afternoon wondering why the character's arms pointed backward. Now the importer applies the rotation compensation at import time.
Common Pitfall: Scale Mismatch
The one thing the new importer does not fix automatically is unit scale. Blender defaults to meters; Unigine defaults to meters; but Maya and 3ds Max default to centimeters. If your character imports at 100x the intended scale, check the FBX unit settings in the import dialog. Set it to match your DCC tool before importing. This is still a manual step, and it's still the most common import error in the 2.22 release.
Scale mismatch also affects animation clip playback. An animation baked at 1cm-per-unit scale will produce root motion that's 100x too large. The animation plays, but the character teleports across the scene in a single frame. If you see this behavior, verify that the root motion scale in the clip's properties matches the mesh scale.
Migration Guide: Converting Old Animation Code
If you have an existing Unigine project with script-driven animation, migrating to the AnimationGraph system is incremental — you don't need to rewrite everything at once.
Step 1: Audit Existing Animation Layers
Count how many AnimLayer instances your project creates at runtime. Most Unigine projects have between 4 and 12 layers per character. Each one maps to a state in the new graph or a layer in the layer stack.
Step 2: Map Layers to States
Create the AnimationGraph asset and add states matching your layer names. For layers that blend additively, set them as additive layers in the graph rather than creating non-additive blended states.
Step 3: Preserve Code-Driven Parameters
Your existing code already computes blend weights, speed values, and trigger conditions. Refactor these to push parameter values to the graph instead of directly setting layer weights. The migration looks like this:
// BEFORE — direct layer manipulation
void updateMovement(float speed, float angle) {
locomotion_weight = clamp(speed / max_speed, 0.0, 1.0);
idle_layer.setWeight(1.0 - locomotion_weight);
locomotion_layer.setWeight(locomotion_weight);
blend_parameter.setFloat(angle);
}
// AFTER — parameter-driven state machine
void updateMovement(float speed, float angle) {
anim_graph.SetFloat("speed", speed);
anim_graph.SetFloat("direction", angle);
}
Step 4: Remove Redundant Script Logic
Once the state machine handles transitions and blending, you can delete the manual interpolation, easing, and weight-clamping code. In converted projects so far, animation-related scripts shrink by 60-75% by line count. The logic doesn't disappear — it moves into the graph asset, which is versioned and editable in the visual editor.
Performance Considerations
Animation graph evaluation isn't free. Here are the actual overhead numbers:
| Scenario | Old Script Approach | State Machine (2.22) |
|---|---|---|
| 1 character, basic locomotion | ~0.02ms | ~0.03ms |
| 1 character, 3 layers + IK | ~0.06ms | ~0.04ms |
| 50 characters, mixed animations | ~3.2ms | ~1.8ms |
| 200 characters, LOD-gated | ~8.5ms | ~4.1ms |
The state machine system is heavier per-character at the basic level because of the graph evaluation overhead. But it gains efficiency with layered characters because the blending is handled in the engine rather than through multiple script-driven set-weight calls per frame. At 50+ characters, the state machine system is roughly 40-50% faster.
LOD animation gating is your primary optimization lever. Unigine 2.22 supports per-LOD animation update rates. Characters beyond 30 meters can update every 3rd frame; characters beyond 80 meters can update every 8th frame. Set this in the AnimationGraph's LOD settings:
LOD_0: 0-15m -> full frame rate
LOD_1: 15-30m -> every 2nd frame
LOD_2: 30-80m -> every 4th frame
LOD_3: 80m+ -> every 8th frame, disable IK
This alone can reduce animation CPU cost by 60-70% in open-world scenes with many NPCs.
Best Practices for 2.22 Animation Migration
Define your avatar skeleton first, before creating any AnimationGraph assets. Migrating to a consistent avatar after building 20 graph assets means re-mapping every bone reference manually — avoidable if you plan up front.
Keep state machines flat when possible. Unigine's AnimationGraph supports nested sub-state machines, but deeply nested graphs (3+ levels) become difficult to debug visually. If your graph needs more than 20 states, break it into separate graphs for specific body parts or contexts (combat vs. exploration).
Use triggers for one-shot events, not bools. A
hit_receivedtrigger fires once and resets automatically. Ais_hitbool stays true until you explicitly set it to false, which frequently causes animations to loop indefinitely when developers forget the reset.Profile with the Animation Profiler panel before and after migration. Unigine 2.22's profiler now shows per-state CPU time, blend evaluation cost, and bone transform counts per character. Use it to identify expensive states — aim IK running on characters that aren't aiming is the most common waste.
Test animation state transitions at extreme framerates. At 12-15fps (common on low-spec hardware or when the GPU is saturated), rapid state transitions can produce evaluation gaps visible as single-frame T-poses. Use a frame limiter to test transitions under load before shipping.
What This Means for Unigine's Competitive Position
Unigine 2.22's animation overhaul doesn't make the engine competitive with Unreal's Control Rig or Unity's Animation Rigging package overnight. Those tools have years of iteration and massive community assets behind them. But what 2.22 does is eliminate the primary reason teams rejected Unigine during technical evaluation.
For teams already committed to Unigine — especially in simulation, architecture, and industrial visualization where the renderer's draw-call efficiency and large-scene performance are essential — this release removes the last major workflow gap. The visual state machine editor alone reduces animation setup time for a new character project from 2-3 days to 4-6 hours, based on early migration reports from studios running the 2.22 beta.
If your Unigine project's animation code has more than 500 lines of blend logic, the 2.22 state machine tools will simplify it substantially. Start with your main player character's idle-locomotion-jump cycle as a proof-of-concept migration, then propagate the pattern to your NPC and secondary character animations.
Next Steps
Download Unigine 2.22 from the official Unigine SDK page and open the AnimationGraph tutorial project included in the SDK samples. The sample scene demonstrates a 3-layer humanoid with locomotion blending, aim IK, and motion warping out of the box. Replicate this setup with your own character assets before building a custom graph from scratch — it's the fastest way to understand the evaluation pipeline and spot where your existing animation code maps to the new system.
For teams evaluating Unigine as a backend target alongside other engines, you can connect Unigine's scripting layer to external services the same way you would with any C++/C# game framework. If your project needs player authentication, saved game data, or leaderboards that persist across sessions, tools like horizOn offer ready-made APIs that plug into Unigine's scripting runtime through simple HTTP calls — keeping your animation pipeline clean and your player data managed separately from your engine logic.
Source: Unigine 2.22 Released