Claude
Skills
Sign in
Back

ue-animation-system

Included with Lifetime
$97 forever

Use this skill when working with Unreal Engine animation: AnimInstance, montage playback, blend space, state machine, anim notify, IK, AnimGraph, skeletal mesh, or linked anim graphs. See references/anim-notify-reference.md for notify patterns and references/locomotion-setup.md for locomotion setup. For montage integration with GAS, see ue-gameplay-abilities.

General

What this skill does


# UE Animation System

You are an expert in Unreal Engine's animation system.

## Context Check

Read `.agents/ue-project-context.md` first. Note which plugins are enabled
(Control Rig, Motion Matching, Full Body IK), whether GAS is in use, and the
skeleton/character hierarchy.

## Information to Gather

1. What animation need? (locomotion, ability, cinematic, IK, procedural)
2. C++ only, Blueprint only, or mixed?
3. Does the character use `ACharacter` with an existing `USkeletalMeshComponent`?
4. Is GAS active? (affects montage replication)
5. Multiplayer? (determines replication strategy)
6. Does the project use modular linked anim layers?

---

## Architecture

```
ACharacter / AActor
  └── USkeletalMeshComponent
        └── UAnimInstance subclass
              ├── NativeInitializeAnimation()           [setup, game thread]
              ├── NativeUpdateAnimation(float dt)       [game thread]
              ├── NativeThreadSafeUpdateAnimation(dt)   [worker thread]
              ├── FAnimInstanceProxy                    [worker thread eval]
              └── Montage API / Linked Layers
```

Animation updates run in two phases. Game thread: `NativeUpdateAnimation` —
safe to read gameplay state. Worker thread: blend tree evaluation. Write all
shared state as `UPROPERTY() Transient` members in `NativeUpdateAnimation`;
read those cached values in `NativeThreadSafeUpdateAnimation`.

---

## AnimInstance

### Subclass Pattern

```cpp
// MyAnimInstance.h
UCLASS()
class MYGAME_API UMyAnimInstance : public UAnimInstance
{
    GENERATED_BODY()

    virtual void NativeInitializeAnimation() override;
    virtual void NativeUpdateAnimation(float DeltaSeconds) override;
    virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override;

protected:
    UPROPERTY(Transient) TObjectPtr<ACharacter> OwningCharacter;
    UPROPERTY(Transient) TObjectPtr<UCharacterMovementComponent> MovementComp;

    UPROPERTY(Transient, BlueprintReadOnly, Category="Locomotion")
    float Speed = 0.f;

    UPROPERTY(Transient, BlueprintReadOnly, Category="Locomotion")
    float Direction = 0.f;

    UPROPERTY(Transient, BlueprintReadOnly, Category="Locomotion")
    bool bIsInAir = false;
};
```

```cpp
// MyAnimInstance.cpp
void UMyAnimInstance::NativeInitializeAnimation()
{
    Super::NativeInitializeAnimation(); // ALWAYS call super
    OwningCharacter = Cast<ACharacter>(TryGetPawnOwner());
    if (OwningCharacter)
        MovementComp = OwningCharacter->GetCharacterMovement();
}

void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
    Super::NativeUpdateAnimation(DeltaSeconds);
    if (!OwningCharacter || !MovementComp) return;

    const FVector Velocity = MovementComp->Velocity;
    Speed    = Velocity.Size2D();
    bIsInAir = MovementComp->IsFalling();

    if (Speed > 3.f)
    {
        const FRotator ActorRot    = OwningCharacter->GetActorRotation();
        const FRotator VelocityRot = Velocity.ToOrientationRotator();
        Direction = UKismetMathLibrary::NormalizedDeltaRotator(
            VelocityRot, ActorRot).Yaw;
    }
}

void UMyAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
    Super::NativeThreadSafeUpdateAnimation(DeltaSeconds);
    // Only read UPROPERTY members written in NativeUpdateAnimation above.
    // Do NOT call any UObject functions not marked BlueprintThreadSafe.
}
```

### FAnimInstanceProxy — Thread-Safe Access

Heavy animation logic can run on worker threads via
`NativeThreadSafeUpdateAnimation`. Access shared data through the proxy:

```cpp
void UMyAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
    FMyAnimInstanceProxy& Proxy = GetProxyOnAnyThread<FMyAnimInstanceProxy>();
    Proxy.Speed = Proxy.Velocity.Size();
    Proxy.bIsFalling = Proxy.MovementMode == EMovementMode::MOVE_Falling;
}
```

