© mengzhishanghun · 原创文章
首发于 博客园 · 禁止未经授权转载
核心问题
在 UE5.4 中实现运行时动画录制,最关键的两个问题是:
-
如何获取正确的骨骼数据 - 避免崩溃和数据不匹配
-
如何正确保存 AnimSequence - 使用引擎标准 API
本文直击核心,提供最简洁有效的解决方案。
一、获取正确的骨骼数据
核心原则:三个关键 API 的正确使用
// ✅ 正确:使用 SkeletalMesh 的 RefSkeletonUSkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();// ✅ 正确:使用 GetRawBoneNum() 获取实际骨骼数const int32 NumBones = RefSkeleton.GetRawBoneNum();// ✅ 正确:运行时变换数据const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();
错误示范与后果
| 错误代码 | 问题 | 后果 |
|----------|------|------|
| Skeleton->GetReferenceSkeleton() | 包含 ControlRig 虚拟骨骼 | 崩溃:FKControlRig.cpp:126 |
| RefSkeleton.GetNum() | 返回全部骨骼(含虚拟) | 数组越界 |
| GetBoneNames() 后再遍历 RefSkeleton | 骨骼名重复添加 | 数据混乱 |
关键 API 对比表
| 数据源 | 方法 | 返回值示例 | 是否正确 |
|--------|------|------------|----------|
| Skeleton->GetReferenceSkeleton().GetNum() | 全部骨骼 | 161 (89实际+72虚拟) | ❌ |
| SkeletalMesh->GetRefSkeleton().GetNum() | 全部骨骼 | 161 | ❌ |
| SkeletalMesh->GetRefSkeleton().GetRawBoneNum() | 实际骨骼 | 89 | ✅ |
| SkeletalMeshComponent->GetBoneSpaceTransforms().Num() | 运行时变换 | 89 | ✅ 用于对比验证 |
引擎源码依据
AnimSequence.cpp 第 2674-2678 行(官方实现):
const int32 NumBones = RefSkeleton.GetRawBoneNum(); // ← 引擎使用 GetRawBoneNum()const TArray<FTransform> BoneSpaceTransforms = MeshComponent->GetBoneSpaceTransforms();check(BoneSpaceTransforms.Num() >= NumBones); // ← 引擎期望这个条件成立
完整初始化代码
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;}
关键点说明
为什么必须用 SkeletalMesh 的 RefSkeleton?
// ❌ 错误:Skeleton 的 RefSkeleton 包含 ControlRig 虚拟骨骼const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetSkeleton()->GetReferenceSkeleton(); // 161个骨骼// ✅ 正确:SkeletalMesh 的 RefSkeleton 是实际骨骼层级const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton(); // 配合 GetRawBoneNum() 使用
Skeleton 中的虚拟骨骼示例:
-
root_CONTROL- ControlRig 根控制器 -
ik_hand_l_CONTROL- IK 手部控制器 -
这些骨骼在
BoneSpaceTransforms中不存在!
为什么必须用 GetRawBoneNum()?
// GetNum() vs GetRawBoneNum() 的区别RefSkeleton.GetNum(); // 返回全部:RawBones + VirtualBones = 161RefSkeleton.GetRawBoneNum(); // 返回实际:RawBones = 89
验证方法:
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
二、正确保存 AnimSequence
核心原则:使用 IAnimationDataController
UE5 废弃了直接修改 AnimSequence 的方式,必须通过 IAnimationDataController 接口。
标准保存流程(5 个关键步骤)
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 对比:UE4 vs UE5
| 功能 | UE4(已弃用) | UE5(标准) |
|------|---------------|-------------|
| 添加轨道 | AddNewRawTrack() | Controller->AddBoneCurve() |
| 设置关键帧 | 直接修改 RawAnimationData | Controller->SetBoneTrackKeys() |
| 作用域管理 | OpenBracket() / CloseBracket() | FScopedBracket (RAII) |
| 完成通知 | 无 | Controller->NotifyPopulated() |
关键步骤详解
步骤 3:为什么使用 FScopedBracket?
// ❌ UE4 旧方式(容易忘记 CloseBracket)Controller->OpenBracket(LOCTEXT("AddTracks", "Adding Tracks"));// ... 添加数据Controller->CloseBracket(); // 如果中途 return 会泄漏// ✅ UE5 标准方式(RAII 自动管理){ IAnimationDataController::FScopedBracket ScopedBracket(Controller, LOCTEXT(...)); // ... 添加数据} // 离开作用域自动调用 CloseBracket
步骤 4:为什么不能用固定帧率?
// ❌ 错误:固定 30fpsController->SetFrameRate(FFrameRate(30, 1));// 如果游戏运行在 60fps,录制的动画会以 2 倍速播放!// ✅ 正确:动态计算float ActualFrameRate = (float)NumFrames / RecordingDuration;Controller->SetFrameRate(FFrameRate(FMath::RoundToInt(ActualFrameRate), 1));
步骤 5.4:NotifyPopulated() 的作用
Controller->NotifyPopulated(); // ← 必须调用!
作用:
-
通知引擎数据填充完成
-
触发内部缓存更新
-
验证数据完整性
-
不调用会导致动画无法播放
三、完整代码示例
头文件 (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;};
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())); } }}
四、核心技术点总结
骨骼数据获取(3 个必须)
| 步骤 | API | 错误会导致 |
|------|-----|------------|
| 1. 数据源 | SkeletalMesh->GetRefSkeleton() | 使用 Skeleton 会包含虚拟骨骼 |
| 2. 骨骼数 | RefSkeleton.GetRawBoneNum() | 使用 GetNum() 会越界 |
| 3. 验证 | BoneSpaceTransforms.Num() >= NumBones | 不检查会崩溃 |
AnimSequence 保存(5 个关键)
| 步骤 | API | 作用 |
|------|-----|------|
| 1. 获取 Controller | Model->GetController() | 唯一修改接口 |
| 2. 作用域管理 | FScopedBracket | RAII 自动清理 |
| 3. 添加轨道 | AddBoneCurve() | UE5 标准 API |
| 4. 设置数据 | SetBoneTrackKeys() | Pos/Rot/Scale 数量必须一致 |
| 5. 完成通知 | NotifyPopulated() | 必须调用 |
五、调试检查清单
录制前检查
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()
保存前检查
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()); }}
六、引擎源码参考位置
AnimSequence.cpp 关键函数
| 函数 | 行号 | 作用 |
|------|------|------|
| CreateAnimation(SkeletalMeshComponent*) | 2659-2692 | 标准骨骼数据获取 |
| CreateAnimation(SkeletalMesh*) | 2630-2657 | 使用参考姿态 |
| GetBonePose() | 1598-1715 | 播放时数据提取 |
查看方法:
文件路径:Engine/Source/Runtime/Engine/Private/Animation/AnimSequence.cpp关键行:const int32 NumBones = RefSkeleton.GetRawBoneNum(); // 第 2674 行
结论
UE5.4 运行时动画录制的核心在于:
- 正确获取骨骼数据:
- 使用 SkeletalMesh->GetRefSkeleton() 而非 Skeleton 的
- 使用 GetRawBoneNum() 而非 GetNum()
- 必须验证 BoneSpaceTransforms.Num() >= NumBones
- 正确保存 AnimSequence:
- 通过 IAnimationDataController 接口操作
- 使用 FScopedBracket 管理作用域
- 调用 AddBoneCurve() 而非 AddBoneTrack()
- 必须调用 NotifyPopulated()
遵循这些原则,就能实现稳定可靠的动画录制系统。
引擎版本:Unreal Engine 5.4
参考源码:AnimSequence.cpp (2630-2692行)
感谢阅读,欢迎点赞、关注、收藏,有问题可在评论区交流。
如果本文对你有帮助,点击这里捐赠支持作者。