Anim instances in C++
The Animation Blueprint's event graph moves into C++. C++ works out a few numbers each frame (speed, "is accelerating", "been idle long enough to relax"), and the anim graph in the Blueprint just reads them to drive the blend space and state machine. Doing that gathering in the thread-safe update lets it run off the game thread.
The class ladder
| Class | Adds | Why a separate level |
|---|---|---|
UWarriorBaseAnimInstance | Nothing yet | Common parent for every anim class, including linked layers. |
UWarriorCharacterAnimInstance | Owning character + movement component, GroundSpeed, bHasAcceleration | Heroes and enemies both need locomotion data. |
UWarriorHeroAnimInstance | Idle timer → bShouldEnterRelaxState | Only the hero has a "relax" idle. |
UWarriorHeroLinkedAnimLayer | Nothing yet | Parent for linked anim layers, e.g. separate unarmed and axe locomotion swapped in at runtime. |
Linked anim layers: the main anim BP calls a "layer" (e.g. Locomotion) defined by an Animation Layer
Interface. Different anim classes implement it, one for unarmed and one for the axe, and you swap them with
LinkAnimClassLayers when the weapon changes. The main graph never changes. (This is Lyra's approach too.)
The three functions
| Function | When | Thread | Your use |
|---|---|---|---|
NativeInitializeAnimation | Once, when the instance is created | Game | Cache OwningCharacter and its movement component. |
NativeUpdateAnimation | Every frame, first | Game | Hero: idle timer. |
NativeThreadSafeUpdateAnimation | Every frame, after the above | Worker thread (when multithreaded anim update is on) | Ground speed, acceleration. |
void UWarriorCharacterAnimInstance::NativeInitializeAnimation() {
OwningCharacter = Cast<AWarriorBaseCharacter>(TryGetPawnOwner());
if (OwningCharacter) OwningMovementComponent = OwningCharacter->GetCharacterMovement();
}
void UWarriorCharacterAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds) {
if (!OwningCharacter || !OwningMovementComponent) return;
GroundSpeed = OwningCharacter->GetVelocity().Size();
bHasAcceleration = OwningMovementComponent->GetCurrentAcceleration().SizeSquared2D() > 0.f;
}
- Why null-check every frame? Anim instances also run in the editor preview (the anim BP editor, the
Persona viewport), where there's no pawn.
TryGetPawnOwner()returns null there. Without the check, just opening the asset would crash the editor. SizeSquared2D() > 0: squared length skips a square root. You only need "is it non-zero", and squared is zero exactly when length is. 2D ignores falling.- Acceleration vs velocity: acceleration is "player is pushing the stick". Velocity is "still sliding". Using acceleration to leave idle means the character responds to input straight away, not after he's started moving.
UPROPERTY()on the cached pointers means the GC knows about them. See Garbage collection.
The relax state
if (bHasAcceleration) { IdleElapsedTime = 0.f; bShouldEnterRelaxState = false; }
else { IdleElapsedTime += DeltaSeconds; bShouldEnterRelaxState = (IdleElapsedTime >= RelaxThreshold); }
A timer built from DeltaSeconds: accumulate while idle, reset on input. RelaxThreshold is
EditDefaultsOnly, so the anim BP can tune it (5 s by default). IdleElapsedTime has no
UPROPERTY: it's a plain float, so the GC doesn't care, and nobody needs to see it in the editor.
1. Frame order. The hero reads bHasAcceleration in NativeUpdateAnimation, but the
parent writes it in NativeThreadSafeUpdateAnimation, which runs later in the frame. So the hero
always sees last frame's value. For a 5-second relax timer nobody will ever notice. It's the same kind of bug as
the Niagara drift question (Tick and frame order), so spotting it
shows you understand frame order.
2. GetVelocity().Size() includes falling speed. Mid-jump, "ground speed" climbs as you
fall. Size2D() is the usual choice for a locomotion blend space.
3. Reading the character from a worker thread is what the course does and works fine here. Epic's stricter advice is to copy what you need on the game thread (or use Property Access in the anim graph) and have the thread-safe function only read your own data. Know that this is the "proper" answer if asked.
Quiz yourself
Why does NativeThreadSafeUpdateAnimation exist? What's the benefit?
It can run on a worker thread in parallel, taking animation work off the game thread. With many animated characters, that's real frame time saved.
Why can TryGetPawnOwner() return null, and when?
In the editor's animation preview (no pawn exists), or before the instance is attached to a pawn. That's why every update null-checks first.
Why use acceleration, not velocity, to decide "the player is moving"?
Acceleration reflects input straight away. Velocity lags (he's still sliding after release, and still near zero on the first frame of input).
Spot the frame-order issue in the hero's relax logic.
NativeUpdateAnimation (hero) reads bHasAcceleration before NativeThreadSafeUpdateAnimation (parent) writes it this frame. It's always one frame stale. Harmless here, but the same kind of bug as a follower lagging one frame behind.
What's the point of UWarriorCharacterAnimInstance being separate from the hero one?
Enemies need the same locomotion data (speed, acceleration) but not the hero-only relax state. Shared code sits in the shared parent.
What are linked anim layers for in this project?
Swapping whole chunks of animation (e.g. unarmed vs axe locomotion) at runtime via LinkAnimClassLayers, without changing the main anim graph.
Why is SizeSquared2D() > 0 used instead of Size2D() > 0?
It skips a square root. For a zero check the answer is the same.