```cpp
// FAnimInstanceProxy declaration — worker thread data container
USTRUCT()
struct FMyAnimInstanceProxy : public FAnimInstanceProxy
{
    GENERATED_BODY()
    FMyAnimInstanceProxy() = default;
    explicit FMyAnimInstanceProxy(UAnimInstance* Instance) : FAnimInstanceProxy(Instance) {}

    float Speed = 0.f;
    FVector Velocity = FVector::ZeroVector;
    TEnumAsByte<EMovementMode> MovementMode = MOVE_None;

    virtual void PreUpdate(UAnimInstance* AnimInstance, float DeltaSeconds) override;
    virtual void Update(float DeltaSeconds) override;
};

// In UMyAnimInstance: override CreateAnimInstanceProxy to return your proxy
virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override
{ return new FMyAnimInstanceProxy(this); }
```

The engine copies data between game thread and worker thread at safe sync points.

---

## Montages

Source: `AnimMontage.h`, `AnimInstance.h`

Key API (`UAnimInstance`):
```cpp
float Montage_Play(UAnimMontage*, float PlayRate=1.f,
    EMontagePlayReturnType=MontageLength, float StartAt=0.f, bool bStopAll=true);
void  Montage_Stop(float BlendOut, const UAnimMontage* Montage=nullptr);
void  Montage_Pause(const UAnimMontage* Montage=nullptr);
void  Montage_Resume(const UAnimMontage* Montage);
void  Montage_JumpToSection(FName Section, const UAnimMontage* Montage=nullptr);
void  Montage_SetNextSection(FName From, FName To, const UAnimMontage* Montage=nullptr);
bool  Montage_IsActive(const UAnimMontage*) const;
bool  Montage_IsPlaying(const UAnimMontage*) const;
FName Montage_GetCurrentSection(const UAnimMontage* Montage=nullptr) const;
float Montage_GetPosition(const UAnimMontage*) const;
```

### Playing + Delegate Pattern

```cpp
void UMyComponent::PlayAttackMontage(UAnimMontage* Montage)
{
    UAnimInstance* AnimInst = GetMesh()->GetAnimInstance();
    if (!AnimInst || !Montage) return;

    // Play FIRST — Montage_SetEndDelegate calls GetActiveInstanceForMontage()
    // internally, which returns nullptr until Montage_Play creates the instance.
    if (AnimInst->Montage_Play(Montage) <= 0.f) return;

    FOnMontageEnded EndDelegate;
    EndDelegate.BindUObject(this, &UMyComponent::OnAttackEnded);
    AnimInst->Montage_SetEndDelegate(EndDelegate, Montage);

    FOnMontageBlendingOutStarted BlendOutDelegate;
    BlendOutDelegate.BindUObject(this, &UMyComponent::OnAttackBlendingOut);
    AnimInst->Montage_SetBlendingOutDelegate(BlendOutDelegate, Montage);
}

void UMyComponent::OnAttackEnded(UAnimMontage* Montage, bool bInterrupted) { }
void UMyComponent::OnAttackBlendingOut(UAnimMontage* Montage, bool bInterrupted) { }
```

### Dynamic Slot Montage

```cpp
UAnimMontage* DynMontage = AnimInst->PlaySlotAnimationAsDynamicMontage(
    SomeSequence, FName("UpperBody"), 0.25f, 0.25f, 1.f, 1);
```

### Multiplayer Replication

- **With GAS**: use `UAbilitySystemComponent::PlayMontage()` — GAS handles
  replication via `FGameplayAbilityRepAnimMontage`.
- **Without GAS**: replicate a montage pointer or a custom rep struct; server
  calls `Montage_Play`, clients play on `OnRep_`.
- Never call `Montage_Play` independently on all net roles without sync.

### GAS Integration — PlayMontageAndWait

```cpp
// GAS ability task — PlayMontageAndWait (requires GameplayAbilities module)
UAbilityTask_PlayMontageAndWait* Task =
    UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
        this, NAME_None, AttackMontage, 1.f);
Task->OnCompleted.AddDynamic(this, &UMyAbility::OnMontageCompleted);
Task->OnInterrupted.AddDynamic(this, &UMyAbility::OnMontageInterrupted);
Task->ReadyForActivation();  // must call to start the task
```

---

## Blend Spaces

Blend spaces are data assets sampled in the AnimGraph. Drive them by setting
`UPROPERTY` members on the AnimInstance that the AnimGraph reads.

- **1D** (`UBlendSpace1D`): single axis, typically Speed (0–600).
  Use `FInterpolationParameter` with `InterpolationType=SpringDamper`,
  `InterpolationTime=0.15`, `DampingRatio=1.0`.
- **2D** (`UBlendSpace`): two axes — Direction (-180 to 180) and Speed (0–600).
  Cardinal direction samples at each speed tier.
- **Aim Offset** (`UAimOffsetBlendSpace`): additive blend space for Yaw/P

Related in General