Loading...
Loading...
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.
npx skill4agent add quodsoler/unreal-engine-skills ue-animation-system.agents/ue-project-context.mdACharacterUSkeletalMeshComponentACharacter / AActor
└── USkeletalMeshComponent
└── UAnimInstance subclass
├── NativeInitializeAnimation() [setup, game thread]
├── NativeUpdateAnimation(float dt) [game thread]
├── NativeThreadSafeUpdateAnimation(dt) [worker thread]
├── FAnimInstanceProxy [worker thread eval]
└── Montage API / Linked LayersNativeUpdateAnimationUPROPERTY() TransientNativeUpdateAnimationNativeThreadSafeUpdateAnimation// 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;
};// 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.
}NativeThreadSafeUpdateAnimationvoid UMyAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
FMyAnimInstanceProxy& Proxy = GetProxyOnAnyThread<FMyAnimInstanceProxy>();
Proxy.Speed = Proxy.Velocity.Size();
Proxy.bIsFalling = Proxy.MovementMode == EMovementMode::MOVE_Falling;
}// 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); }AnimMontage.hAnimInstance.hUAnimInstancefloat 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;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) { }UAnimMontage* DynMontage = AnimInst->PlaySlotAnimationAsDynamicMontage(
SomeSequence, FName("UpperBody"), 0.25f, 0.25f, 1.f, 1);UAbilitySystemComponent::PlayMontage()FGameplayAbilityRepAnimMontageMontage_PlayOnRep_Montage_Play// 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 taskUPROPERTYUBlendSpace1DFInterpolationParameterInterpolationType=SpringDamperInterpolationTime=0.15DampingRatio=1.0UBlendSpaceUAimOffsetBlendSpacereferences/locomotion-setup.md// In NativeInitializeAnimation()
AddNativeTransitionBinding(
FName("LocomotionSM"), FName("Idle"), FName("Walk/Run"),
FCanTakeTransition::CreateUObject(this, &UMyAnimInstance::CanStartMoving),
FName("IdleToMoving"));
AddNativeStateEntryBinding(
FName("LocomotionSM"), FName("Land"),
FOnGraphStateChanged::CreateUObject(this, &UMyAnimInstance::OnLandEntered));const FAnimNode_StateMachine* SM =
GetStateMachineInstanceFromName(FName("LocomotionSM"));
float RunWeight = GetInstanceStateWeight(
GetStateMachineIndex(FName("LocomotionSM")), SM->GetCurrentState());CanEnterTransitionAnimNotify.hAnimNotifyState.hUCLASS(meta=(DisplayName="Footstep"))
class MYGAME_API UFootstepNotify : public UAnimNotify
{
GENERATED_BODY()
public:
// Always override the UE5 3-argument signature
virtual void Notify(USkeletalMeshComponent* MeshComp,
UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Footstep")
FName FootSocket = FName("foot_l");
};UCLASS(meta=(DisplayName="Weapon Collision Window"))
class MYGAME_API UWeaponCollisionState : public UAnimNotifyState
{
GENERATED_BODY()
public:
virtual void NotifyBegin(USkeletalMeshComponent*, UAnimSequenceBase*,
float TotalDuration, const FAnimNotifyEventReference&) override;
virtual void NotifyEnd(USkeletalMeshComponent*, UAnimSequenceBase*,
const FAnimNotifyEventReference&) override;
};bIsNativeBranchingPoint = trueBranchingPointNotify()Notify()Montage_AdvanceAnimInst->OnPlayMontageNotifyBegin.AddDynamic(
this, &UMyComponent::HandleNotifyBegin);
void UMyComponent::HandleNotifyBegin(FName NotifyName,
const FBranchingPointNotifyPayload& Payload)
{
if (NotifyName == FName("EnableHitbox")) ActivateHitDetection();
}references/anim-notify-reference.mdFVector UMyAnimInstance::GetFootTarget(FName SocketName) const
{
const FVector Foot = GetOwningComponent()->GetSocketLocation(SocketName);
FHitResult Hit;
FCollisionQueryParams P(SCENE_QUERY_STAT(FootIK), true);
P.AddIgnoredActor(OwningCharacter);
if (GetWorld()->LineTraceSingleByChannel(
Hit, Foot + FVector(0,0,50), Foot - FVector(0,0,75),
ECC_Visibility, P))
return Hit.ImpactPoint;
return Foot;
}| Node | Purpose |
|---|---|
| Two Bone IK | Two-joint IK (arm, leg) — effector + joint target |
| FABRIK | Multi-bone chain IK — tip bone + effector |
| Look At | Single bone tracks target location with clamp |
| Copy Bone | Copies transform components between bones |
| Spline IK | Bones follow a spline curve (spine, tail) |
UpperBodyspine_01UpperBody// In NativeUpdateAnimation:
const FRotator Delta = UKismetMathLibrary::NormalizedDeltaRotator(
OwningCharacter->GetBaseAimRotation(),
OwningCharacter->GetActorRotation());
AimYaw = FMath::Clamp(Delta.Yaw, -90.f, 90.f);
AimPitch = FMath::Clamp(Delta.Pitch, -90.f, 90.f);AimYawAimPitchAnimNode_LinkedAnimGraph.hAnimNode_LinkedAnimLayer.hUAnimLayerInterface// Switch locomotion implementation
AnimInst->LinkAnimClassLayers(UClimbingLocomotionLayer::StaticClass());
// Reset all layers to defaults:
AnimInst->LinkAnimClassLayers(nullptr);
// Retrieve a linked instance:
UAnimInstance* Layer =
AnimInst->GetLinkedAnimLayerInstanceByClass(UClimbingLocomotionLayer::StaticClass());AnimInst->LinkAnimGraphByTag(FName("CombatGraph"), UMyCombatAnimInstance::StaticClass());
UAnimInstance* Sub = AnimInst->GetLinkedAnimGraphInstanceByTag(FName("CombatGraph"));AnimInst->SetReceiveNotifiesFromLinkedInstances(true);
AnimInst->SetPropagateNotifiesToLinkedInstances(true);
// UE5.2+: let linked layers share main instance montage evaluation:
// AnimInst->SetUseMainInstanceMontageEvaluationData(true);// Options: NoRootMotionExtraction, IgnoreRootMotion,
// RootMotionFromMontagesOnly, RootMotionFromEverything
RootMotionMode = ERootMotionMode::RootMotionFromMontagesOnly;
// Per-montage disable
FAnimMontageInstance* Inst = AnimInst->GetActiveInstanceForMontage(Montage);
if (Inst) { Inst->PushDisableRootMotion(); /* ... */ Inst->PopDisableRootMotion(); }ENetworkSmoothingMode::ExponentialbAllowPhysicsRotationDuringAnimRootMotionFRootMotionMovementParams| Anti-Pattern | Fix |
|---|---|
Reading gameplay state in | Cache values as |
Polling | Bind |
| Two montages sharing the same slot group | Use distinct slot names ( |
Skipping | Always call super — it initializes the proxy and skeleton |
| Calling non-thread-safe UObject methods in thread-safe update | Only read primitive |
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "AnimGraphRuntime"
});
// Optional:
PrivateDependencyModuleNames.Add("ControlRig"); // Control Rig IK
PrivateDependencyModuleNames.Add("GameplayAbilities"); // GAS montage tasksue-gameplay-abilitiesPlayMontageAndWaitue-actor-component-architectureue-cpp-foundationsUPROPERTYTWeakObjectPtrreferences/anim-notify-reference.mdreferences/locomotion-setup.md