AnimationEditMode 模块全面系统分析
目录
1 模块概述
1.1 基本信息
| 属性 | 值 |
|---|---|
| 模块名称 | AnimationEditMode |
| 类型 | Editor |
| 描述 | 动画编辑模式模块,为动画编辑提供统一的编辑上下文接口,桥接旧的 Persona 编辑模式与新的 Interactive Tools Framework |
整个模块体量很小,只有 4 个文件:一个 .Build.cs 构建文件、一个接口头文件、一个编辑模式头文件和一个实现文件。代码量不大,但设计思路非常值得学习——它完美展示了 UE5 是如何在"不破坏旧代码"的前提下,把旧的编辑模式体系平滑迁移到新框架的。
在 AnimationEditMode 模块中,定义了 3 个核心类型:IAnimationEditContext(编辑上下文接口)、FAnimationEditMode(编辑模式实现)、UAnimationEditModeContext(UObject 兼容性包装器)。
这个模块依赖了引擎的 Core、CoreUObject、Engine、UnrealEd、EditorInteractiveToolsFramework、InteractiveToolsFramework 共 6 个模块。核心依赖是:FEdMode(旧编辑模式基类)、UEdMode(新脚本化编辑模式)、UContextObjectStore(交互工具上下文存储)。
1.2 这个模块到底解决了什么问题?
在 UE5 的编辑器架构演进中,Epic 引入了一套全新的 Interactive Tools Framework(交互工具框架),它基于 UEdMode(脚本化编辑模式)来管理编辑器中的交互操作。而在此之前,动画编辑相关的功能(Persona 系统)使用的是旧的 FEdMode 体系,并且大量代码依赖 IPersonaEditMode 接口来获取编辑上下文(如相机目标、预览场景、调试信息)。
问题来了:旧的 Persona 编辑模式代码依赖 IPersonaEditMode,新的框架需要 IAnimationEditContext,两边接口不兼容。 如果直接重写所有 Persona 相关代码,工作量巨大且风险极高。
AnimationEditMode 模块就是为这个场景设计的"适配器"——它定义了一个新的 IAnimationEditContext 接口,同时提供了一个 FAnimationEditMode 类,这个类既实现了旧的 FEdMode,又实现了新的 IAnimationEditContext。更巧妙的是,它还内嵌了一个 UAnimationEditModeContext(UObject 包装器),可以注册到新框架的 UContextObjectStore 中,让所有使用 Interactive Tools Framework 的工具都能通过统一的接口访问动画编辑上下文。
旧代码 (IPersonaEditMode) 新代码 (Interactive Tools)
│ │
│ ▼
│ UContextObjectStore
│ │
▼ ▼
FAnimationEditMode ◄────────── UAnimationEditModeContext
│ (UObject 包装器)
│
▼
IAnimationEditContext (统一接口)
├── GetCameraTarget()
├── GetAnimPreviewScene()
└── GetOnScreenDebugInfo()
2 模块整体架构解析
2.1 架构图
┌─────────────────────────────────────────────────────────────────────┐
│ 编辑器编辑模式 (Editor Mode) │
│ │
│ ┌─────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ FEdMode (旧体系) │ │ UEdMode + Interactive Tools │ │
│ │ - Persona 编辑模式 │ │ (新体系) │ │
│ │ - IPersonaEditMode │ │ - UContextObjectStore │ │
│ └───────────┬─────────────┘ └────────────────┬─────────────────┘ │
│ │ │ │
└──────────────┼────────────────────────────────────┼───────────────────┘
│ │
│ ┌─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ FAnimationEditMode (核心适配器) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 继承: │ │
│ │ - FEdMode (旧的编辑模式基类) │ │
│ │ - IAnimationEditContext (新的编辑上下文接口) │ │
│ │ - FNoncopyable (禁止拷贝) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 核心职责: │ │
│ │ - Enter() / Exit(): 管理编辑模式生命周期 │ │
│ │ - 创建并注册 UAnimationEditModeContext 到 ContextObjectStore │ │
│ │ - 实现 IAnimationEditContext 的三个接口方法 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 成员变量: │ │
│ │ - AnimationEditModeContext: TObjectPtr<UAnimationEditModeContext> │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ 创建并持有
▼
┌─────────────────────────────────────────────────────────────────────┐
│ UAnimationEditModeContext (UObject 兼容层) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 继承: │ │
│ │ - UObject (可被 GC 管理、可注册到 ContextObjectStore) │ │
│ │ - IAnimationEditContext (对外暴露相同的接口) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 核心职责: │ │
│ │ - 将 IAnimationEditContext 接口包装为 UObject 形式 │ │
│ │ - 内部持有真实 EditMode 的裸指针,方法调用全部转发 │ │
│ │ - 通过 CreateFor() 静态工厂方法创建 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 成员变量: │ │
│ │ - EditMode: IAnimationEditContext* (指向真实编辑模式的裸指针) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ 实现
▼
┌─────────────────────────────────────────────────────────────────────┐
│ IAnimationEditContext (接口) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 纯虚接口: │ │
│ │ - GetCameraTarget(FSphere& OutTarget) → bool │ │
│ │ - GetAnimPreviewScene() → IPersonaPreviewScene& │ │
│ │ - GetOnScreenDebugInfo(TArray<FText>& OutDebugInfo) → void │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
2.2 模块间依赖关系
AnimationEditMode 模块
│
├──→ Core (基础类型、容器)
├──→ CoreUObject (UObject、接口系统)
├──→ Engine (IPersonaPreviewScene 等)
├──→ UnrealEd (FEdMode、EditorModeManager)
├──→ EditorInteractiveToolsFramework (UEdMode、UContextObjectStore)
└──→ InteractiveToolsFramework (交互工具基础设施)
依赖关系很清晰:
- Core / CoreUObject:提供最基础的 C++ 和 UObject 类型支持
- Engine:提供
IPersonaPreviewScene(动画预览场景接口),这是IAnimationEditContext的核心返回值 - UnrealEd:提供
FEdMode(旧编辑模式基类)和EditorModeManager - EditorInteractiveToolsFramework:提供
UEdMode(新脚本化编辑模式)和UContextObjectStore - InteractiveToolsFramework:交互工具框架的基础设施
2.3 模块划分
1. IAnimationEditContext — 接口定义层
职责:
- 定义动画编辑上下文的统一接口
- 提供相机目标获取、动画预览场景访问、屏幕调试信息收集三个能力
特点:
- 纯虚接口,不包含任何实现
- 三个方法中只有
GetAnimPreviewScene()是纯虚的(必须实现),其余两个有默认空实现 - 使用 UE 的
UINTERFACE宏同时生成 UObject 包装接口和 C++ 纯虚接口
2. FAnimationEditMode — 编辑模式实现层
职责:
- 实现完整的编辑模式生命周期管理
- 同时满足旧
FEdMode体系和新IAnimationEditContext体系的要求 - 在进入/退出编辑模式时注册/注销
UAnimationEditModeContext
特点:
- 多重继承:
FEdMode+IAnimationEditContext - 禁止拷贝(
= delete拷贝构造和赋值运算符) - 通过
TObjectPtr持有UAnimationEditModeContext,确保 GC 安全
3. UAnimationEditModeContext — UObject 兼容适配层
职责:
- 将
IAnimationEditContext接口包装为UObject形式 - 使编辑上下文可以被注册到
UContextObjectStore中 - 所有方法调用透明转发给真实的
FAnimationEditMode实现
特点:
- 继承
UObject+IAnimationEditContext - 使用裸指针
IAnimationEditContext*指向真实实现(非拥有关系) - 静态工厂方法
CreateFor()创建实例,构造函数私有化 - 每个方法都使用
check(EditMode)确保指针有效性
2.4 数据流走向
编辑模式进入流程:
1. 编辑器切换到动画编辑模式
↓
2. FAnimationEditMode::Enter() 被调用
↓
3. 调用父类 FEdMode::Enter() 完成基础初始化
↓
4. 通过 GetModeManager() 获取当前激活的 UEdMode (脚本化模式)
↓
5. 从 UEdMode 获取 InteractiveToolsContext
↓
6. 从 InteractiveToolsContext 获取 ContextObjectStore
↓
7. 将 AnimationEditModeContext 注册到 ContextObjectStore
↓
8. 新框架中的交互工具可以通过 ContextObjectStore 查询到 IAnimationEditContext
编辑模式退出流程:
1. 编辑器切换到其他模式
↓
2. FAnimationEditMode::Exit() 被调用
↓
3. 从 ContextObjectStore 中移除 AnimationEditModeContext
↓
4. 调用父类 FEdMode::Exit() 完成清理
上下文查询流程(新框架中的工具使用):
1. 交互工具需要获取动画编辑上下文
↓
2. 从 InteractiveToolsContext 获取 ContextObjectStore
↓
3. 通过 ContextObjectStore 查询 UAnimationEditModeContext
↓
4. UAnimationEditModeContext 将调用转发给 FAnimationEditMode
↓
5. FAnimationEditMode 返回实际的动画预览场景 / 相机目标 / 调试信息
2.5 核心技术栈
| 技术/类 | 用途 |
|---|---|
FEdMode | 旧编辑器编辑模式基类 |
UEdMode | 新的脚本化编辑模式 |
UContextObjectStore | 交互工具上下文对象存储,用于在工具间共享上下文 |
IAnimationEditContext | 动画编辑上下文的统一接口 |
IPersonaPreviewScene | Persona 动画预览场景接口 |
UINTERFACE | UE 反射宏,同时生成 UObject 和 C++ 接口 |
TObjectPtr | UE5 的智能指针,替代原始 UObject*,GC 安全 |
FReferenceCollector | GC 引用收集器,用于手动标记 UObject 引用 |
2.6 架构设计优势
- 无缝兼容:同时支持旧的
FEdMode体系和新的 Interactive Tools Framework,不需要大规模重写已有代码 - 接口统一:
IAnimationEditContext提供了清晰的接口契约,新旧代码都可以通过同一套接口访问动画编辑上下文 - 适配器模式:
UAnimationEditModeContext是典型的适配器模式实现,将 C++ 接口包装为 UObject,使其能被UContextObjectStore管理 - 生命周期安全:
FAnimationEditMode在Enter()时注册、Exit()时注销,确保上下文对象只在编辑模式激活期间可用 - GC 安全:使用
TObjectPtr和AddReferencedObjects确保 UObject 不会被意外回收 - 低耦合:
UAnimationEditModeContext通过裸指针指向真实实现,不拥有所有权,避免循环引用
2.7 潜在改进点
- 裸指针风险:
UAnimationEditModeContext使用裸指针IAnimationEditContext*指向FAnimationEditMode,如果FAnimationEditMode先于UAnimationEditModeContext销毁,会导致悬空指针。虽然当前通过Enter()/Exit()的生命周期管理避免了这个问题,但缺乏编译期保障 - 单例假设:
Enter()方法中通过GetActiveScriptableMode(Info.ID)获取脚本化模式,假设了当前只有一个激活的脚本化模式,如果未来支持多模式共存,需要调整 - 接口扩展性:
IAnimationEditContext目前只有三个方法,如果未来需要扩展更多上下文信息,需要修改接口,可能影响所有实现者
3 类级代码注释详解
3.1 IAnimationEditContext 接口
3.1.1 概述
IAnimationEditContext 是 AnimationEditMode 模块的核心接口,定义了动画编辑过程中需要向外部工具暴露的上下文信息。它遵循 UE 的标准接口定义模式——使用 UINTERFACE 宏生成 UObject 反射接口,同时定义一个同名的 C++ 纯虚接口类。
核心设计理念:
- 最小化接口:只暴露三个最必要的方法,避免接口膨胀
- 可选实现:
GetCameraTarget和GetOnScreenDebugInfo有默认空实现,只有GetAnimPreviewScene是纯虚的 - 关注点分离:将"编辑上下文"从"编辑模式实现"中解耦出来
3.1.2 接口方法详解
GetCameraTarget(FSphere& OutTarget) → bool
功能分析:
- 获取相机聚焦目标,当用户在视口中按下 ‘F’ 键(默认)时用于聚焦
- 返回一个
FSphere(球体),描述目标物体的包围球 - 返回值
true表示成功填充了目标球体
默认实现: 返回 false,表示没有可聚焦的目标
使用场景: 在动画编辑视口中,用户选中骨骼后按 F 键,视口应该聚焦到该骨骼位置。这个方法就是为这个聚焦行为提供目标数据的。
GetAnimPreviewScene() → IPersonaPreviewScene&
功能分析:
- 获取动画预览场景的引用
- 这是唯一一个纯虚方法,所有实现者必须提供
- 返回
IPersonaPreviewScene接口引用,这是 Persona 系统的核心场景抽象
为什么是纯虚: 动画编辑模式的核心就是预览场景,没有预览场景就没有动画编辑,所以这个方法必须实现。
GetOnScreenDebugInfo(TArray& OutDebugInfo)
功能分析:
- 收集需要在视口中显示的调试文本信息
- 将调试文本追加到
OutDebugInfo数组中 - 主要用于骨骼控制器(Skeletal Controls)在视口中显示调试信息
默认实现: 空实现,不做任何操作
设计意图: 这个方法替代了传统的 DrawHUD 方式——不再是直接在视口上绘制,而是将文本信息收集起来,由框架统一渲染。这样可以在不同层级(如骨骼、控制器、工具)收集调试信息,最终统一展示。
3.2 FAnimationEditMode 类
3.2.1 概述
FAnimationEditMode 是 AnimationEditMode 模块的核心实现类。它同时继承自 FEdMode(旧编辑模式基类)和 IAnimationEditContext(新的编辑上下文接口),充当两套体系之间的桥梁。
核心设计理念:
- 双重身份:既是旧体系的编辑模式,又是新体系的上下文提供者
- 适配器模式:创建
UAnimationEditModeContext作为 UObject 包装器,注册到新框架中 - 生命周期管理:在
Enter()/Exit()中精确控制上下文对象的注册和注销
3.2.2 构造函数
FAnimationEditMode::FAnimationEditMode()
: AnimationEditModeContext(UAnimationEditModeContext::CreateFor(this))
功能分析:
- 使用成员初始化列表调用
UAnimationEditModeContext::CreateFor(this)创建上下文包装器 - 将
this(即FAnimationEditMode自身)作为IAnimationEditContext*传入 - 上下文包装器内部保存了这个裸指针,后续所有接口调用都会转发回来
关键点: CreateFor 是私有静态工厂方法,确保 UAnimationEditModeContext 的创建方式受控,外部代码无法随意创建。
3.2.3 Enter() — 进入编辑模式
void FAnimationEditMode::Enter()
{
FEdMode::Enter();
if (const UEdMode* EdMode = GetModeManager()->GetActiveScriptableMode(Info.ID))
{
UContextObjectStore* ContextObjectStore = EdMode->GetInteractiveToolsContext()->ContextObjectStore;
ContextObjectStore->AddContextObject(AnimationEditModeContext.Get());
}
}
功能分析(逐步骤):
- 调用父类 Enter():
FEdMode::Enter()完成旧体系的编辑模式初始化 - 获取脚本化模式:通过
GetModeManager()->GetActiveScriptableMode(Info.ID)获取当前激活的UEdMode(新体系)Info.ID是编辑模式的唯一标识符- 这里假设旧的
FEdMode和新的UEdMode使用相同的 ID
- 获取 ContextObjectStore:从
UEdMode的交互工具上下文中获取UContextObjectStore - 注册上下文对象:将
AnimationEditModeContext添加到ContextObjectStore中- 此后,所有使用该 InteractiveToolsContext 的工具都可以通过
ContextObjectStore查询到IAnimationEditContext
- 此后,所有使用该 InteractiveToolsContext 的工具都可以通过
设计要点:
- 使用
if判断来防御性编程——如果脚本化模式不存在,不会崩溃,只是跳过注册 ContextObjectStore是UContextObjectStore的实例,它维护了一个类型到对象的映射,支持按类型查询
3.2.4 Exit() — 退出编辑模式
void FAnimationEditMode::Exit()
{
if (const UEdMode* EdMode = GetModeManager()->GetActiveScriptableMode(Info.ID))
{
UContextObjectStore* ContextObjectStore = EdMode->GetInteractiveToolsContext()->ContextObjectStore;
ContextObjectStore->RemoveContextObject(AnimationEditModeContext.Get());
}
FEdMode::Exit();
}
功能分析(逐步骤):
- 移除上下文对象:从
ContextObjectStore中移除AnimationEditModeContext- 这一步确保退出编辑模式后,不再有工具能通过旧路径访问动画编辑上下文
- 调用父类 Exit():
FEdMode::Exit()完成旧体系的清理
执行顺序的意义: 先移除上下文对象,再调用父类 Exit()。这确保了在父类清理过程中,不会有工具尝试通过上下文对象访问已部分销毁的状态。
3.2.5 AddReferencedObjects() — GC 引用管理
void FAnimationEditMode::AddReferencedObjects(FReferenceCollector& Collector)
{
FEdMode::AddReferencedObjects(Collector);
Collector.AddReferencedObject(AnimationEditModeContext);
}
功能分析:
- 重写
FEdMode::AddReferencedObjects,将AnimationEditModeContext添加到 GC 引用收集器中 - 这是 UE 的 GC 机制要求——所有
UObject*类型的成员变量都需要通过此方法告知 GC,否则可能被意外回收 FEdMode本身不是UObject,但它实现了FGCObject接口,可以参与 GC 引用管理
3.3 UAnimationEditModeContext 类
3.3.1 概述
UAnimationEditModeContext 是一个 UObject 包装器,它实现了 IAnimationEditContext 接口,但所有方法调用都转发给内部的真实实现。这个类的存在纯粹是为了"兼容性"——将 C++ 接口包装为 UObject,使其能被 UContextObjectStore 管理。
代码注释原文:
A compatibility context object to support IPersonaEditMode-based code. It simply calls into a different IAnimationEditContext in its implementations.
翻译过来就是:“一个兼容性上下文对象,用于支持基于 IPersonaEditMode 的代码。它只是在其实现中调用另一个 IAnimationEditContext。”
3.3.2 成员变量
IAnimationEditContext* EditMode = nullptr;
功能分析:
- 裸指针,指向真实的
IAnimationEditContext实现(即FAnimationEditMode) - 非拥有关系——
UAnimationEditModeContext不负责EditMode的生命周期 - 由
FAnimationEditMode在构造函数中通过CreateFor()设置
设计考量: 使用裸指针而非智能指针是因为:
FAnimationEditMode的生命周期一定长于UAnimationEditModeContext(前者持有后者的TObjectPtr)- 避免循环引用(
FAnimationEditMode→TObjectPtr→UAnimationEditModeContext→TSharedPtr→FAnimationEditMode)
3.3.3 CreateFor() — 静态工厂方法
static UAnimationEditModeContext* CreateFor(IAnimationEditContext* InEditMode)
{
UAnimationEditModeContext* NewPersonaContext = NewObject<UAnimationEditModeContext>();
NewPersonaContext->EditMode = InEditMode;
return NewPersonaContext;
}
功能分析:
- 使用
NewObject创建 UObject 实例(确保 GC 正确追踪) - 将传入的
IAnimationEditContext*设置为内部指针 - 方法为
private,只有FAnimationEditMode可以调用(通过friend声明)
为什么用工厂方法:
- 控制创建方式,确保
EditMode指针在创建时就被正确设置 - 外部代码无法创建未初始化的
UAnimationEditModeContext实例
3.3.4 接口方法实现
三个方法的结构完全一致,以 GetCameraTarget 为例:
bool UAnimationEditModeContext::GetCameraTarget(FSphere& OutTarget) const
{
check(EditMode);
return EditMode->GetCameraTarget(OutTarget);
}
功能分析:
check(EditMode)— 断言内部指针非空,如果为nullptr则触发断言崩溃- 将调用直接转发给
EditMode(即FAnimationEditMode) - 这是一个典型的**代理模式(Proxy Pattern)**实现
为什么需要 check: 在正常的生命周期中,EditMode 永远不会为空(因为 UAnimationEditModeContext 的生命周期完全由 FAnimationEditMode 控制)。如果出现空指针,说明存在严重的生命周期管理 Bug,check 可以在开发阶段尽早暴露问题。
4 功能使用示例编写
示例1:在自定义动画编辑模式中复用 AnimationEditMode
假设我们需要为自己的骨骼网格编辑工具创建一个编辑模式,可以继承 FAnimationEditMode 来获得完整的动画编辑上下文支持:
// MySkeletalEditMode.h
#pragma once
#include "AnimationEditMode.h"
#include "MySkeletalEditMode.generated.h"
UCLASS()
class UMySkeletalEditContext : public UObject, public IAnimationEditContext
{
GENERATED_BODY()
// ... 自定义实现
};
class FMySkeletalEditMode : public FAnimationEditMode
{
public:
FMySkeletalEditMode();
virtual void Enter() override;
virtual void Exit() override;
// 重写 IAnimationEditContext 方法以提供自定义行为
virtual bool GetCameraTarget(FSphere& OutTarget) const override;
virtual class IPersonaPreviewScene& GetAnimPreviewScene() const override;
virtual void GetOnScreenDebugInfo(TArray<FText>& OutDebugInfo) const override;
private:
// 自定义的预览场景
TSharedPtr<class FMyPersonaPreviewScene> MyPreviewScene;
};
// MySkeletalEditMode.cpp
#include "MySkeletalEditMode.h"
FMySkeletalEditMode::FMySkeletalEditMode()
: MyPreviewScene(MakeShared<FMyPersonaPreviewScene>())
{
}
void FMySkeletalEditMode::Enter()
{
// 父类会处理 ContextObjectStore 注册
FAnimationEditMode::Enter();
// 自定义的初始化逻辑
MyPreviewScene->Initialize();
UE_LOG(LogTemp, Display, TEXT("[MySkeletalEditMode] 进入自定义骨骼编辑模式"));
}
void FMySkeletalEditMode::Exit()
{
// 自定义的清理逻辑
MyPreviewScene->Shutdown();
// 父类会处理 ContextObjectStore 注销
FAnimationEditMode::Exit();
UE_LOG(LogTemp, Display, TEXT("[MySkeletalEditMode] 退出自定义骨骼编辑模式"));
}
bool FMySkeletalEditMode::GetCameraTarget(FSphere& OutTarget) const
{
// 自定义的相机目标逻辑:聚焦到当前选中的骨骼
if (MyPreviewScene->GetSelectedBoneLocation(OutTarget.Center))
{
OutTarget.W = 50.0f; // 50 单位半径
return true;
}
return false;
}
IPersonaPreviewScene& FMySkeletalEditMode::GetAnimPreviewScene() const
{
return *MyPreviewScene;
}
void FMySkeletalEditMode::GetOnScreenDebugInfo(TArray<FText>& OutDebugInfo) const
{
OutDebugInfo.Add(FText::FromString(
FString::Printf(TEXT("选中骨骼: %s"), *MyPreviewScene->GetSelectedBoneName().ToString())
));
}
示例2:在新框架的交互工具中访问动画编辑上下文
假设我们使用 Interactive Tools Framework 开发了一个骨骼操作工具,可以通过 UContextObjectStore 获取动画编辑上下文:
// MyBoneManipulationTool.h
#pragma once
#include "InteractiveTool.h"
#include "MyBoneManipulationTool.generated.h"
UCLASS()
class UMyBoneManipulationTool : public UInteractiveTool
{
GENERATED_BODY()
public:
virtual void Setup() override;
virtual void Render(IToolsContextRenderAPI* RenderAPI) override;
virtual void OnTick(float DeltaTime) override;
private:
// 从 ContextObjectStore 获取的动画编辑上下文
IAnimationEditContext* AnimationEditContext = nullptr;
};
// MyBoneManipulationTool.cpp
#include "MyBoneManipulationTool.h"
#include "AnimationEditContext.h"
#include "ContextObjectStore.h"
#include "ToolsContext.h"
void UMyBoneManipulationTool::Setup()
{
Super::Setup();
// 从 ContextObjectStore 中查询动画编辑上下文
if (UContextObjectStore* ContextStore = GetToolManager()->GetContextObjectStore())
{
// 按类型查询:查找实现了 IAnimationEditContext 接口的 UObject
UAnimationEditModeContext* ContextObject =
ContextStore->FindContext<UAnimationEditModeContext>();
if (ContextObject)
{
// 获取到上下文对象,可以直接使用
AnimationEditContext = ContextObject;
UE_LOG(LogTemp, Display, TEXT("[BoneTool] 成功获取动画编辑上下文"));
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[BoneTool] 未找到动画编辑上下文,部分功能不可用"));
}
}
}
void UMyBoneManipulationTool::Render(IToolsContextRenderAPI* RenderAPI)
{
if (!AnimationEditContext)
{
return;
}
// 获取动画预览场景,做一些自定义渲染
IPersonaPreviewScene& PreviewScene = AnimationEditContext->GetAnimPreviewScene();
// 获取相机目标,调整视口
FSphere CameraTarget;
if (AnimationEditContext->GetCameraTarget(CameraTarget))
{
// 使用相机目标调整渲染参数
// ...
}
}
void UMyBoneManipulationTool::OnTick(float DeltaTime)
{
if (!AnimationEditContext)
{
return;
}
// 收集调试信息
TArray<FText> DebugInfo;
AnimationEditContext->GetOnScreenDebugInfo(DebugInfo);
// 追加自定义调试信息
DebugInfo.Add(FText::FromString(TEXT("骨骼操作工具正在运行")));
// 将调试信息输出到视口
// ...
}
示例3:在现有 Persona 代码中迁移到 IAnimationEditContext
假设项目中有一段旧的 Persona 代码,原先依赖 IPersonaEditMode:
// 旧代码(迁移前)
void UMyOldPersonaTool::DoSomething(IPersonaEditMode* PersonaEditMode)
{
// 旧接口:直接使用 IPersonaEditMode
IPersonaPreviewScene& Scene = PersonaEditMode->GetAnimPreviewScene();
FSphere Target;
PersonaEditMode->GetCameraTarget(Target);
}
迁移到新接口后:
// 新代码(迁移后)
void UMyNewTool::DoSomething(IAnimationEditContext* AnimEditContext)
{
// 新接口:使用 IAnimationEditContext,API 完全兼容
IPersonaPreviewScene& Scene = AnimEditContext->GetAnimPreviewScene();
FSphere Target;
AnimEditContext->GetCameraTarget(Target);
}
迁移非常简单——只需要把参数类型从 IPersonaEditMode* 改为 IAnimationEditContext*,方法调用完全不变。这正是 AnimationEditMode 模块设计的核心价值:在不改变调用方式的前提下,完成底层框架的切换。
5 总结与最佳实践
5.1 核心要点
-
AnimationEditMode 是一个"桥接"模块
- 它连接了旧的
FEdMode体系和新 Interactive Tools Framework - 它不是一个功能模块,而是一个架构适配层
- 理解它的关键在于理解"为什么要做这个适配"
- 它连接了旧的
-
三层结构各司其职
IAnimationEditContext:定义"能做什么"(接口契约)FAnimationEditMode:实现"怎么做"(编辑模式逻辑 + 生命周期管理)UAnimationEditModeContext:解决"放在哪"(UObject 包装,可注册到 ContextObjectStore)
-
适配器模式是核心
UAnimationEditModeContext是典型的对象适配器模式- 将 C++ 接口适配为 UObject,使其能被 UE 编辑器框架管理
- 所有方法调用透明转发,调用者无感知
-
生命周期管理是重点
Enter()注册上下文 →Exit()注销上下文FAnimationEditMode持有UAnimationEditModeContext的所有权UAnimationEditModeContext不持有FAnimationEditMode的所有权
5.2 最佳实践
-
继承 FAnimationEditMode 而非直接使用
FAnimationEditMode提供了 ContextObjectStore 注册/注销的完整流程- 自定义编辑模式应该继承它,专注于实现
IAnimationEditContext的三个接口方法 - 在子类的
Enter()/Exit()中务必调用父类方法
-
通过 ContextObjectStore 获取上下文
- 新框架的交互工具应该通过
UContextObjectStore::FindContext<T>()查询上下文 - 不要假设上下文一定存在,做好空指针检查
- 上下文只在编辑模式激活期间可用
- 新框架的交互工具应该通过
-
接口设计遵循"最小化原则"
IAnimationEditContext只定义了三个方法,不多不少- 如果未来需要扩展,优先考虑新增接口而非修改现有接口
- 非纯虚方法(有默认实现)不会强制所有实现者修改代码
-
GC 安全不容忽视
- 所有
UObject*成员变量都应使用TObjectPtr(UE5.1+) - 非
UObject类(如FEdMode)需要重写AddReferencedObjects来告知 GC - 裸指针(如
IAnimationEditContext*)的使用必须确保生命周期安全
- 所有
-
理解而非照搬
- AnimationEditMode 的代码量很小,但设计思路值得学习
- 在自己的项目中遇到类似的"新旧框架适配"问题时,可以参考这个模块的适配器模式
- 关键不是代码本身,而是它展示的"如何在不破坏旧代码的前提下平滑迁移"
5.3 常见问题
问题1:为什么 UAnimationEditModeContext 使用裸指针而不是智能指针?
答:为了避免循环引用。FAnimationEditMode(非 UObject)通过 TObjectPtr 持有 UAnimationEditModeContext(UObject),如果后者再通过智能指针持有前者,会形成循环引用。由于 FAnimationEditMode 的生命周期一定长于 UAnimationEditModeContext(前者在构造函数中创建后者,在析构时销毁),使用裸指针是安全的。
问题2:为什么 Enter() 中获取 UEdMode 时使用了 if 判断?
答:防御性编程。GetActiveScriptableMode 可能返回 nullptr(例如编辑模式没有对应的脚本化模式),直接使用会导致空指针崩溃。使用 if 判断可以优雅降级——没有脚本化模式就不注册上下文,不影响旧体系的正常工作。
问题3:AddReferencedObjects 为什么需要?
答:FAnimationEditMode 不是 UObject,但它持有 UObject* 成员(AnimationEditModeContext)。UE 的 GC 系统无法自动追踪非 UObject 中的 UObject 引用,所以需要手动通过 AddReferencedObjects 告知 GC:“这个 UObject 还在被使用,不要回收它”。
附录:文件结构
AnimationEditMode/
├── AnimationEditMode.Build.cs # 模块构建文件
├── Private/
│ └── AnimationEditMode.cpp # 核心实现
└── Public/
├── AnimationEditContext.h # IAnimationEditContext 接口定义
└── AnimationEditMode.h # FAnimationEditMode + UAnimationEditModeContext 声明
相关资源
- UE5 官方文档:Interactive Tools Framework
- UE5 官方文档:Editor Modes
- Persona 动画编辑器架构
FEdMode/UEdMode源码UContextObjectStore源码

213

被折叠的 条评论
为什么被折叠?



