UE: The Key to Runtime Animation Recording—Obtaining Correct Bone Data and Saving It
The Core Problems
When implementing runtime animation recording in UE5.4, the two most critical questions are:
- How to obtain correct bone data - Avoid crashes and data mismatches
- How to save an AnimSequence correctly - Use the engine’s standard APIs
This article goes straight to the essentials and provides the most concise, effective solution.
1. Obtaining Correct Bone Data
Core Principle: Use Three Key APIs Correctly
// ✅ 正确:使用 SkeletalMesh 的 RefSkeleton
USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();
// ✅ 正确:使用 GetRawBoneNum() 获取实际骨骼数
const int32 NumBones = RefSkeleton.GetRawBoneNum();
// ✅ 正确:运行时变换数据
const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();
Incorrect Examples and Their Consequences
| Incorrect code | Problem | Consequence |
|---|---|---|
Skeleton->GetReferenceSkeleton() | Includes ControlRig virtual bones | Crash: FKControlRig.cpp:126 |
RefSkeleton.GetNum() | Returns all bones (including virtual bones) | Array out of bounds |
GetBoneNames() followed by iterating RefSkeleton | Bone names added twice | Inconsistent data |
Comparison of Key APIs
| Data source | Method | Example return value | Correct? |
|---|---|---|---|
Skeleton->GetReferenceSkeleton().GetNum() | All bones | 161 (89 actual + 72 virtual) | ❌ |
SkeletalMesh->GetRefSkeleton().GetNum() | All bones | 161 | ❌ |
SkeletalMesh->GetRefSkeleton().GetRawBoneNum() | Actual bones | 89 | ✅ |
SkeletalMeshComponent->GetBoneSpaceTransforms().Num() | Runtime transforms | 89 | ✅ For comparison and validation |
Evidence from the Engine Source
AnimSequence.cpp, lines 2674–2678 (official implementation):
const int32 NumBones = RefSkeleton.GetRawBoneNum(); // ← 引擎使用 GetRawBoneNum()
const TArray<FTransform> BoneSpaceTransforms = MeshComponent->GetBoneSpaceTransforms();
check(BoneSpaceTransforms.Num() >= NumBones); // ← 引擎期望这个条件成立
Complete Initialization Code
void ATestActor::StartRecording(int _FrameSize, ACharacter* _TargetActor)
{
TargetActor = _TargetActor;
SkeletalMeshComponent = TargetActor->GetMesh();
// 1. 获取正确的 RefSkeleton(SkeletalMesh 的,不是 Skeleton 的)
USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();
// 2. 使用 GetRawBoneNum() 获取实际骨骼数
const int32 NumBones = RefSkeleton.GetRawBoneNum();
// 3. 获取运行时变换数据(用于验证)
const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();
// 4. 安全性检查(引擎期望这个条件)
if (BoneSpaceTransforms.Num() < NumBones)
{
UE_LOG(LogTemp, Error, TEXT("骨骼数据不匹配:BoneSpaceTransforms(%d) < RawBoneNum(%d)"),
BoneSpaceTransforms.Num(), NumBones);
return;
}
// 5. 填充骨骼名称(单次遍历)
BoneNames.Empty();
for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
BoneNames.Add(RefSkeleton.GetBoneName(BoneIndex));
}
// 6. 初始化数据数组
TrackDataArray.SetNum(NumBones);
IsRecording = true;
}
Explanation of the Key Points
Why Must We Use the SkeletalMesh’s RefSkeleton?
// ❌ 错误:Skeleton 的 RefSkeleton 包含 ControlRig 虚拟骨骼
const FReferenceSkeleton& RefSkeleton =
SkeletalMesh->GetSkeleton()->GetReferenceSkeleton(); // 161个骨骼
// ✅ 正确:SkeletalMesh 的 RefSkeleton 是实际骨骼层级
const FReferenceSkeleton& RefSkeleton =
SkeletalMesh->GetRefSkeleton(); // 配合 GetRawBoneNum() 使用
Examples of virtual bones in a Skeleton:
root_CONTROL- ControlRig root controllerik_hand_l_CONTROL- IK hand controller- These bones do not exist in
BoneSpaceTransforms!
Why Must We Use GetRawBoneNum()?
// GetNum() vs GetRawBoneNum() 的区别
RefSkeleton.GetNum(); // 返回全部:RawBones + VirtualBones = 161
RefSkeleton.GetRawBoneNum(); // 返回实际:RawBones = 89
Validation method:
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetNum() = %d"), RefSkeleton.GetNum());
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetRawBoneNum() = %d"), RefSkeleton.GetRawBoneNum());
UE_LOG(LogTemp, Warning, TEXT("BoneSpaceTransforms.Num() = %d"), BoneSpaceTransforms.Num());
// 正确结果应该是:
// GetNum() = 161
// GetRawBoneNum() = 89
// BoneSpaceTransforms.Num() = 89
2. Saving an AnimSequence Correctly
Core Principle: Use IAnimationDataController
UE5 deprecated modifying AnimSequence directly. You must use the IAnimationDataController interface.
Standard Saving Procedure (5 Key Steps)
void ATestActor::SpawnAnimAsset()
{
// === 1. 创建资产对象 ===
UPackage* Package = CreatePackage(*AssetPath);
UAnimSequence* AnimSeq = NewObject<UAnimSequence>(Package, UAnimSequence::StaticClass(),
*AssetName, RF_Public | RF_Standalone | RF_MarkAsRootSet);
// 绑定 Skeleton
USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
AnimSeq->SetSkeleton(SkeletalMesh->GetSkeleton());
// === 2. 获取 Controller(唯一修改接口)===
IAnimationDataModel* Model = AnimSeq->GetDataModel();
TScriptInterface<IAnimationDataController> Controller = Model->GetController();
// === 3. 初始化模型(使用 FScopedBracket)===
Controller->InitializeModel();
IAnimationDataController::FScopedBracket ScopedBracket(Controller,
LOCTEXT("RecordAnimation", "Recording Animation"));
// === 4. 设置帧率和帧数 ===
// 计算实际帧率(关键:不能用固定值)
float RecordingDuration = GetWorld()->GetTimeSeconds() - RecordingStartTime;
int32 NumFrames = TrackDataArray[0].PosKeys.Num();
float ActualFrameRate = (float)NumFrames / RecordingDuration;
ActualFrameRate = FMath::Clamp(ActualFrameRate, 24.0f, 120.0f);
Controller->SetFrameRate(FFrameRate(FMath::RoundToInt(ActualFrameRate), 1));
Controller->SetNumberOfFrames(NumFrames);
// === 5. 添加骨骼轨道和关键帧 ===
for (int32 BoneIndex = 0; BoneIndex < BoneNames.Num(); ++BoneIndex)
{
const FName& BoneName = BoneNames[BoneIndex];
const FRawAnimSequenceTrack& Track = TrackDataArray[BoneIndex];
// 5.1 添加骨骼曲线
Controller->AddBoneCurve(BoneName);
// 5.2 准备 Scale 数据(引擎要求 Pos/Rot/Scale 数量一致)
TArray<FVector3f> ScaleKeys;
ScaleKeys.Init(FVector3f(1.0f, 1.0f, 1.0f), Track.PosKeys.Num());
// 5.3 设置关键帧
Controller->SetBoneTrackKeys(BoneName, Track.PosKeys, Track.RotKeys, ScaleKeys);
}
// 5.4 通知数据填充完成(必须调用)
Controller->NotifyPopulated();
// === 6. 启用根骨骼运动(可选)===
AnimSeq->bEnableRootMotion = true;
AnimSeq->RootMotionRootLock = ERootMotionRootLock::RefPose;
// === 7. 保存资产 ===
FAssetRegistryModule::AssetCreated(AnimSeq);
Package->MarkPackageDirty();
FString PackageFileName = FPackageName::LongPackageNameToFilename(
AssetPath, FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
bool bSaved = UPackage::SavePackage(Package, AnimSeq, *PackageFileName, SaveArgs);
}
API Comparison: UE4 vs. UE5
| Function | UE4 (deprecated) | UE5 (standard) |
|---|---|---|
| Add tracks | AddNewRawTrack() | Controller->AddBoneCurve() |
| Set keyframes | Modify RawAnimationData directly | Controller->SetBoneTrackKeys() |
| Scope management | OpenBracket() / CloseBracket() | FScopedBracket (RAII) |
| Completion notification | None | Controller->NotifyPopulated() |
Key Steps Explained
Step 3: Why Use FScopedBracket?
// ❌ UE4 旧方式(容易忘记 CloseBracket)
Controller->OpenBracket(LOCTEXT("AddTracks", "Adding Tracks"));
// ... 添加数据
Controller->CloseBracket(); // 如果中途 return 会泄漏
// ✅ UE5 标准方式(RAII 自动管理)
{
IAnimationDataController::FScopedBracket ScopedBracket(Controller, LOCTEXT(...));
// ... 添加数据
} // 离开作用域自动调用 CloseBracket
Step 4: Why Can’t We Use a Fixed Frame Rate?
// ❌ 错误:固定 30fps
Controller->SetFrameRate(FFrameRate(30, 1));
// 如果游戏运行在 60fps,录制的动画会以 2 倍速播放!
// ✅ 正确:动态计算
float ActualFrameRate = (float)NumFrames / RecordingDuration;
Controller->SetFrameRate(FFrameRate(FMath::RoundToInt(ActualFrameRate), 1));
Step 5.4: What NotifyPopulated() Does
Controller->NotifyPopulated(); // ← 必须调用!
Purpose:
- Notify the engine that data population is complete
- Trigger internal cache updates
- Validate data integrity
- The animation will not play if this is not called
3. Complete Code Example
Header File (TestActor.h)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"
UCLASS()
class TEST03_API ATestActor : public AActor
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void StartRecording(int _FrameSize, ACharacter* _TargetActor = nullptr);
UFUNCTION(BlueprintCallable)
void EndRecording();
private:
virtual void Tick(float DeltaTime) override;
void SpawnAnimAsset();
UPROPERTY()
ACharacter* TargetActor;
UPROPERTY()
USkeletalMeshComponent* SkeletalMeshComponent;
TArray<FName> BoneNames;
TArray<FRawAnimSequenceTrack> TrackDataArray;
bool IsRecording = false;
float RecordingStartTime;
FTransform InitialActorTransform;
};
Recording Logic in Tick
void ATestActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (IsRecording)
{
const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();
// 计算相对位移(用于根骨骼运动)
FTransform CurrentActorTransform = TargetActor->GetActorTransform();
FTransform RelativeActorTransform = CurrentActorTransform.GetRelativeTransform(InitialActorTransform);
for (int i = 0; i < BoneNames.Num(); ++i)
{
FTransform FinalBoneTransform;
if (i == 0) // 根骨骼
{
// 烘焙世界位移到根骨骼
FinalBoneTransform = BoneSpaceTransforms[0] * RelativeActorTransform;
}
else
{
FinalBoneTransform = BoneSpaceTransforms[i];
}
TrackDataArray[i].PosKeys.Add(FVector3f(FinalBoneTransform.GetLocation()));
TrackDataArray[i].RotKeys.Add(FQuat4f(FinalBoneTransform.GetRotation()));
TrackDataArray[i].ScaleKeys.Add(FVector3f(FinalBoneTransform.GetScale3D()));
}
}
}
4. Summary of Core Technical Points
Obtaining Bone Data (3 Requirements)
| Step | API | Consequence of getting it wrong |
|---|---|---|
| 1. Data source | SkeletalMesh->GetRefSkeleton() | Using Skeleton includes virtual bones |
| 2. Bone count | RefSkeleton.GetRawBoneNum() | Using GetNum() causes an out-of-bounds access |
| 3. Validation | BoneSpaceTransforms.Num() >= NumBones | Failing to check causes crashes |
Saving an AnimSequence (5 Key Points)
| Step | API | Purpose |
|---|---|---|
| 1. Get the Controller | Model->GetController() | The only modification interface |
| 2. Scope management | FScopedBracket | Automatic RAII cleanup |
| 3. Add tracks | AddBoneCurve() | Standard UE5 API |
| 4. Set data | SetBoneTrackKeys() | Pos/Rot/Scale counts must match |
| 5. Completion notification | NotifyPopulated() | Must be called |
5. Debugging Checklist
Checks Before Recording
UE_LOG(LogTemp, Warning, TEXT("=== 骨骼数据诊断 ==="));
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetRawBoneNum() = %d"), RefSkeleton.GetRawBoneNum());
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetNum() = %d"), RefSkeleton.GetNum());
UE_LOG(LogTemp, Warning, TEXT("BoneSpaceTransforms.Num() = %d"), BoneSpaceTransforms.Num());
// 正确结果:RawBoneNum == BoneSpaceTransforms.Num()
Checks Before Saving
UE_LOG(LogTemp, Warning, TEXT("生成动画: 骨骼=%d, 帧数=%d, 帧率=%.2f fps"),
BoneNames.Num(), NumFrames, ActualFrameRate);
// 检查数据完整性
for (int32 i = 0; i < BoneNames.Num(); ++i)
{
if (TrackDataArray[i].PosKeys.Num() != NumFrames)
{
UE_LOG(LogTemp, Error, TEXT("骨骼 %s 数据不完整"), *BoneNames[i].ToString());
}
}
6. Engine Source Reference Locations
Key Functions in AnimSequence.cpp
| Function | Lines | Purpose |
|---|---|---|
CreateAnimation(SkeletalMeshComponent*) | 2659-2692 | Standard bone data retrieval |
CreateAnimation(SkeletalMesh*) | 2630-2657 | Uses the reference pose |
GetBonePose() | 1598-1715 | Data extraction during playback |
How to view it:
文件路径:Engine/Source/Runtime/Engine/Private/Animation/AnimSequence.cpp
关键行:const int32 NumBones = RefSkeleton.GetRawBoneNum(); // 第 2674 行
Conclusion
Runtime animation recording in UE5.4 comes down to:
- Obtaining correct bone data:
- Use
SkeletalMesh->GetRefSkeleton()rather than the Skeleton’s - Use
GetRawBoneNum()rather thanGetNum() - You must validate
BoneSpaceTransforms.Num() >= NumBones
- Use
- Saving the AnimSequence correctly:
- Operate through the
IAnimationDataControllerinterface - Use
FScopedBracketto manage the scope - Call
AddBoneCurve()rather thanAddBoneTrack() - You must call
NotifyPopulated()
- Operate through the
Following these principles lets you implement a stable, reliable animation recording system.
Engine version: Unreal Engine 5.4
Source reference: AnimSequence.cpp (lines 2630–2692)
Thanks for reading. Feel free to like, follow, or bookmark this article, and discuss any questions in the comments.
If this article helped you, click here to donate and support the author.