UE5源码分析之Editor——AnimationEditMode模块全面分析

AnimationEditMode 模块全面系统分析


目录

  1. 模块概述
  2. 模块整体架构解析
  3. 类级代码注释详解
  4. 功能使用示例编写
  5. 总结与最佳实践

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动画编辑上下文的统一接口
IPersonaPreviewScenePersona 动画预览场景接口
UINTERFACEUE 反射宏,同时生成 UObject 和 C++ 接口
TObjectPtrUE5 的智能指针,替代原始 UObject*,GC 安全
FReferenceCollectorGC 引用收集器,用于手动标记 UObject 引用

2.6 架构设计优势

  1. 无缝兼容:同时支持旧的 FEdMode 体系和新的 Interactive Tools Framework,不需要大规模重写已有代码
  2. 接口统一IAnimationEditContext 提供了清晰的接口契约,新旧代码都可以通过同一套接口访问动画编辑上下文
  3. 适配器模式UAnimationEditModeContext 是典型的适配器模式实现,将 C++ 接口包装为 UObject,使其能被 UContextObjectStore 管理
  4. 生命周期安全FAnimationEditModeEnter() 时注册、Exit() 时注销,确保上下文对象只在编辑模式激活期间可用
  5. GC 安全:使用 TObjectPtrAddReferencedObjects 确保 UObject 不会被意外回收
  6. 低耦合UAnimationEditModeContext 通过裸指针指向真实实现,不拥有所有权,避免循环引用

2.7 潜在改进点

  1. 裸指针风险UAnimationEditModeContext 使用裸指针 IAnimationEditContext* 指向 FAnimationEditMode,如果 FAnimationEditMode 先于 UAnimationEditModeContext 销毁,会导致悬空指针。虽然当前通过 Enter()/Exit() 的生命周期管理避免了这个问题,但缺乏编译期保障
  2. 单例假设Enter() 方法中通过 GetActiveScriptableMode(Info.ID) 获取脚本化模式,假设了当前只有一个激活的脚本化模式,如果未来支持多模式共存,需要调整
  3. 接口扩展性IAnimationEditContext 目前只有三个方法,如果未来需要扩展更多上下文信息,需要修改接口,可能影响所有实现者

3 类级代码注释详解

3.1 IAnimationEditContext 接口

3.1.1 概述

IAnimationEditContext 是 AnimationEditMode 模块的核心接口,定义了动画编辑过程中需要向外部工具暴露的上下文信息。它遵循 UE 的标准接口定义模式——使用 UINTERFACE 宏生成 UObject 反射接口,同时定义一个同名的 C++ 纯虚接口类。

核心设计理念:

  • 最小化接口:只暴露三个最必要的方法,避免接口膨胀
  • 可选实现GetCameraTargetGetOnScreenDebugInfo 有默认空实现,只有 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());
    }
}

功能分析(逐步骤):

  1. 调用父类 Enter()FEdMode::Enter() 完成旧体系的编辑模式初始化
  2. 获取脚本化模式:通过 GetModeManager()->GetActiveScriptableMode(Info.ID) 获取当前激活的 UEdMode(新体系)
    • Info.ID 是编辑模式的唯一标识符
    • 这里假设旧的 FEdMode 和新的 UEdMode 使用相同的 ID
  3. 获取 ContextObjectStore:从 UEdMode 的交互工具上下文中获取 UContextObjectStore
  4. 注册上下文对象:将 AnimationEditModeContext 添加到 ContextObjectStore
    • 此后,所有使用该 InteractiveToolsContext 的工具都可以通过 ContextObjectStore 查询到 IAnimationEditContext

设计要点:

  • 使用 if 判断来防御性编程——如果脚本化模式不存在,不会崩溃,只是跳过注册
  • ContextObjectStoreUContextObjectStore 的实例,它维护了一个类型到对象的映射,支持按类型查询

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();
}

功能分析(逐步骤):

  1. 移除上下文对象:从 ContextObjectStore 中移除 AnimationEditModeContext
    • 这一步确保退出编辑模式后,不再有工具能通过旧路径访问动画编辑上下文
  2. 调用父类 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
  • 避免循环引用(FAnimationEditModeTObjectPtrUAnimationEditModeContextTSharedPtrFAnimationEditMode

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 核心要点

  1. AnimationEditMode 是一个"桥接"模块

    • 它连接了旧的 FEdMode 体系和新 Interactive Tools Framework
    • 它不是一个功能模块,而是一个架构适配层
    • 理解它的关键在于理解"为什么要做这个适配"
  2. 三层结构各司其职

    • IAnimationEditContext:定义"能做什么"(接口契约)
    • FAnimationEditMode:实现"怎么做"(编辑模式逻辑 + 生命周期管理)
    • UAnimationEditModeContext:解决"放在哪"(UObject 包装,可注册到 ContextObjectStore)
  3. 适配器模式是核心

    • UAnimationEditModeContext 是典型的对象适配器模式
    • 将 C++ 接口适配为 UObject,使其能被 UE 编辑器框架管理
    • 所有方法调用透明转发,调用者无感知
  4. 生命周期管理是重点

    • Enter() 注册上下文 → Exit() 注销上下文
    • FAnimationEditMode 持有 UAnimationEditModeContext 的所有权
    • UAnimationEditModeContext 不持有 FAnimationEditMode 的所有权

5.2 最佳实践

  1. 继承 FAnimationEditMode 而非直接使用

    • FAnimationEditMode 提供了 ContextObjectStore 注册/注销的完整流程
    • 自定义编辑模式应该继承它,专注于实现 IAnimationEditContext 的三个接口方法
    • 在子类的 Enter()/Exit() 中务必调用父类方法
  2. 通过 ContextObjectStore 获取上下文

    • 新框架的交互工具应该通过 UContextObjectStore::FindContext<T>() 查询上下文
    • 不要假设上下文一定存在,做好空指针检查
    • 上下文只在编辑模式激活期间可用
  3. 接口设计遵循"最小化原则"

    • IAnimationEditContext 只定义了三个方法,不多不少
    • 如果未来需要扩展,优先考虑新增接口而非修改现有接口
    • 非纯虚方法(有默认实现)不会强制所有实现者修改代码
  4. GC 安全不容忽视

    • 所有 UObject* 成员变量都应使用 TObjectPtr(UE5.1+)
    • UObject 类(如 FEdMode)需要重写 AddReferencedObjects 来告知 GC
    • 裸指针(如 IAnimationEditContext*)的使用必须确保生命周期安全
  5. 理解而非照搬

    • 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 源码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值