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

AnimationBlueprintEditor 模块全面系统分析

前言

AnimationBlueprintEditor 是 UE5 编辑器中最复杂的编辑器模块之一。它不像 AsyncMixin 那样小巧精悍(一个 .h + 一个 .cpp),也不像 CommonGame 那样主要做"胶水串联"工作。它是一个真正意义上的、完整的资产编辑器------承载着动画师和 TA 每天最核心的生产力工具。

初次接触这个模块的时候,很容易被它庞杂的继承关系和层层包裹的架构绕晕。FAnimationBlueprintEditor 继承了 FBlueprintEditor(蓝图编辑器基类),又实现了 IHasPersonaToolkit 接口,内部还依赖 Persona 模块提供的预览场景、骨骼树、动画浏览器等一系列基础设施。再加上 StateMachine 和 AnimationGraph 两套 Schema、十几种 Slate 节点工厂、编译管道的前后钩子......代码体量虽然不是特别大(约 30+ 个文件),但概念密度非常高。

我花了不少时间才逐渐理清楚:这个模块本质上在做的事情,就是把引擎中"动画蓝图"这个抽象概念,变成一个可以拖拽、连线、实时预览的编辑器体验。为了实现这个目标,Epic 采用了经典的多层架构------编辑器主体层管调度、编辑模式层管布局、Slate 可视化层管渲染、配置层管用户偏好。一层层拆开来看之后,就会发现设计得相当工整。

因此,这篇文章不是一个简单的代码注释堆砌,而是尝试从架构设计的视角,逐层剖析 AnimationBlueprintEditor 的运转逻辑,并附带可以直接落地的扩展示例。希望读完能对大家理解 UE5 编辑器架构有所助益。


目录

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

1 模块概述

1.1 基本信息

属性
模块名称 AnimationBlueprintEditor
类型 Editor
描述 动画蓝图编辑器,提供动画蓝图(AnimBlueprint)的完整编辑功能,包括动画图、状态机、混合空间等可视化编辑能力

AnimationBlueprintEditor 是 UE5 编辑器中的一个核心模块,负责为动画蓝图(UAnimBlueprint)提供完整的可视化编辑环境。它继承并扩展了 FBlueprintEditor(通用蓝图编辑器),与 Persona(动画工具框架)深度集成,是动画师和技术动画师日常工作的主战场。

整个模块包含约 30+ 个文件,涵盖编辑器主体、编辑模式、Slate 可视化节点、连接绘制策略、文档标签工厂等多个层次。代码量适中,结构清晰,非常适合作为学习 UE5 编辑器扩展的范例。

在 AnimationBlueprintEditor 模块中,主要定义了以下核心类:

  • IAnimationBlueprintEditor --- 编辑器公共接口
  • FAnimationBlueprintEditor --- 编辑器主体实现(约 2600 行 .cpp,承载了全部核心逻辑)
  • FAnimationBlueprintEditorMode --- 标准编辑模式(布局管理)
  • FAnimationBlueprintInterfaceEditorMode --- 接口编辑模式
  • FAnimationBlueprintEditorModule --- 模块生命周期管理
  • FAnimationGraphNodeFactory / FAnimationGraphPinFactory / FAnimationGraphPinConnectionFactory --- Slate 可视化工厂
  • FAnimGraphConnectionDrawingPolicy / FStateMachineConnectionDrawingPolicy --- 连接线绘制策略
  • FBlendSpaceDocumentTabFactory --- 混合空间文档标签

该模块依赖引擎的 Persona、Kismet、AnimGraph、BlueprintGraph、GraphEditor 等核心模块。核心依赖是:FPersonaModule(Persona 工具框架)、FBlueprintEditor(蓝图编辑器基类)、UAnimGraphNode_Base(动画图节点基类)、FTabManager(标签页布局管理)。

2 模块整体架构解析

2.1 架构图

想要理解 AnimationBlueprintEditor 的整体架构,可以从下面的层级关系入手。整个编辑器基于 FBlueprintEditor 框架,通过 Persona 模块获得动画预览能力,再通过自定义的 Slate 可视化工厂渲染出动画专用的图节点外观。

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                         用户交互层 (User Interaction)                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │  动画图编辑   │  │  状态机编辑   │  │  骨骼树面板   │  │  预览视口     │   │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘   │
└─────────┼──────────────────┼──────────────────┼──────────────────┼───────────┘
          │                  │                  │                  │
          └──────────────────┴──────────────────┴──────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                      FAnimationBlueprintEditor (编辑器主体)                     │
│  ┌───────────────────────────────────────────────────────────────────────┐   │
│  │  继承链: IAnimationBlueprintEditor → FBlueprintEditor → IHasPersonaToolkit│  │
│  └───────────────────────────────────────────────────────────────────────┘   │
│  ┌───────────────────────────────────────────────────────────────────────┐   │
│  │  核心成员:                                                                │  │
│  │  - PersonaToolkit: TSharedPtr<IPersonaToolkit>   (Persona 工具集)       │  │
│  │  - SkeletonTree: TSharedPtr<ISkeletonTree>       (骨骼树)               │  │
│  │  - PreviewEditor: TSharedPtr<SWidget>            (预览实例属性面板)       │  │
│  │  - SelectedAnimGraphNodes: TArray<TWeakObjectPtr<UAnimGraphNode_Base>>  │  │
│  │  - SequenceBrowser: TWeakPtr<IAnimationSequenceBrowser> (动画序列浏览器) │  │
│  └───────────────────────────────────────────────────────────────────────┘   │
│  ┌───────────────────────────────────────────────────────────────────────┐   │
│  │  关键功能:                                                                │  │
│  │  - InitAnimationBlueprintEditor()  初始化编辑器                           │  │
│  │  - OnSelectedNodesChangedImpl()    节点选择变更处理                        │  │
│  │  - OnBlueprintPreCompile/PostCompile  编译前后处理                        │  │
│  │  - HandlePinDefaultValueChanged()  引脚默认值实时传播到预览                │  │
│  │  - HandlePoseWatchSelectedNodes()  姿态监视选中节点                       │  │
│  │  - OnConvertTo*()                  节点类型转换系列函数                   │  │
│  └───────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
          │                              │                              │
          ▼                              ▼                              ▼
┌─────────────────────┐  ┌──────────────────────────┐  ┌──────────────────────┐
│  FAnimationBlueprint│  │  FAnimationBlueprint     │  │  FAnimationBlueprint │
│  EditorMode         │  │  InterfaceEditorMode     │  │  TemplateEditorMode  │
│  (标准编辑模式)      │  │  (接口编辑模式)           │  │  (模板编辑模式)       │
├─────────────────────┤  ├──────────────────────────┤  ├──────────────────────┤
│ - 完整布局:          │  │ - 简化布局:              │  │ - 与标准模式相同      │
│   视口+骨骼树+       │  │   文档编辑区+细节面板     │  │   但隐藏部分骨骼相关  │
│   图编辑器+资源浏览器│  │ - 无预览视口             │  │   面板               │
│ - 注册所有 Tab 工厂  │  │ - 仅注册蓝图接口 Tab     │  │                      │
└─────────────────────┘  └──────────────────────────┘  └──────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          Slate 可视化层 (Visual Layer)                          │
│  ┌───────────────────────────────────────────────────────────────────────┐   │
│  │  FAnimationGraphNodeFactory (节点工厂)                                    │  │
│  │  ├─ SAnimationGraphNode        (通用动画节点外观)                        │  │
│  │  ├─ SGraphNodeAnimationResult  (输出结果节点)                            │  │
│  │  ├─ SGraphNodeSequencePlayer   (序列播放器节点)                          │  │
│  │  ├─ SGraphNodeBlendSpacePlayer (混合空间播放器节点)                       │  │
│  │  ├─ SGraphNodeBlendSpaceGraph  (混合空间图节点)                           │  │
│  │  ├─ SGraphNodeLayeredBoneBlend (分层骨骼混合节点)                         │  │
│  │  ├─ SGraphNodeLinkedLayer      (链接动画层节点)                           │  │
│  │  ├─ SGraphNodeStateMachineInstance (状态机实例节点)                       │  │
│  │  ├─ SGraphNodeAnimState        (动画状态节点)                             │  │
│  │  ├─ SGraphNodeAnimTransition   (动画过渡节点)                             │  │
│  │  └─ SAnimNodeReference         (动画节点引用)                             │  │
│  └───────────────────────────────────────────────────────────────────────┘   │
│  ┌───────────────────────────────────────────────────────────────────────┐   │
│  │  FAnimationGraphPinFactory (引脚工厂)                                      │  │
│  │  └─ SGraphPinPose            (Pose 引脚外观,带属性列表显示)               │  │
│  └───────────────────────────────────────────────────────────────────────┘   │
│  ┌───────────────────────────────────────────────────────────────────────┐   │
│  │  FAnimationGraphPinConnectionFactory (连接线工厂)                         │  │
│  │  ├─ FAnimGraphConnectionDrawingPolicy      (动画图连接绘制)               │  │
│  │  └─ FStateMachineConnectionDrawingPolicy   (状态机连接绘制)               │  │
│  └───────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 模块间依赖关系

如果上述的架构图看起来还有点抽象,可以参考下面的依赖关系进行理解。

复制代码
FAnimationBlueprintEditorModule (模块入口)
    ↓ 创建
FAnimationBlueprintEditor (编辑器主体)
    ↓ 继承
FBlueprintEditor (蓝图编辑器基类)
    ↓ 依赖
┌───────────────────┬───────────────────┬───────────────────┐
│  FPersonaModule   │  AnimGraph 模块    │  BlueprintGraph   │
│  (Persona 框架)   │  (动画图节点)      │  (蓝图图节点)     │
└───────────────────┴───────────────────┴───────────────────┘
    ↓                       ↓                    ↓
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ IPersonaToolkit   │ │ UAnimGraphNode_Base│ │ FBlueprintEditor  │
│ (预览场景/骨骼网格)│ │ (动画节点基类)     │ │ (通用编辑功能)    │
└───────────────────┘ └───────────────────┘ └───────────────────┘

可能大家看到这里会感到奇怪,为什么 AnimationBlueprintEditor 不直接自己实现所有功能,而是要依赖 Persona 模块?

实际上,UE5 引擎开发者的目的是为了提供一套可复用的动画工具框架。Persona 模块提供了动画预览场景、骨骼网格组件、骨骼树、动画序列浏览器等通用基础设施,这些不仅在动画蓝图编辑器中使用,在骨骼编辑器(SkeletonEditor)、动画编辑器(AnimationEditor)中同样被使用。通过将通用能力下沉到 Persona,AnimationBlueprintEditor 可以专注于动画蓝图特有的编辑逻辑------如图节点管理、姿态监视、节点类型转换、编译管道等。

同时,这种架构为扩展提供了极大的灵活性。例如,如果想为动画蓝图编辑器添加自定义的工具栏按钮,只需要通过 IAnimationBlueprintEditorModule::GetAllAnimationBlueprintEditorToolbarExtenders() 注册一个扩展器即可,完全不需要修改编辑器本身的代码。

2.3 模块划分

对于每个模块的功能总结如下:

1. 编辑器主体层 --- FAnimationBlueprintEditor

职责:

  • 继承 FBlueprintEditor,提供动画蓝图编辑的核心功能
  • 管理 Persona 工具集(IPersonaToolkit)和骨骼树(ISkeletonTree
  • 处理节点选择、姿态监视(Pose Watch)、节点类型转换
  • 协调编译前/后处理流程,确保预览实例与编辑数据同步
  • 管理编辑器设置(UAnimationBlueprintEditorOptions)的加载和保存

特点:

  • 实现 IAnimationBlueprintEditorIHasPersonaToolkit 接口
  • 支持三种编辑模式:标准模式、接口模式、模板模式
  • 通过 FOnPostUndo 多播委托支持子控件刷新
  • 钩入蓝图异常处理以优雅地应对无限循环

2. 编辑模式层 --- FAnimationBlueprintEditorMode / FAnimationBlueprintInterfaceEditorMode

职责:

  • 定义编辑器窗口的标签页布局(Tab Layout)
  • 注册各类标签页工厂(Tab Factory)
  • 处理模式激活/停用时的状态切换

特点:

  • 标准模式:完整的三栏布局(视口 | 图编辑区 | 细节面板)
  • 接口模式:简化的两栏布局(图编辑区 | 细节面板)
  • 模板模式:与标准模式相同,但隐藏部分骨骼相关面板

3. 模块管理层 --- FAnimationBlueprintEditorModule

职责:

  • 管理模块的启动和关闭生命周期
  • 注册 Slate 可视化工厂(节点、引脚、连接线)
  • 注册编辑器设置(UAnimationBlueprintEditorSettings
  • 注册消息日志(MessageLog)分类
  • 处理新蓝图创建时的默认节点添加

特点:

  • 实现 IAnimationBlueprintEditorModule 接口
  • 提供菜单和工具栏的扩展点(FExtensibilityManager
  • 支持自定义调试对象(OnGetCustomDebugObjects 委托)

4. Slate 可视化层 --- 节点工厂、引脚工厂、连接绘制策略

职责:

  • FAnimationGraphNodeFactory:根据 UEdGraphNode 类型创建对应的 Slate 外观控件
  • FAnimationGraphPinFactory:为 Pose 引脚创建带属性列表显示的特殊外观
  • FAnimGraphConnectionDrawingPolicy:在动画图中以不同颜色/粗细绘制连接线
  • FStateMachineConnectionDrawingPolicy:在状态机中绘制状态过渡连接线

特点:

  • 基于 FGraphPanelNodeFactory / FGraphPanelPinFactory / FGraphPanelPinConnectionFactory 的工厂模式
  • 每种动画节点类型都有对应的 Slate 控件(如 SGraphNodeSequencePlayer 显示序列缩略图)
  • 编译后自动刷新引脚属性缓存

5. 文档标签层 --- FBlendSpaceDocumentTabFactory

职责:

  • 在动画蓝图编辑器内部打开混合空间(BlendSpace)的编辑文档
  • 支持从动画图节点直接跳转到关联的混合空间进行编辑

特点:

  • 继承 FDocumentTabFactory,与蓝图编辑器的文档管理系统集成
  • 通过 TabPayload_BlendSpaceGraph 传递混合空间图引用

6. 配置与辅助层 --- UAnimationBlueprintEditorSettings / UAnimationBlueprintToolMenuContext

职责:

  • UAnimationBlueprintEditorSettings:编辑器用户偏好设置(姿态监视、图角标文本)
  • UAnimationBlueprintToolMenuContext:工具菜单上下文,用于在菜单回调中获取编辑器实例

2.4 数据流走向

编辑器初始化流程:

复制代码
1. FAnimationBlueprintEditorModule::CreateAnimationBlueprintEditor() 被调用
   ↓
2. 创建 FAnimationBlueprintEditor 实例
   ↓
3. InitAnimationBlueprintEditor() 开始初始化
   ↓
4. 通过 PersonaModule 创建 PersonaToolkit
   ↓
5. 设置预览场景的默认动画模式为 AnimationBlueprint
   ↓
6. 注册预览网格变更回调
   ↓
7. 创建骨骼树(SkeletonTree)
   ↓
8. 注册蓝图编译事件
   ↓
9. 创建默认命令和绑定
   ↓
10. 根据蓝图类型(标准/接口/模板)添加对应的编辑模式
   ↓
11. 设置预览动画蓝图实例
   ↓
12. PostLayoutBlueprintEditorInitialization() 完成布局初始化

节点选择变更流程:

复制代码
1. 用户在图中点击/框选节点
   ↓
2. FBlueprintEditor::OnSelectedNodesChangedImpl() 被调用
   ↓
3. FAnimationBlueprintEditor::OnSelectedNodesChangedImpl() 覆盖处理
   ↓
4. ReleaseAllManagedNodes() --- 通知所有旧选中节点取消选中
   ↓
5. 计算新旧选集的差异(SortedContainerDifference 算法)
   ↓
6. 更新 SelectedAnimGraphNodes 列表
   ↓
7. AcquireAllManagedNodes() --- 通知所有新选中节点被选中
   ↓
8. 如果开启了"隐藏无关节点",执行 HideUnrelatedNodes()
   ↓
9. 如果开启了"姿态监视选中节点",执行 HandlePoseWatchSelectedNodes()

编译流程:

复制代码
1. 用户触发编译(点击 Compile 按钮或快捷键)
   ↓
2. OnBlueprintPreCompile() --- 编译前处理
   ├─ 清理正在运行的 AnimNotifyState(避免编译时崩溃)
   ├─ 缓存当前调试对象(DebuggedMeshComponent)
   └─ 关闭使用 AnimScriptInstance 的其他编辑器
   ↓
3. 蓝图编译执行(引擎内部)
   ↓
4. OnBlueprintPostCompile() --- 编译后处理
   ├─ 重新初始化动画实例(如果被 GC 清空)
   ├─ 重新应用预览动画蓝图
   ├─ 重新设置调试对象
   ├─ 清除并重建选中节点列表
   └─ 调用 OnPostCompile() 同步节点数据到预览实例
   ↓
5. OnPostCompile() --- 后编译数据同步
   ├─ 重新触发节点选中回调
   └─ 遍历所有 AnimGraphNode,调用 CopyNodeDataToPreviewNode()

2.5 核心技术栈

技术/类 用途
FBlueprintEditor 蓝图编辑器基类,提供图编辑、编译、调试等通用功能
IPersonaToolkit Persona 工具集接口,提供预览场景、骨骼网格组件、动画蓝图访问
IPersonaPreviewScene 预览场景接口,管理预览世界、默认动画模式等
ISkeletonTree 骨骼树接口,提供骨骼/插槽的选择和显示
FWorkflowAllowedTabSet 工作流标签页集合,管理可生成的标签页
FTabManager / FDocumentTabFactory 标签页布局管理和文档标签工厂
FGraphPanelNodeFactory 图节点工厂,根据 UEdGraphNode 创建 Slate 控件
FGraphPanelPinFactory 图引脚工厂,根据 UEdGraphPin 创建 Slate 控件
FGraphPanelPinConnectionFactory 连接线工厂,根据 Schema 创建绘制策略
FKismetConnectionDrawingPolicy 蓝图连接线绘制策略基类
FExtensibilityManager 扩展性管理器,支持菜单和工具栏扩展
UPoseWatch 姿态监视对象,在预览视口中显示节点的骨骼姿态
UAnimGraphNode_Base 动画图节点基类,所有动画节点的 C++ 基类
FScopedTransaction 撤销/重做事务支持

2.6 架构设计优势

  1. 继承复用 :继承 FBlueprintEditor,复用了所有蓝图编辑的基础功能(图编辑、编译、调试、查找替换等),只专注于动画特有逻辑
  2. 模式分离 :三种编辑模式(标准/接口/模板)通过 FApplicationMode 机制实现,布局完全独立,互不干扰
  3. 工厂模式:Slate 可视化层通过工厂模式实现,新增节点类型只需在工厂中添加映射即可
  4. 扩展性强 :通过 FExtensibilityManager 和委托机制,外部模块可以扩展菜单、工具栏和调试对象
  5. 实时预览 :通过 CopyNodeDataToPreviewNode 机制,引脚默认值变更可以实时反映到预览视口中
  6. 安全的编译管道:编译前/后处理确保了 AnimNotifyState 的安全清理和调试对象的重连
  7. 姿态监视系统 :通过 UPoseWatch 和自动选中监视,提供了强大的动画调试能力

潜在改进点

  1. 代码耦合度FAnimationBlueprintEditor 承载了过多职责(节点管理、姿态监视、编译处理、类型转换等),可考虑将部分功能拆分为独立的子系统
  2. 节点转换代码重复OnConvertTo* 系列函数存在大量相似代码,可抽象为模板函数或策略模式
  3. 测试覆盖:模块中缺少自动化测试,编译管道和节点管理逻辑的回归测试较为重要
  4. 文档注释:部分 Slate 控件类缺少详细的文档注释

3 类级代码注释详解

3.1 IAnimationBlueprintEditor 接口

概述

IAnimationBlueprintEditor 是动画蓝图编辑器的公共接口,继承自 FBlueprintEditorIHasPersonaToolkit。它定义了动画蓝图编辑器特有的 API 契约。

核心设计理念:

  • 继承 FBlueprintEditor:复用蓝图编辑器的全部功能
  • 继承 IHasPersonaToolkit:提供 Persona 工具集的访问入口
  • 纯虚接口:所有方法都是纯虚函数,由 FAnimationBlueprintEditor 实现

完整接口定义:

cpp 复制代码
// IAnimationBlueprintEditor.h
class IAnimationBlueprintEditor : public FBlueprintEditor, public IHasPersonaToolkit
{
public:
    /** 获取上次使用的图引脚类型(用于创建新引脚时的默认类型) */
    virtual const FEdGraphPinType& GetLastGraphPinTypeUsed() const = 0;

    /** 设置上次使用的图引脚类型 */
    virtual void SetLastGraphPinTypeUsed(const FEdGraphPinType& InType) = 0;

    /** 获取编辑器内嵌的动画序列浏览器 */
    virtual IAnimationSequenceBrowser* GetAssetBrowser() const = 0;

    /** 获取预览动画实例(考虑链接层和链接动画图的情况) */
    virtual UAnimInstance* GetPreviewInstance() const = 0;
};

接口方法逐个分析:

  • GetLastGraphPinTypeUsed() / SetLastGraphPinTypeUsed():在新建图引脚时,编辑器需要知道上次用户选择的引脚类型以提供合理的默认值。这个信息由编辑器主体维护(存储在 LastGraphPinType 成员变量中),并在构造时初始化为 PC_Boolean
  • GetAssetBrowser():动画序列浏览器是一个内嵌在编辑器中的面板,用于浏览和筛选可用于动画图节点的动画序列。它通过弱引用(TWeakPtr<IAnimationSequenceBrowser>)持有,因为浏览器 Slate 控件可能在标签页切换时被销毁。
  • GetPreviewInstance():这个接口的实现比看上去复杂得多。它不是简单返回预览组件上的动画实例,而是需要考虑 Preview Animation Blueprint 的存在------当动画蓝图设置了预览动画蓝图时,取决于应用方式(LinkedLayersLinkedAnimGraph),真正的预览实例可能是链接层实例或链接动画图实例。

设计亮点:

  • 接口极简------只有 4 个纯虚方法,而通过继承 FBlueprintEditor 获得了近百个方法
  • GetPreviewInstance() 的实现处理了多层间接引用,详见后文 FAnimationBlueprintEditor 的分析

3.2 FAnimationBlueprintEditor 类

概述

FAnimationBlueprintEditor 是模块的核心实现类,继承自 IAnimationBlueprintEditor,是整个动画蓝图编辑器的"大脑"。它的 .cpp 文件约 2600 行代码,是模块中最大的单个文件。

3.2.1 构造函数与析构函数

FAnimationBlueprintEditor::FAnimationBlueprintEditor()

cpp 复制代码
FAnimationBlueprintEditor::FAnimationBlueprintEditor()
    : PersonaMeshDetailLayout(nullptr)
    , DebuggedMeshComponent(nullptr)
{
    GEditor->OnBlueprintPreCompile().AddRaw(this, &FAnimationBlueprintEditor::OnBlueprintPreCompile);
    LastGraphPinType.ResetToDefaults();
    LastGraphPinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
}

功能分析:

  • 注册到 GEditor->OnBlueprintPreCompile() 委托,以便在任何蓝图编译前执行清理逻辑。注意这里注册的是全局委托而非特定蓝图的委托------因为即使是其他蓝图的编译也可能影响当前打开编辑器中的 AnimNotifyState
  • 初始化 LastGraphPinType 为 Boolean 类型(新创建引脚时的默认类型)
  • DebuggedMeshComponent 初始化为 nullptr,只有在编译前才会被赋值为当前调试的骨骼网格组件

FAnimationBlueprintEditor::~FAnimationBlueprintEditor()

cpp 复制代码
FAnimationBlueprintEditor::~FAnimationBlueprintEditor()
{
    // 停止监听设置变更
    UAnimationBlueprintEditorSettings* AnimationBlueprintEditorSettings = GetMutableDefault<UAnimationBlueprintEditorSettings>();
    AnimationBlueprintEditorSettings->UnregisterOnUpdateSettings(AnimationBlueprintEditorSettingsChangedHandle);

    // 移除所有因选中而创建的临时姿态监视
    RemoveAllSelectionPoseWatches();

    // 取消委托注册
    GEditor->OnBlueprintPreCompile().RemoveAll(this);
    GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetPostImport.RemoveAll(this);
    FReimportManager::Instance()->OnPostReimport().RemoveAll(this);

    SaveEditorSettings();

    // 显式重置 PersonaToolkit 以强制销毁预览场景
    PersonaToolkit.Reset();

    // 清理 EditorModeManager 中的预览场景引用,避免 use-after-free
    FEditorModeTools& ModeTools = GetEditorModeManager();
    ((FAssetEditorModeManager&)ModeTools).SetPreviewScene(nullptr);

    FBlueprintCoreDelegates::OnScriptException.Remove(ScriptExceptionHandle);
}

功能分析(逐步骤解读):

  1. 取消设置监听 :调用 UnregisterOnUpdateSettings 移除 HandleUpdateSettings 回调
  2. 清理临时 PoseWatches :调用 RemoveAllSelectionPoseWatches() 移除所有 ShouldDeleteOnDeselect 为 true 的姿态监视
  3. 取消全局委托
    • OnBlueprintPreCompile().RemoveAll(this) --- 移除编译前回调
    • OnAssetPostImportOnPostReimport --- 移除资产重导入回调
  4. 保存编辑器设置 :将 bHideUnrelatedNodes 等状态写入 EditorOptions
  5. 销毁 PersonaToolkit :这是最关键也最容易被忽略的一步。PersonaToolkit.Reset() 会强制销毁预览场景(PreviewWorld),但 EditorModeManager 仍然持有对预览场景的引用。因此紧接着必须:
  6. 清理 EditorModeManager 引用SetPreviewScene(nullptr) 防止下次 PreviewWorld 清理时触发 EditorModeManager 中已失效的回调
  7. 移除脚本异常钩子 :清理 OnScriptException 委托

设计亮点:

  • 析构函数中的清理顺序非常讲究------先清理 PoseWatch,再取消委托注册,最后显式销毁 PersonaToolkit 和清理 EditorModeManager,确保不会出现悬空指针
  • SetPreviewScene(nullptr) 这一步注释中明确写了:"This avoids a memory after delete use",说明这是一个踩坑后的修复

3.2.2 初始化方法 --- InitAnimationBlueprintEditor()

这是编辑器初始化的核心方法,约 150 行代码,流程如下:

cpp 复制代码
void FAnimationBlueprintEditor::InitAnimationBlueprintEditor(
    const EToolkitMode::Type Mode,
    const TSharedPtr<class IToolkitHost>& InitToolkitHost,
    UAnimBlueprint* InAnimBlueprint)
{
    // 1. 记录新建状态并重置
    bool bNewlyCreated = InAnimBlueprint->bIsNewlyCreated;
    InAnimBlueprint->bIsNewlyCreated = false;

    // 2. 创建工具栏
    if (!Toolbar.IsValid())
    {
        Toolbar = MakeShareable(new FBlueprintEditorToolbar(SharedThis(this)));
    }

    // 3. 加载编辑器设置
    LoadEditorSettings();

    // 4. 创建 Persona 工具集
    FPersonaModule& PersonaModule = FModuleManager::GetModuleChecked<FPersonaModule>("Persona");
    PersonaToolkit = PersonaModule.CreatePersonaToolkit(InAnimBlueprint, FPersonaToolkitArgs());

    // 5. 配置预览场景
    PersonaToolkit->GetPreviewScene()->SetDefaultAnimationMode(
        EPreviewSceneDefaultAnimationMode::AnimationBlueprint);
    PersonaToolkit->GetPreviewScene()->RegisterOnPreviewMeshChanged(
        FOnPreviewMeshChanged::CreateSP(this, &FAnimationBlueprintEditor::HandlePreviewMeshChanged));

    // 6. 为预览场景设置自定义描述(用于调试对象标签)
    if (FWorldContext* WorldContext = GEngine->GetWorldContextFromWorld(
        PersonaToolkit->GetPreviewScene()->GetWorld()))
    {
        static constexpr TCHAR Format[] = TEXT("AnimBPEditor (%s)");
        WorldContext->CustomDescription = FString::Printf(Format, *InAnimBlueprint->GetName());
    }

    // 7. 创建骨骼树
    if (InAnimBlueprint->BlueprintType != BPTYPE_Interface && !InAnimBlueprint->bIsTemplate)
    {
        FSkeletonTreeArgs SkeletonTreeArgs;
        SkeletonTreeArgs.OnSelectionChanged = FOnSkeletonTreeSelectionChanged::CreateSP(
            this, &FAnimationBlueprintEditor::HandleSelectionChanged);
        SkeletonTreeArgs.PreviewScene = GetPreviewScene();
        SkeletonTreeArgs.ContextName = GetToolkitFName();

        ISkeletonEditorModule& SkeletonEditorModule =
            FModuleManager::LoadModuleChecked<ISkeletonEditorModule>("SkeletonEditor");
        SkeletonTree = SkeletonEditorModule.CreateSkeletonTree(
            PersonaToolkit->GetSkeleton(), SkeletonTreeArgs);
    }

    // 8. 注册编译事件
    InAnimBlueprint->OnCompiled().AddSP(this, &FAnimationBlueprintEditor::OnBlueprintPostCompile);

    // 9. 创建命令、绑定、菜单
    CreateDefaultCommands();
    BindCommands();
    RegisterMenus();

    // 10. 初始化资产编辑器
    InitAssetEditor(Mode, InitToolkitHost, AnimationBlueprintEditorAppName,
        FTabManager::FLayout::NullLayout, true, true, ObjectsBeingEdited);

    // 11. 根据蓝图类型选择编辑模式
    if (InAnimBlueprint->BlueprintType == BPTYPE_Interface)
    {
        // 接口模式
        AddApplicationMode(
            FAnimationBlueprintEditorModes::AnimationBlueprintInterfaceEditorMode,
            MakeShareable(new FAnimationBlueprintInterfaceEditorMode(SharedThis(this))));
        SetCurrentMode(FAnimationBlueprintEditorModes::AnimationBlueprintInterfaceEditorMode);
    }
    else if (InAnimBlueprint->bIsTemplate)
    {
        // 模板模式
        AddApplicationMode(
            FAnimationBlueprintEditorModes::AnimationBlueprintTemplateEditorMode,
            MakeShareable(new FAnimationBlueprintEditorMode(SharedThis(this))));
        SetCurrentMode(FAnimationBlueprintEditorModes::AnimationBlueprintTemplateEditorMode);
    }
    else
    {
        // 标准模式
        AddApplicationMode(
            FAnimationBlueprintEditorModes::AnimationBlueprintEditorMode,
            MakeShareable(new FAnimationBlueprintEditorMode(SharedThis(this))));
        SetCurrentMode(FAnimationBlueprintEditorModes::AnimationBlueprintEditorMode);
    }

    // 12. 设置预览动画蓝图
    if (bHasBlueprintPreview)
    {
        UAnimBlueprint* PreviewAnimBlueprint = AnimBlueprint->GetPreviewAnimationBlueprint();
        if (PreviewAnimBlueprint)
        {
            PersonaToolkit->GetPreviewScene()->SetPreviewAnimationBlueprint(
                PreviewAnimBlueprint, AnimBlueprint);
            PreviewAnimBlueprint->OnCompiled().AddSP(
                this, &FAnimationBlueprintEditor::HandlePreviewAnimBlueprintCompiled);
        }
        else
        {
            PersonaToolkit->GetPreviewScene()->SetPreviewAnimationBlueprint(
                AnimBlueprint, nullptr);
        }
        PersonaUtils::SetObjectBeingDebugged(AnimBlueprint,
            PreviewMeshComponent->GetAnimInstance());
    }

    // 13. 布局后初始化
    PostLayoutBlueprintEditorInitialization();

    // 14. 对新建接口蓝图,自动打开动画层图
    if (bNewlyCreated && InAnimBlueprint->BlueprintType == BPTYPE_Interface)
    {
        NewDocument_OnClick(CGT_NewAnimationLayer);
    }

    // 15. 注册设置变更通知
    AnimationBlueprintEditorSettingsChangedHandle =
        GetMutableDefault<UAnimationBlueprintEditorSettings>()->RegisterOnUpdateSettings(
            UAnimationBlueprintEditorSettings::FOnUpdateSettingsMulticaster::FDelegate::CreateSP(
                this, &FAnimationBlueprintEditor::HandleUpdateSettings));

    // 16. 禁用预览场景中的网格点击代理
    PersonaToolkit->GetPreviewScene()->SetAllowMeshHitProxies(false);

    // 17. 注册脚本异常钩子
    ScriptExceptionHandle = FBlueprintCoreDelegates::OnScriptException.AddSP(
        this, &FAnimationBlueprintEditor::HandleScriptException);
}

关键设计:

  • 对接口蓝图,新建时自动打开一个新的动画层图(CGT_NewAnimationLayer),这是一个人性化的默认行为
  • 对预览场景,设置 SetAllowMeshHitProxies(false) 避免在预览视口中误选网格骨骼
  • 模式类型通过 FAnimationBlueprintEditorModes 命名空间中的常量定义,三套模式使用同一套控件名但不同布局

3.2.3 节点选择管理 --- OnSelectedNodesChangedImpl()

这是节点选择变更的核心处理函数,也是理解编辑器如何管理"选中状态"的关键:

cpp 复制代码
void FAnimationBlueprintEditor::OnSelectedNodesChangedImpl(const TSet<class UObject*>& NewSelection)
{
    FBlueprintEditor::OnSelectedNodesChangedImpl(NewSelection);

    IPersonaEditorModeManager* const PersonaEditorModeManager =
        static_cast<IPersonaEditorModeManager*>(&GetEditorModeManager());

    if (PersonaEditorModeManager)
    {
        // 1. 构建新旧选集的排序版本
        TArray<FSelectedNodePtr> AddSelection;   // 新加入选集的节点
        TArray<FSelectedNodePtr> RemSelection;   // 从选集中移除的节点

        {
            TArray<FSelectedNodePtr> OldSelectionSorted(SelectedAnimGraphNodes);
            TArray<FSelectedNodePtr> NewSelectionSorted;

            for (UObject* NewSelectedObject : NewSelection)
            {
                if (UAnimGraphNode_Base* NewSelectedAnimGraphNode =
                    Cast<UAnimGraphNode_Base>(NewSelectedObject))
                {
                    NewSelectionSorted.Add(NewSelectedAnimGraphNode);
                }
            }

            auto SortPredicate = [](const FSelectedNodePtr& Lhs, const FSelectedNodePtr& Rhs)
            {
                return Lhs.Get() < Rhs.Get();
            };

            OldSelectionSorted.Sort(SortPredicate);
            NewSelectionSorted.Sort(SortPredicate);

            SortedContainerDifference(OldSelectionSorted, NewSelectionSorted,
                AddSelection, RemSelection, SortPredicate);
        }

        // 2. 释放旧选择(通知所有旧选中节点取消选中)
        ReleaseAllManagedNodes();

        // 3. 移除不再选中的节点
        for (FSelectedNodePtr CurrentAnimGraphNode : RemSelection)
        {
            SelectedAnimGraphNodes.Remove(CurrentAnimGraphNode);
        }

        // 4. 添加新选中的节点
        for (FSelectedNodePtr CurrentAnimGraphNode : AddSelection)
        {
            SelectedAnimGraphNodes.Add(CurrentAnimGraphNode);
        }

        // 5. 获取新选择(通知所有新选中节点被选中)
        AcquireAllManagedNodes();
    }

    // 6. 隐藏无关节点(如果启用)
    if (bHideUnrelatedNodes && !bLockNodeFadeState)
    {
        ResetAllNodesUnrelatedStates();
        if (bSelectRegularNode)
        {
            HideUnrelatedNodes();
        }
    }

    // 7. 姿态监视选中节点(如果启用)
    if (GetDefault<UAnimationBlueprintEditorSettings>()->bPoseWatchSelectedNodes)
    {
        HandlePoseWatchSelectedNodes();
    }
}

SortedContainerDifference 算法:

这是一个高效的集合差异算法,专门用于处理已排序的唯一值集合:

cpp 复制代码
template <typename TContainerType, typename TPredicate>
void SortedContainerDifference(
    const TContainerType& LhsContainer, const TContainerType& RhsContainer,
    TContainerType& OutLhsDifference, TContainerType& OutRhsDifference,
    const TPredicate& SortPredicate)
{
    for (unsigned int LhsIndex = 0, RhsIndex = 0,
         LhsMax = LhsContainer.Num(), RhsMax = RhsContainer.Num();
         (LhsIndex < LhsMax) || (RhsIndex < RhsMax); )
    {
        if ((LhsIndex < LhsMax) && (!(RhsIndex < RhsMax) ||
            SortPredicate(LhsContainer[LhsIndex], RhsContainer[RhsIndex])))
        {
            OutRhsDifference.Add(LhsContainer[LhsIndex]);
            ++LhsIndex;
        }
        else if ((RhsIndex < RhsMax) && (!(LhsIndex < LhsMax) ||
            SortPredicate(RhsContainer[RhsIndex], LhsContainer[LhsIndex])))
        {
            OutLhsDifference.Add(RhsContainer[RhsIndex]);
            ++RhsIndex;
        }
        else
        {
            ++LhsIndex;
            ++RhsIndex;
        }
    }
}

这个算法的核心思路类似归并排序的合并步骤------同时遍历两个有序数组,比较指针位置的元素大小,决定哪个元素应该在差异集中。时间复杂度 O(n+m),空间复杂度 O(1)(不计输出)。

ReleaseAllManagedNodes / AcquireAllManagedNodes 机制:

cpp 复制代码
void FAnimationBlueprintEditor::ReleaseAllManagedNodes()
{
    NotifyAllNodesOnPoseWatchChanged(false);  // 通知所有节点取消姿态监视
    NotifyAllNodesOnSelection(false);          // 通知所有节点取消选中
}

void FAnimationBlueprintEditor::AcquireAllManagedNodes()
{
    NotifyAllNodesOnPoseWatchChanged(true);   // 通知所有节点恢复姿态监视
    NotifyAllNodesOnSelection(true);           // 通知所有节点恢复选中
}

这两个函数确保在节点选择状态变更时,所有受影响的节点都能收到正确的选中/取消选中通知。OnNodeSelectedOnPoseWatchChangedUAnimGraphNode_Base 的虚函数,由各子类实现具体的选中高亮、预览更新等逻辑。

NotifyAllNodesOnSelection 的实现:

cpp 复制代码
void FAnimationBlueprintEditor::NotifyAllNodesOnSelection(const bool bInIsSelected)
{
    FEditorModeTools& ModeTools = GetEditorModeManager();
    for (TWeakObjectPtr<class UAnimGraphNode_Base> CurrentAnimGraphNode : SelectedAnimGraphNodes)
    {
        UAnimGraphNode_Base* const CurrentAnimGraphNodePtr = CurrentAnimGraphNode.Get();
        FAnimNode_Base* const PreviewNode = FindAnimNode(CurrentAnimGraphNodePtr);
        // Note: 当 bInIsSelected 为 false 时传入 null PreviewNode 是必要的
        // 因为节点可能已不存在,但仍需要取消选中状态
        if (CurrentAnimGraphNodePtr && (!bInIsSelected || PreviewNode))
        {
            CurrentAnimGraphNodePtr->OnNodeSelected(bInIsSelected, ModeTools, PreviewNode);
        }
    }
}

设计亮点:

  • SortedContainerDifference 是一个经典的双指针算法,清晰高效
  • 通过 ReleaseAllManagedNodes / AcquireAllManagedNodes 的配对调用,确保节点状态的一致性
  • 维护 SelectedAnimGraphNodes 列表的顺序,这对 UI 的显示顺序很重要

3.2.4 编译管道

编译管道是编辑器中最重要的安全机制之一,它确保在蓝图重编译时不会因为类被替换或 UFunction 被清理而崩溃。

OnBlueprintPreCompile() --- 编译前处理:

cpp 复制代码
void FAnimationBlueprintEditor::OnBlueprintPreCompile(UBlueprint* BlueprintToCompile)
{
    if (PersonaToolkit.IsValid())
    {
        UDebugSkelMeshComponent* PreviewMeshComponent =
            PersonaToolkit->GetPreviewMeshComponent();
        if (PreviewMeshComponent && PreviewMeshComponent->PreviewInstance)
        {
            UAnimPreviewInstance* Instance = PreviewMeshComponent->PreviewInstance;
            USkeletalMeshComponent* SkelMeshComp = Instance->GetSkelMeshComponent();

            // 逆序遍历,安全地在遍历中删除元素
            for (int32 Idx = Instance->ActiveAnimNotifyState.Num() - 1; Idx >= 0; --Idx)
            {
                FAnimNotifyEvent& Event = Instance->ActiveAnimNotifyState[Idx];
                const FAnimNotifyEventReference& EventReference =
                    Instance->ActiveAnimNotifyEventReference[Idx];
                // 只清理属于即将编译的类的 NotifyState
                if (Event.NotifyStateClass->GetClass() == BlueprintToCompile->GeneratedClass)
                {
                    Event.NotifyStateClass->NotifyEnd(SkelMeshComp,
                        Cast<UAnimSequenceBase>(Event.NotifyStateClass->GetOuter()),
                        EventReference);
                    Instance->ActiveAnimNotifyState.RemoveAt(Idx);
                    Instance->ActiveAnimNotifyEventReference.RemoveAt(Idx);
                }
            }
        }
    }

    // 缓存调试对象
    if (GetObjectsCurrentlyBeingEdited()->Num() > 0 &&
        BlueprintToCompile == GetBlueprintObj())
    {
        DebuggedMeshComponent = nullptr;
        UAnimInstance* CurrentDebugObject =
            Cast<UAnimInstance>(BlueprintToCompile->GetObjectBeingDebugged());
        if (CurrentDebugObject)
        {
            // 强制关闭使用 AnimScriptInstance 的其他编辑器
            GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->CloseOtherEditors(
                CurrentDebugObject, nullptr);
            DebuggedMeshComponent = CurrentDebugObject->GetSkelMeshComponent();
        }
    }
}

关键安全措施:

  1. 逆序遍历 ActiveAnimNotifyState :使用 Idx-- 从尾到头遍历,这样当调用 RemoveAt(Idx) 时不会影响尚未遍历到的元素的索引
  2. NotifyEnd 调用 :在类的 UFunction 被 GC 清理之前,先调用 NotifyEnd() 结束正在运行的 NotifyState。如果不这么做,编译后类的 UFunction 指针可能已经失效,NotifyEnd 调用将导致崩溃
  3. 关闭其他编辑器CloseOtherEditors 确保没有其他编辑器(如属性矩阵)在使用即将失效的 AnimScriptInstance

OnBlueprintPostCompile() --- 编译后处理:

cpp 复制代码
void FAnimationBlueprintEditor::OnBlueprintPostCompile(UBlueprint* InBlueprint)
{
    if (InBlueprint == GetBlueprintObj())
    {
        if (DebuggedMeshComponent != nullptr)
        {
            // 如果动画实例被 GC 清空,重新初始化
            if (DebuggedMeshComponent->GetAnimInstance() == nullptr)
            {
                DebuggedMeshComponent->InitAnim(true);
            }

            // 重新应用预览动画蓝图
            UAnimBlueprint* AnimBlueprint = GetAnimBlueprint();
            UAnimBlueprint* PreviewAnimBlueprint =
                AnimBlueprint ? AnimBlueprint->GetPreviewAnimationBlueprint() : nullptr;
            if (PreviewAnimBlueprint)
            {
                PersonaToolkit->GetPreviewScene()->SetPreviewAnimationBlueprint(
                    PreviewAnimBlueprint, AnimBlueprint);
            }

            // 重新设置调试对象
            if (UAnimInstance* NewInstance = DebuggedMeshComponent->GetAnimInstance())
            {
                if ((AnimBlueprint && NewInstance->IsA(AnimBlueprint->GeneratedClass)) ||
                    (PreviewAnimBlueprint && NewInstance->IsA(PreviewAnimBlueprint->GeneratedClass)))
                {
                    PersonaUtils::SetObjectBeingDebugged(AnimBlueprint, NewInstance);
                }
            }
        }

        // 清除选中节点并同步数据
        ClearSelectedAnimGraphNodes();
        OnPostCompile();

        // 不再缓存调试组件
        DebuggedMeshComponent = nullptr;
    }

    // 确保 Tick 被重新启用(编译前可能因无限循环被禁用)
    if (USkeletalMeshComponent* SkeletalMeshComponent =
        GetPreviewScene()->GetPreviewMeshComponent())
    {
        SkeletalMeshComponent->SetComponentTickEnabled(true);
    }
}

OnPostCompile() --- 后编译数据同步:

cpp 复制代码
void FAnimationBlueprintEditor::OnPostCompile()
{
    // 如果当前 UI 选择状态是图选择,重新触发节点选中
    if (CurrentUISelection == FBlueprintEditor::SelectionState_Graph)
    {
        FGraphPanelSelectionSet SelectionSet = GetSelectedNodes();
        OnSelectedNodesChangedImpl(SelectionSet);
        FocusInspectorOnGraphSelection(SelectionSet, /*bForceRefresh=*/ true);
    }

    // 遍历所有 AnimGraphNode,同步编辑数据到预览实例
    UEdGraph* FocusedGraph = GetFocusedGraph();
    if (FocusedGraph)
    {
        for (UEdGraphNode* Node : FocusedGraph->Nodes)
        {
            UAnimGraphNode_Base* AnimGraphNode = Cast<UAnimGraphNode_Base>(Node);
            if (AnimGraphNode)
            {
                FAnimNode_Base* AnimNode = FindAnimNode(AnimGraphNode);
                if (AnimNode)
                {
                    AnimGraphNode->CopyNodeDataToPreviewNode(AnimNode);
                }
            }
        }
    }
}

编译管道总结:

编译管道的三层结构(PreCompile → 引擎编译 → PostCompile → PostCompileDataSync)确保了:

  • 编译前:安全地结束所有正在运行的 AnimNotifyState,缓存调试对象
  • 编译后:重新初始化动画实例(GC 后),恢复调试状态
  • 数据同步:将编辑器中修改的节点数据复制到运行时预览实例

3.2.5 姿态监视系统

姿态监视(Pose Watch)是动画蓝图编辑器中最实用的调试功能之一------它可以在预览视口中可视化地显示特定节点的骨骼姿态。

HandlePoseWatchSelectedNodes() --- 自动监视选中节点:

cpp 复制代码
void FAnimationBlueprintEditor::HandlePoseWatchSelectedNodes()
{
    TSharedPtr<SGraphEditor> FocusedGraphEd = FocusedGraphEdPtr.Pin();
    if (FocusedGraphEd.IsValid())
    {
        ReleaseAllManagedNodes();

        UAnimBlueprint* AnimBP = GetAnimBlueprint();
        TArray<UEdGraphNode*> AllNodes = FocusedGraphEd->GetCurrentGraph()->Nodes;
        FGraphPanelSelectionSet SelectionNodes = GetSelectedNodes();

        for (UEdGraphNode* Node : AllNodes)
        {
            UAnimGraphNode_Base* GraphNode = Cast<UAnimGraphNode_Base>(Node);
            UPoseWatch* PoseWatch = AnimationEditorUtils::FindPoseWatchForNode(GraphNode, AnimBP);
            if (GraphNode)
            {
                if (SelectionNodes.Contains(Node))
                {
                    // 选中的节点:如果没有 PoseWatch 且可监视 → 创建临时 PoseWatch
                    if (!PoseWatch && GraphNode->IsPoseWatchable())
                    {
                        PoseWatch = AnimationEditorUtils::MakePoseWatchForNode(AnimBP, GraphNode);
                        PoseWatch->SetShouldDeleteOnDeselect(true);
                    }
                }
                else
                {
                    // 未选中的节点:如果 PoseWatch 是临时的 → 删除
                    if (PoseWatch && PoseWatch->GetShouldDeleteOnDeselect())
                    {
                        AnimationEditorUtils::RemovePoseWatch(PoseWatch, AnimBP);
                    }
                }
            }
        }

        AcquireAllManagedNodes();
    }
}

核心逻辑:

  1. 遍历当前图中所有节点
  2. 选中的、可监视的节点 → 创建临时 UPoseWatch(标记 ShouldDeleteOnDeselect
  3. 未选中的、临时创建的 UPoseWatch → 自动删除
  4. 永久创建的 UPoseWatch(用户手动添加的)不受影响

OnTogglePoseWatch() --- 手动切换姿态监视:

cpp 复制代码
void FAnimationBlueprintEditor::OnTogglePoseWatch()
{
    ReleaseAllManagedNodes();

    const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes();
    UAnimBlueprint* AnimBP = GetAnimBlueprint();

    // 不能在子动画蓝图中切换姿态监视
    if (UAnimBlueprint::FindRootAnimBlueprint(AnimBP))
    {
        return;
    }

    for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt)
    {
        if (UAnimGraphNode_Base* SelectedNode = Cast<UAnimGraphNode_Base>(*NodeIt))
        {
            UPoseWatch* ExistingPoseWatch =
                AnimationEditorUtils::FindPoseWatchForNode(SelectedNode, AnimBP);
            if (ExistingPoseWatch)
            {
                if (ExistingPoseWatch->GetShouldDeleteOnDeselect())
                {
                    // 临时 → 提升为永久
                    ExistingPoseWatch->SetShouldDeleteOnDeselect(false);
                }
                else if (GetDefault<UAnimationBlueprintEditorSettings>()->bPoseWatchSelectedNodes)
                {
                    // 永久 → 降级为临时
                    ExistingPoseWatch->SetShouldDeleteOnDeselect(true);
                }
                else
                {
                    // 永久 → 删除
                    AnimationEditorUtils::RemovePoseWatch(ExistingPoseWatch, AnimBP);
                }
            }
            else if (SelectedNode->IsPoseWatchable())
            {
                // 无 PoseWatch → 创建新的永久 PoseWatch
                UPoseWatch* NewPoseWatch =
                    AnimationEditorUtils::MakePoseWatchForNode(AnimBP, SelectedNode);
            }
        }
    }

    AcquireAllManagedNodes();
}

姿态监视的三种状态切换逻辑:

当前状态 下一步
无 PoseWatch 创建永久 PoseWatch
临时 PoseWatch 提升为永久(SetShouldDeleteOnDeselect(false)
永久 PoseWatch + 自动监视开启 降级为临时(SetShouldDeleteOnDeselect(true)
永久 PoseWatch + 自动监视关闭 删除

3.2.6 节点类型转换

编辑器提供了一系列节点类型转换函数,允许用户在动画图中将一个节点类型转换为另一个相关类型。所有转换函数遵循统一的模板:

复制代码
1. 获取当前选中的节点集
2. 创建 FScopedTransaction(支持撤销)
3. 对每个选中节点:
   a. 检查类型是否匹配
   b. 创建新节点(FGraphNodeCreator)
   c. 复制通用状态(FEdGraphUtilities::CopyCommonState)
   d. 复制引脚数据(CopyPinData)
   e. 从图中移除旧节点
4. 刷新图显示
5. 标记蓝图为结构修改

SequencePlayer → SequenceEvaluator 转换为例:

cpp 复制代码
void FAnimationBlueprintEditor::OnConvertToSequenceEvaluator()
{
    FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes();
    if (SelectedNodes.Num() > 0)
    {
        const FScopedTransaction Transaction(
            LOCTEXT("ConvertToSequenceEvaluator", "Convert to Single Frame Animation"));

        for (auto NodeIter = SelectedNodes.CreateIterator(); NodeIter; ++NodeIter)
        {
            UAnimGraphNode_SequencePlayer* OldNode =
                Cast<UAnimGraphNode_SequencePlayer>(*NodeIter);

            if (OldNode && OldNode->Node.GetSequence())
            {
                UEdGraph* TargetGraph = OldNode->GetGraph();
                TargetGraph->Modify();
                OldNode->Modify();

                // 创建新节点
                FGraphNodeCreator<UAnimGraphNode_SequenceEvaluator> NodeCreator(*TargetGraph);
                UAnimGraphNode_SequenceEvaluator* NewNode = NodeCreator.CreateNode();
                NewNode->Node.SetSequence(OldNode->Node.GetSequence());
                NodeCreator.Finalize();

                // 复制状态和引脚数据
                FEdGraphUtilities::CopyCommonState(OldNode, NewNode);
                CopyPinData(OldNode, NewNode, TEXT("Pose"));

                // 移除旧节点
                NodeIter.RemoveCurrent();
                TargetGraph->RemoveNode(OldNode);
            }
        }

        TSharedPtr<SGraphEditor> FocusedGraphEd = FocusedGraphEdPtr.Pin();
        FocusedGraphEd->NotifyGraphChanged();
        FocusedGraphEd->ClearSelectionSet();

        FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetAnimBlueprint());
    }
}

CopyPinData 辅助函数:

cpp 复制代码
static void CopyPinData(UEdGraphNode* InOldNode, UEdGraphNode* InNewNode, const TCHAR* InPinName)
{
    UEdGraphPin* OldPin = InOldNode->FindPin(InPinName);
    UEdGraphPin* NewPin = InNewNode->FindPin(InPinName);
    if (ensure(OldPin && NewPin))
    {
        NewPin->MovePersistentDataFromOldPin(*OldPin);
    }
}

MovePersistentDataFromOldPin 会将老引脚上的连接关系、默认值等持久数据迁移到新引脚上。

支持的转换方向:

源类型 目标类型
SequencePlayer SequenceEvaluator
SequenceEvaluator SequencePlayer
BlendSpacePlayer BlendSpaceEvaluator
BlendSpaceEvaluator BlendSpacePlayer
BlendSpacePlayer BlendSpaceGraph
RotationOffsetBlendSpace AimOffsetLookAt
AimOffsetLookAt RotationOffsetBlendSpace
RotationOffsetBlendSpace RotationOffsetBlendSpaceGraph
PoseBlendNode PoseByName
PoseByName PoseBlendNode

设计亮点:

  • 使用 FScopedTransaction 确保所有转换都是可撤销的
  • CopyPinData 辅助函数通过引脚名称匹配来迁移数据,保持连接关系
  • 每种转换都是一个独立的函数,虽然代码有重复,但职责清晰,方便查找和修改

3.2.7 实时预览同步

HandlePinDefaultValueChanged():

当动画图中的引脚默认值发生变化时,此函数将变更实时传播到预览实例:

cpp 复制代码
void FAnimationBlueprintEditor::HandlePinDefaultValueChanged(UEdGraphPin* InPinThatChanged)
{
    UAnimGraphNode_Base* AnimGraphNode =
        Cast<UAnimGraphNode_Base>(InPinThatChanged->GetOwningNode());
    if (AnimGraphNode)
    {
        FAnimNode_Base* AnimNode = FindAnimNode(AnimGraphNode);
        if (AnimNode)
        {
            AnimGraphNode->CopyNodeDataToPreviewNode(AnimNode);
        }
    }
}

绑定时机: 此回调在 OnGraphEditorFocused 中绑定到 UAnimationGraph::OnPinDefaultValueChanged,在 OnGraphEditorBackgrounded 中解绑:

cpp 复制代码
void FAnimationBlueprintEditor::OnGraphEditorFocused(const TSharedRef<SGraphEditor>& InGraphEditor)
{
    // 先清理旧的姿态监视
    if (GetDefault<UAnimationBlueprintEditorSettings>()->bPoseWatchSelectedNodes)
    {
        RemoveAllSelectionPoseWatches();
    }

    FBlueprintEditor::OnGraphEditorFocused(InGraphEditor);

    // 绑定引脚值变更回调
    UAnimationGraph* AnimationGraph = Cast<UAnimationGraph>(InGraphEditor->GetCurrentGraph());
    if (AnimationGraph)
    {
        OnPinDefaultValueChangedHandle = AnimationGraph->OnPinDefaultValueChanged.Add(
            FOnPinDefaultValueChanged::FDelegate::CreateSP(
                this, &FAnimationBlueprintEditor::HandlePinDefaultValueChanged));
    }

    // 重新应用姿态监视
    if (GetDefault<UAnimationBlueprintEditorSettings>()->bPoseWatchSelectedNodes)
    {
        HandlePoseWatchSelectedNodes();
    }
}

NotifyPostChange():

当属性面板中的属性发生变化时,遍历所有选中的动画图节点,调用 CopyNodeDataToPreviewNode() 同步到预览实例:

cpp 复制代码
void FAnimationBlueprintEditor::NotifyPostChange(
    const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged)
{
    FBlueprintEditor::NotifyPostChange(PropertyChangedEvent, PropertyThatChanged);

    // 将属性变更推送到预览实例
    for (TWeakObjectPtr<class UAnimGraphNode_Base> CurrentAnimGraphNode : SelectedAnimGraphNodes)
    {
        if (UAnimGraphNode_Base* CurrentNode = CurrentAnimGraphNode.Get())
        {
            if (FAnimNode_Base* PreviewNode = FindAnimNode(CurrentNode))
            {
                CurrentNode->CopyNodeDataToPreviewNode(PreviewNode);
            }
        }
    }
}

FindAnimNode():

在运行时动画实例中查找与编辑器中 UAnimGraphNode_Base 对应的 FAnimNode_Base

cpp 复制代码
FAnimNode_Base* FAnimationBlueprintEditor::FindAnimNode(
    UAnimGraphNode_Base* AnimGraphNode) const
{
    FAnimNode_Base* AnimNode = nullptr;
    if (AnimGraphNode)
    {
        // 优先使用调试对象中的骨骼网格组件
        USkeletalMeshComponent* SkeletalMeshComponentToUse = nullptr;
        if (UAnimInstance* AnimInstance =
            Cast<UAnimInstance>(GetAnimBlueprint()->GetObjectBeingDebugged()))
        {
            SkeletalMeshComponentToUse = AnimInstance->GetSkelMeshComponent();
        }
        else
        {
            SkeletalMeshComponentToUse = GetPreviewScene()->GetPreviewMeshComponent();
        }

        if (SkeletalMeshComponentToUse != nullptr &&
            SkeletalMeshComponentToUse->GetAnimInstance() != nullptr)
        {
            AnimNode = AnimGraphNode->FindDebugAnimNode(SkeletalMeshComponentToUse);
        }
    }
    return AnimNode;
}

3.2.8 调试对象管理

HandleSetObjectBeingDebugged():

当调试对象变更时,编辑器需要调整预览视口的行为:

cpp 复制代码
void FAnimationBlueprintEditor::HandleSetObjectBeingDebugged(UObject* InObject)
{
    FBlueprintEditor::HandleSetObjectBeingDebugged(InObject);

    // 重新触发节点选中以更新内部指针
    if (CurrentUISelection == FBlueprintEditor::SelectionState_Graph)
    {
        FGraphPanelSelectionSet SelectionSet = GetSelectedNodes();
        OnSelectedNodesChangedImpl(SelectionSet);
    }

    if (UAnimInstance* AnimInstance = Cast<UAnimInstance>(InObject))
    {
        USkeletalMeshComponent* SkeletalMeshComponent = AnimInstance->GetSkelMeshComponent();
        if (SkeletalMeshComponent)
        {
            if (InObject->GetWorld()->IsPreviewWorld())
            {
                // 预览世界中的实例 → 恢复默认预览模式
                GetPreviewScene()->ShowDefaultMode();
                // 清除 CopyPose 引用
            }
            else
            {
                // 游戏世界中的实例 → 使用 CopyPose 在预览视口中显示
                GetPreviewScene()->GetPreviewMeshComponent()->EnablePreview(true, nullptr);
                // 设置调试骨骼网格组件(CopyPose 模式)
                GetPreviewScene()->GetPreviewMeshComponent()->PreviewInstance
                    ->SetDebugSkeletalMeshComponent(SkeletalMeshComponent);
            }
        }
    }
    else
    {
        // 非 AnimInstance 调试对象 → 清除 CopyPose 并恢复默认模式
        GetPreviewScene()->ShowDefaultMode();
    }
}

HandleScriptException():

钩入蓝图异常处理,当检测到预览实例中的无限循环时,自动禁用骨骼网格组件的 Tick:

cpp 复制代码
void FAnimationBlueprintEditor::HandleScriptException(
    const UObject* InObject, const FFrame& InFrame, const FBlueprintExceptionInfo& InInfo)
{
    if (InInfo.GetType() == EBlueprintExceptionType::InfiniteLoop)
    {
        if (InObject && InObject->IsA<UAnimInstance>())
        {
            UWorld* ObjectWorld = InObject->GetWorld();
            TSharedRef<IPersonaPreviewScene> ThisPreviewScene = GetPreviewScene();
            if (ObjectWorld == ThisPreviewScene->GetWorld())
            {
                if (USkeletalMeshComponent* SkeletalMeshComponent =
                    ThisPreviewScene->GetPreviewMeshComponent())
                {
                    SkeletalMeshComponent->SetComponentTickEnabled(false);
                }
            }
        }
    }
}

设计亮点:

  • 无限循环保护机制非常实用------动画蓝图编辑器中的逻辑错误可能导致无限递归,如果不禁用 Tick,编辑器会卡死
  • SetComponentTickEnabled(false) 后,编译成功时会自动在 OnBlueprintPostCompile 中重新启用

3.2.9 视口通知

HandleViewportCreated():

当预览视口创建时,添加两个关键通知:

  1. 编译状态通知:根据蓝图状态显示不同的图标和文字:

    • BS_UpToDate / BS_UpToDateWithWarnings:折叠(不可见)
    • BS_Dirty:显示"Preview out of date" + "Compile"按钮
    • BS_Error:显示"Compile Error" + 感叹号三角形图标
  2. 无限循环通知:当检测到无限循环(Tick 被禁用)时,显示红色警告:"Infinite Loop Detected"

这两个通知是通过 IPersonaViewport::AddNotification() 添加到预览视口中的,参数包括:

  • 严重程度(Error/Warning)
  • 是否可关闭
  • Slate 控件内容
  • 可见性控制属性

3.2.10 GetPreviewInstance() --- 多层间接引用处理

cpp 复制代码
UAnimInstance* FAnimationBlueprintEditor::GetPreviewInstance() const
{
    UDebugSkelMeshComponent* PreviewMeshComponent = PersonaToolkit->GetPreviewMeshComponent();
    if (PreviewMeshComponent->IsAnimBlueprintInstanced())
    {
        UAnimInstance* PreviewInstance = PreviewMeshComponent->GetAnimInstance();
        UAnimBlueprint* AnimBlueprint = GetAnimBlueprint();
        UAnimBlueprint* PreviewAnimBlueprint = AnimBlueprint->GetPreviewAnimationBlueprint();
        if (PreviewAnimBlueprint)
        {
            EPreviewAnimationBlueprintApplicationMethod ApplicationMethod =
                AnimBlueprint->GetPreviewAnimationBlueprintApplicationMethod();
            if (ApplicationMethod == EPreviewAnimationBlueprintApplicationMethod::LinkedLayers)
            {
                PreviewInstance = PreviewInstance->GetLinkedAnimLayerInstanceByClass(
                    AnimBlueprint->GeneratedClass.Get());
            }
            else if (ApplicationMethod == EPreviewAnimationBlueprintApplicationMethod::LinkedAnimGraph)
            {
                PreviewInstance = PreviewInstance->GetLinkedAnimGraphInstanceByTag(
                    AnimBlueprint->GetPreviewAnimationBlueprintTag());
            }
        }
        return PreviewInstance;
    }
    return nullptr;
}

这个方法体现了 Preview Animation Blueprint 机制的复杂性------当动画蓝图设置了 Preview Animation Blueprint 时,真正的运行时实例可能是:

  • LinkedLayers 模式:通过 GetLinkedAnimLayerInstanceByClass 获取链接层实例
  • LinkedAnimGraph 模式:通过 GetLinkedAnimGraphInstanceByTag 获取链接图实例
  • 无预览蓝图:直接返回 PreviewMeshComponent->GetAnimInstance()

3.3 FAnimationBlueprintEditorMode 类

概述

FAnimationBlueprintEditorMode 继承自 FBlueprintEditorApplicationMode,定义了动画蓝图编辑器在标准模式下的标签页布局。

布局结构(v1.6):

复制代码
┌─────────────┬──────────────────────┬──────────────┐
│  左栏 (25%) │    中栏 (55%)        │  右栏 (20%)  │
├─────────────┤                      ├──────────────┤
│   视口      │   文档编辑区 (80%)    │  细节面板     │
│  (50%)     │                      │  高级预览     │
│             │                      │  资产覆盖     │
├─────────────┼──────────────────────┼──────────────┤
│  骨骼树     │   编译结果/查找 (20%) │  预览属性     │
│  姿态监视   │                      │  资源浏览器   │
│  我的蓝图   │                      │  插槽名称     │
│  (50%)     │                      │              │
└─────────────┴──────────────────────┴──────────────┘

构造函数分析:

cpp 复制代码
FAnimationBlueprintEditorMode::FAnimationBlueprintEditorMode(
    const TSharedRef<FAnimationBlueprintEditor>& InAnimationBlueprintEditor)
    : FBlueprintEditorApplicationMode(InAnimationBlueprintEditor,
        FAnimationBlueprintEditorModes::AnimationBlueprintEditorMode,
        FAnimationBlueprintEditorModes::GetLocalizedMode, false, false)
{
    PreviewScenePtr = InAnimationBlueprintEditor->GetPreviewScene();
    AnimBlueprintPtr = CastChecked<UAnimBlueprint>(InAnimationBlueprintEditor->GetBlueprintObj());

    bool bIsTemplate = false;
    if (UAnimBlueprint* AnimBlueprint = AnimBlueprintPtr.Get())
    {
        bIsTemplate = AnimBlueprint->bIsTemplate;
    }

    TabLayout = FTabManager::NewLayout("Stanalone_AnimationBlueprintEditMode_Layout_v1.6")
        // ... 大量布局代码 ...

    // 注册 Persona 视口 Tab
    FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked<FPersonaModule>("Persona");
    FPersonaViewportArgs ViewportArgs(InAnimationBlueprintEditor->GetPersonaToolkit()->GetPreviewScene());
    ViewportArgs.OnViewportCreated = FOnViewportCreated::CreateSP(
        &InAnimationBlueprintEditor.Get(), &FAnimationBlueprintEditor::HandleViewportCreated);
    ViewportArgs.BlueprintEditor = InAnimationBlueprintEditor;
    ViewportArgs.ContextName = TEXT("AnimationBlueprintEditor.Viewport");
    PersonaModule.RegisterPersonaViewportTabFactories(TabFactories, InAnimationBlueprintEditor, ViewportArgs);

    // 注册各种 Tab 工厂
    TabFactories.RegisterFactory(PersonaModule.CreateAdvancedPreviewSceneTabFactory(...));
    TabFactories.RegisterFactory(PersonaModule.CreateAnimationAssetBrowserTabFactory(...));
    TabFactories.RegisterFactory(PersonaModule.CreateAnimBlueprintPreviewTabFactory(...));
    TabFactories.RegisterFactory(PersonaModule.CreateAnimBlueprintAssetOverridesTabFactory(...));
    TabFactories.RegisterFactory(PersonaModule.CreatePoseWatchTabFactory(...));
    TabFactories.RegisterFactory(PersonaModule.CreateCurveViewerTabFactory(...));
    TabFactories.RegisterFactory(PersonaModule.CreateAnimAssetFindReplaceTabFactory(...));

    if (!bIsTemplate)
    {
        ISkeletonEditorModule& SkeletonEditorModule =
            FModuleManager::LoadModuleChecked<ISkeletonEditorModule>("SkeletonEditor");
        TabFactories.RegisterFactory(SkeletonEditorModule.CreateSkeletonTreeTabFactory(...));
        TabFactories.RegisterFactory(PersonaModule.CreateSkeletonSlotNamesTabFactory(...));
    }

    // 注册工具栏扩展
    PersonaModule.OnRegisterTabs().Broadcast(TabFactories, InAnimationBlueprintEditor);
    LayoutExtender = MakeShared<FLayoutExtender>();
    PersonaModule.OnRegisterLayoutExtensions().Broadcast(*LayoutExtender.Get());
    TabLayout->ProcessExtensions(*LayoutExtender.Get());
}

设计亮点:

  1. 布局版本号"Stanalone_AnimationBlueprintEditMode_Layout_v1.6"(注意:原文确实拼写为 "Stanalone",而非 "Standalone",这是 Epic 代码中的一个历史拼写错误)。布局版本号确保了用户升级后布局可以平滑迁移。
  2. LayoutExtender 机制 :通过 OnRegisterLayoutExtensions 委托,其他模块可以向动画蓝图编辑器的布局中注入额外的标签页。这是一个完全解耦的扩展机制。
  3. 模板模式判断 :通过 AnimBlueprint->bIsTemplate 动态决定是否注册骨骼树和插槽名称标签页。

TabFactories 注册的标签页:

  • PersonaViewport --- 预览视口
  • AdvancedPreviewSceneTab --- 高级预览设置
  • AnimationAssetBrowser --- 动画序列浏览器
  • AnimBlueprintPreviewEditor --- 预览实例属性编辑器
  • AnimBlueprintAssetOverrides --- 资产覆盖面板
  • PoseWatchTab --- 姿态监视管理器
  • CurveViewer --- 曲线查看器
  • AnimAssetFindReplace --- 动画资产查找替换
  • SkeletonTree --- 骨骼树(非模板模式)
  • SkeletonSlotNames --- 骨骼插槽名称(非模板模式)

PostActivateMode():

cpp 复制代码
void FAnimationBlueprintEditorMode::PostActivateMode()
{
    if (UAnimBlueprint* AnimBlueprint = AnimBlueprintPtr.Get())
    {
        // 切换到图编辑模式时关闭任何活动的预览动画
        PreviewScenePtr.Pin()->SetPreviewAnimationAsset(NULL, false);

        // 确保调试对象有效
        UDebugSkelMeshComponent* PreviewComponent =
            PreviewScenePtr.Pin()->GetPreviewMeshComponent();
        if ((AnimBlueprint->GetObjectBeingDebugged() == NULL) &&
            (PreviewComponent->IsAnimBlueprintInstanced()))
        {
            PersonaUtils::SetObjectBeingDebugged(AnimBlueprint,
                PreviewComponent->GetAnimInstance());
        }

        // 派生动画蓝图 → 自动打开资产覆盖标签页
        if (UAnimBlueprint::FindRootAnimBlueprint(AnimBlueprint))
        {
            MyBlueprintEditor.Pin()->GetTabManager()->TryInvokeTab(
                AnimationBlueprintEditorTabs::AssetOverridesTab);
        }
    }
    FBlueprintEditorApplicationMode::PostActivateMode();
}

3.4 FAnimationBlueprintInterfaceEditorMode 类

继承自 FBlueprintInterfaceApplicationMode,为动画蓝图接口提供简化的编辑布局。

布局结构:

  • 中栏(75%):文档编辑区(80%)+ 编译结果/查找(20%)
  • 右栏(25%):我的蓝图(50%)+ 细节面板(50%)

与标准模式相比,接口模式没有预览视口、骨骼树和动画资源浏览器,因为这些对接口编辑来说是不必要的。

3.5 FAnimationBlueprintEditorModule 类

概述

模块的入口类,管理模块的完整生命周期。

StartupModule():

cpp 复制代码
void FAnimationBlueprintEditorModule::StartupModule()
{
    // 1. 创建扩展性管理器
    MenuExtensibilityManager = MakeShareable(new FExtensibilityManager);
    ToolBarExtensibilityManager = MakeShareable(new FExtensibilityManager);

    // 2. 注册 Slate 可视化工厂
    AnimGraphNodeFactory = MakeShareable(new FAnimationGraphNodeFactory());
    FEdGraphUtilities::RegisterVisualNodeFactory(AnimGraphNodeFactory);

    AnimGraphPinFactory = MakeShareable(new FAnimationGraphPinFactory());
    FEdGraphUtilities::RegisterVisualPinFactory(AnimGraphPinFactory);

    AnimGraphPinConnectionFactory = MakeShareable(new FAnimationGraphPinConnectionFactory());
    FEdGraphUtilities::RegisterVisualPinConnectionFactory(AnimGraphPinConnectionFactory);

    // 3. 注册蓝图创建回调
    FKismetEditorUtilities::RegisterOnBlueprintCreatedCallback(this,
        UAnimInstance::StaticClass(),
        FKismetEditorUtilities::FOnBlueprintCreated::CreateRaw(
            this, &FAnimationBlueprintEditorModule::OnNewBlueprintCreated));

    // 4. 注册消息日志
    FMessageLogModule& MessageLogModule =
        FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
    FMessageLogInitializationOptions InitOptions;
    InitOptions.bShowFilters = true;
    InitOptions.bShowPages = true;
    MessageLogModule.RegisterLogListing("AnimBlueprintLog",
        LOCTEXT("AnimBlueprintLog", "Anim Blueprint Log"), InitOptions);

    // 5. 注册编辑器设置
    ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked<ISettingsModule>("Settings");
    SettingsModule.RegisterSettings("Editor", "ContentEditors", "AnimationBlueprintEditor",
        LOCTEXT("AnimationBlueprintEditorSettingsName", "Animation Blueprint Editor"),
        LOCTEXT("AnimationBlueprintEditorSettingsDescription",
            "Configure the look and feel of the Animation Blueprint Editor."),
        GetMutableDefault<UAnimationBlueprintEditorSettings>());
}

ShutdownModule():

cpp 复制代码
void FAnimationBlueprintEditorModule::ShutdownModule()
{
    FKismetEditorUtilities::UnregisterAutoBlueprintNodeCreation(this);
    FEdGraphUtilities::UnregisterVisualNodeFactory(AnimGraphNodeFactory);
    FEdGraphUtilities::UnregisterVisualPinFactory(AnimGraphPinFactory);
    FEdGraphUtilities::UnregisterVisualPinConnectionFactory(AnimGraphPinConnectionFactory);

    ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked<ISettingsModule>("Settings");
    SettingsModule.UnregisterSettings("Editor", "ContentEditors", "AnimationBlueprintEditor");

    MenuExtensibilityManager.Reset();
    ToolBarExtensibilityManager.Reset();
}

OnNewBlueprintCreated():

当新的 UAnimInstance 蓝图被创建时,自动在事件图中添加一个 TryGetPawnOwner 调用节点:

cpp 复制代码
void FAnimationBlueprintEditorModule::OnNewBlueprintCreated(UBlueprint* InBlueprint)
{
    if (InBlueprint->UbergraphPages.Num() > 0)
    {
        UEdGraph* EventGraph = InBlueprint->UbergraphPages[0];
        int32 SafeXPosition = 0;
        int32 SafeYPosition = 0;

        if (EventGraph->Nodes.Num() != 0)
        {
            SafeXPosition = EventGraph->Nodes[0]->NodePosX;
            SafeYPosition = EventGraph->Nodes[EventGraph->Nodes.Num() - 1]->NodePosY +
                EventGraph->Nodes[EventGraph->Nodes.Num() - 1]->NodeHeight + 100;
        }

        // 创建 TryGetPawnOwner 调用节点
        UK2Node_CallFunction* GetOwnerNode = NewObject<UK2Node_CallFunction>(EventGraph);
        UFunction* MakeNodeFunction = UAnimInstance::StaticClass()->FindFunctionByName(
            GET_FUNCTION_NAME_CHECKED(UAnimInstance, TryGetPawnOwner));
        GetOwnerNode->CreateNewGuid();
        GetOwnerNode->PostPlacedNewNode();
        GetOwnerNode->SetFromFunction(MakeNodeFunction);
        GetOwnerNode->SetFlags(RF_Transactional);
        GetOwnerNode->AllocateDefaultPins();
        GetOwnerNode->NodePosX = SafeXPosition;
        GetOwnerNode->NodePosY = SafeYPosition;
        UEdGraphSchema_K2::SetNodeMetaData(GetOwnerNode, FNodeMetadata::DefaultGraphNode);
        GetOwnerNode->MakeAutomaticallyPlacedGhostNode();

        EventGraph->AddNode(GetOwnerNode);
    }
}

MakeAutomaticallyPlacedGhostNode() 标记节点为"自动放置的幽灵节点"------这意味着如果用户自己添加了 TryGetPawnOwner,这个自动添加的会被自动移除。这是一个精巧的设计。

GetTypeActions():

扫描所有 UAnimSequenceBaseUSkeleton 资产,提取其中定义的 AnimNotify,为每个 Notify 创建一个 UAnimNotifyEventNodeSpawner,注册到蓝图动作数据库中。这使得动画通知事件可以在动画蓝图的事件图中作为自定义事件节点放置。

GetInstanceActions():

UAnimBlueprintGeneratedClass 中获取当前动画蓝图的 AnimNotifies,为每个 Notify 创建节点生成器。

3.6 Slate 可视化层

3.6.1 FAnimationGraphNodeFactory

职责: 根据 UEdGraphNode 的实际类型,创建对应的 Slate 控件。

CreateNode 实现:

cpp 复制代码
TSharedPtr<SGraphNode> FAnimationGraphNodeFactory::CreateNode(UEdGraphNode* InNode) const
{
    if (UAnimGraphNode_Base* BaseAnimNode = Cast<UAnimGraphNode_Base>(InNode))
    {
        // 按继承层级从具体到通用进行判断
        if (UAnimGraphNode_Root* RootAnimNode = Cast<UAnimGraphNode_Root>(InNode))
            return SNew(SGraphNodeAnimationResult, RootAnimNode);
        else if (UAnimGraphNode_StateMachineBase* StateMachineInstance =
            Cast<UAnimGraphNode_StateMachineBase>(InNode))
            return SNew(SGraphNodeStateMachineInstance, StateMachineInstance);
        else if (UAnimGraphNode_SequencePlayer* SequencePlayer =
            Cast<UAnimGraphNode_SequencePlayer>(InNode))
            return SNew(SGraphNodeSequencePlayer, SequencePlayer);
        else if (UAnimGraphNode_LayeredBoneBlend* LayeredBlend =
            Cast<UAnimGraphNode_LayeredBoneBlend>(InNode))
            return SNew(SGraphNodeLayeredBoneBlend, LayeredBlend);
        else if (UAnimGraphNode_BlendSpaceBase* BlendSpacePlayer =
            Cast<UAnimGraphNode_BlendSpaceBase>(InNode))
            return SNew(SGraphNodeBlendSpacePlayer, BlendSpacePlayer);
        else if (UAnimGraphNode_BlendSpaceGraphBase* BlendSpaceGraph =
            Cast<UAnimGraphNode_BlendSpaceGraphBase>(InNode))
            return SNew(SGraphNodeBlendSpaceGraph, BlendSpaceGraph);
        else if (UAnimGraphNode_LinkedAnimGraph* LinkedAnimLayer =
            Cast<UAnimGraphNode_LinkedAnimGraph>(InNode))
            return SNew(SGraphNodeLinkedLayer, LinkedAnimLayer);
        else
            return SNew(SAnimationGraphNode, BaseAnimNode);  // 通用回退
    }
    // 状态机节点
    else if (UAnimStateTransitionNode* TransitionNode = Cast<UAnimStateTransitionNode>(InNode))
        return SNew(SGraphNodeAnimTransition, TransitionNode);
    else if (UAnimStateNode* StateNode = Cast<UAnimStateNode>(InNode))
        return SNew(SGraphNodeAnimState, StateNode);
    else if (UAnimStateAliasNode* StateAliasNode = Cast<UAnimStateAliasNode>(InNode))
        return SNew(SGraphNodeAnimStateAlias, StateAliasNode);
    else if (UAnimStateConduitNode* ConduitNode = Cast<UAnimStateConduitNode>(InNode))
        return SNew(SGraphNodeAnimConduit, ConduitNode);
    else if (UAnimStateEntryNode* EntryNode = Cast<UAnimStateEntryNode>(InNode))
        return SNew(SGraphNodeAnimStateEntry, EntryNode);
    else if (UK2Node_AnimNodeReference* AnimNodeReference =
        Cast<UK2Node_AnimNodeReference>(InNode))
        return SNew(SAnimNodeReference, AnimNodeReference);

    return nullptr;
}

类型映射表:

C++ 节点类型 Slate 控件 说明
UAnimGraphNode_Root SGraphNodeAnimationResult 动画图输出结果节点
UAnimGraphNode_StateMachineBase SGraphNodeStateMachineInstance 状态机实例节点
UAnimGraphNode_SequencePlayer SGraphNodeSequencePlayer 序列播放器(显示缩略图)
UAnimGraphNode_LayeredBoneBlend SGraphNodeLayeredBoneBlend 分层骨骼混合
UAnimGraphNode_BlendSpaceBase SGraphNodeBlendSpacePlayer 混合空间播放器
UAnimGraphNode_BlendSpaceGraphBase SGraphNodeBlendSpaceGraph 混合空间图
UAnimGraphNode_LinkedAnimGraph SGraphNodeLinkedLayer 链接动画层
其他 UAnimGraphNode_Base SAnimationGraphNode 通用动画节点(回退)
UAnimStateTransitionNode SGraphNodeAnimTransition 状态过渡
UAnimStateNode SGraphNodeAnimState 动画状态
UAnimStateAliasNode SGraphNodeAnimStateAlias 状态别名
UAnimStateConduitNode SGraphNodeAnimConduit 管道节点
UAnimStateEntryNode SGraphNodeAnimStateEntry 入口节点
UK2Node_AnimNodeReference SAnimNodeReference 动画节点引用

设计亮点:

  • 使用 if-else 链条而非虚函数分发,因为节点类分布在多个模块中,虚函数分发需要跨模块的依赖
  • 判断顺序从具体到通用------UAnimGraphNode_RootUAnimGraphNode_Base 的子类,必须先检查
  • 有通用回退 SAnimationGraphNode,确保任何 UAnimGraphNode_Base 子类至少有一个可用的外观

3.6.2 FAnimationGraphPinFactory

cpp 复制代码
TSharedPtr<SGraphPin> FAnimationGraphPinFactory::CreatePin(UEdGraphPin* InPin) const
{
    if (InPin->GetSchema()->IsA<UAnimationGraphSchema>() &&
        InPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
    {
        if ((InPin->PinType.PinSubCategoryObject == FPoseLink::StaticStruct()) ||
            (InPin->PinType.PinSubCategoryObject == FComponentSpacePoseLink::StaticStruct()))
        {
            return SNew(SGraphPinPose, InPin);
        }
    }

    if (InPin->GetSchema()->IsA<UAnimationStateMachineSchema>() &&
        InPin->PinType.PinCategory == UAnimationStateMachineSchema::PC_Exec)
    {
        return SNew(SGraphPinExec, InPin);
    }

    return nullptr;  // 不处理 → 使用默认引脚外观
}

判断逻辑:

  • Schema 必须是 UAnimationGraphSchema
  • 引脚类型必须是 PC_Struct
  • 子类型必须是 FPoseLinkFComponentSpacePoseLink → 创建 SGraphPinPose(带属性列表显示的特殊引脚)
  • 状态机图中的 PC_Exec 引脚 → 创建 SGraphPinExec

3.6.3 FAnimationGraphPinConnectionFactory

cpp 复制代码
FConnectionDrawingPolicy* FAnimationGraphPinConnectionFactory::CreateConnectionPolicy(
    const UEdGraphSchema* Schema, int32 InBackLayerID, int32 InFrontLayerID,
    float ZoomFactor, const FSlateRect& InClippingRect,
    FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) const
{
    if (Schema->IsA(UAnimationGraphSchema::StaticClass()))
    {
        return new FAnimGraphConnectionDrawingPolicy(InBackLayerID, InFrontLayerID,
            ZoomFactor, InClippingRect, InDrawElements, InGraphObj);
    }
    else if (Schema->IsA(UAnimationStateMachineSchema::StaticClass()))
    {
        return new FStateMachineConnectionDrawingPolicy(InBackLayerID, InFrontLayerID,
            ZoomFactor, InClippingRect, InDrawElements, InGraphObj);
    }
    return nullptr;
}

职责:

  • FAnimGraphConnectionDrawingPolicy:继承 FKismetConnectionDrawingPolicy,为动画图提供自定义的连接线绘制,缓存引脚属性信息以在连接线上显示属性传递
  • FStateMachineConnectionDrawingPolicy:为状态机图提供自定义的过渡连接线绘制

3.7 UAnimationBlueprintEditorSettings 类

cpp 复制代码
UCLASS(config = EditorPerProjectUserSettings)
class UAnimationBlueprintEditorSettings : public UObject
{
    GENERATED_BODY()

public:
    /** 如果为 true,自动对选中的节点启用姿态监视 */
    UPROPERTY(EditAnywhere, config, Category = "Debugging")
    bool bPoseWatchSelectedNodes = false;

    /** 是否在动画图角落显示"ANIMATION"文字 */
    UPROPERTY(EditAnywhere, config, Category = "Graphs")
    bool bShowGraphCornerText = true;

public:
    DECLARE_MULTICAST_DELEGATE_TwoParams(FOnUpdateSettingsMulticaster,
        const UAnimationBlueprintEditorSettings*, EPropertyChangeType::Type);
    FOnUpdateSettingsMulticaster OnSettingsChange;

    FDelegateHandle RegisterOnUpdateSettings(const FOnUpdateSettingsMulticaster::FDelegate& Delegate)
    {
        return OnSettingsChange.Add(Delegate);
    }

    void UnregisterOnUpdateSettings(FDelegateHandle Object)
    {
        OnSettingsChange.Remove(Object);
    }

protected:
    virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
};

设置变更通知机制:

编辑器通过 RegisterOnUpdateSettings 注册回调,监听设置变更:

cpp 复制代码
// 在 InitAnimationBlueprintEditor 中注册
AnimationBlueprintEditorSettingsChangedHandle =
    GetMutableDefault<UAnimationBlueprintEditorSettings>()->RegisterOnUpdateSettings(
        UAnimationBlueprintEditorSettings::FOnUpdateSettingsMulticaster::FDelegate::CreateSP(
            this, &FAnimationBlueprintEditor::HandleUpdateSettings));

当设置变更时,HandleUpdateSettings 被调用:

cpp 复制代码
void FAnimationBlueprintEditor::HandleUpdateSettings(
    const UAnimationBlueprintEditorSettings* AnimationBlueprintEditorSettings,
    EPropertyChangeType::Type ChangeType)
{
    if (AnimationBlueprintEditorSettings->bPoseWatchSelectedNodes != bPreviousPoseWatchSelectedNodes)
    {
        bPreviousPoseWatchSelectedNodes = AnimationBlueprintEditorSettings->bPoseWatchSelectedNodes;
        RemoveAllSelectionPoseWatches();
        if (AnimationBlueprintEditorSettings->bPoseWatchSelectedNodes)
        {
            HandlePoseWatchSelectedNodes();
        }
    }
}

4 功能使用示例编写

示例1:扩展动画蓝图编辑器工具栏

这是最常见的扩展需求------在动画蓝图编辑器的工具栏上添加自定义按钮。

MyAnimBPToolbarExtender.h:

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "IAnimationBlueprintEditorModule.h"

class FMyAnimBPToolbarExtender
{
public:
    static void Register();
    static void Unregister();

private:
    static TSharedRef<FExtender> CreateToolbarExtender(
        const TSharedRef<FUICommandList> InCommandList,
        TSharedRef<IAnimationBlueprintEditor> InAnimationBlueprintEditor);
    
    static void FillToolbar(FToolBarBuilder& ToolbarBuilder,
        TSharedRef<IAnimationBlueprintEditor> InAnimationBlueprintEditor);
    
    static FDelegateHandle ExtenderHandle;
};

MyAnimBPToolbarExtender.cpp:

cpp 复制代码
#include "MyAnimBPToolbarExtender.h"
#include "AnimationBlueprintEditor.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Modules/ModuleManager.h"

FDelegateHandle FMyAnimBPToolbarExtender::ExtenderHandle;

void FMyAnimBPToolbarExtender::Register()
{
    FAnimationBlueprintEditorModule& Module = 
        FModuleManager::LoadModuleChecked<FAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
    
    ExtenderHandle = Module.GetAllAnimationBlueprintEditorToolbarExtenders().Add(
        IAnimationBlueprintEditorModule::FAnimationBlueprintEditorToolbarExtender::CreateStatic(
            &FMyAnimBPToolbarExtender::CreateToolbarExtender
        )
    );
}

void FMyAnimBPToolbarExtender::Unregister()
{
    if (ExtenderHandle.IsValid())
    {
        FAnimationBlueprintEditorModule& Module = 
            FModuleManager::LoadModuleChecked<FAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
        Module.GetAllAnimationBlueprintEditorToolbarExtenders().Remove(ExtenderHandle);
        ExtenderHandle.Reset();
    }
}

TSharedRef<FExtender> FMyAnimBPToolbarExtender::CreateToolbarExtender(
    const TSharedRef<FUICommandList> InCommandList,
    TSharedRef<IAnimationBlueprintEditor> InAnimationBlueprintEditor)
{
    TSharedRef<FExtender> Extender = MakeShareable(new FExtender);
    
    Extender->AddToolBarExtension(
        "Debugging",                        // 扩展目标段名称
        EExtensionHook::After,              // 插入位置
        InCommandList,
        FToolBarExtensionDelegate::CreateStatic(
            &FMyAnimBPToolbarExtender::FillToolbar, InAnimationBlueprintEditor)
    );
    
    return Extender;
}

void FMyAnimBPToolbarExtender::FillToolbar(
    FToolBarBuilder& ToolbarBuilder,
    TSharedRef<IAnimationBlueprintEditor> InAnimationBlueprintEditor)
{
    ToolbarBuilder.AddToolBarButton(
        FUIAction(
            FExecuteAction::CreateLambda([InAnimationBlueprintEditor]()
            {
                // 自定义操作:例如导出当前选中节点的信息
                UE_LOG(LogTemp, Log, TEXT("自定义工具栏按钮被点击"));
            })
        ),
        NAME_None,
        LOCTEXT("MyButtonLabel", "My Button"),
        LOCTEXT("MyButtonTooltip", "Click to execute custom action"),
        FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Recompile")
    );
}

关键点:

  • 工具栏扩展器注入的段名称必须与编辑器中已有的段名匹配。可用的段名包括:"Asset""Debugging""Scripting"
  • EExtensionHook::After 表示插入到目标段之后
  • 扩展器通过 FAnimationBlueprintEditorToolbarExtender 委托注册,该委托接收 FUICommandListIAnimationBlueprintEditor 引用

示例2:创建带自定义外观的动画图节点

假设我们要创建一个自定义动画图节点,并为其提供特殊的 Slate 外观。

MyAnimGraphNode.h:

cpp 复制代码
#pragma once

#include "AnimGraphNode_Base.h"
#include "MyAnimGraphNode.generated.h"

UCLASS()
class UMyAnimGraphNode : public UAnimGraphNode_Base
{
    GENERATED_BODY()

public:
    // 自定义节点标题颜色
    virtual FLinearColor GetNodeTitleColor() const override
    {
        return FLinearColor(0.2f, 0.6f, 1.0f);
    }

    virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override
    {
        return LOCTEXT("MyNodeTitle", "My Custom Anim Node");
    }

    // 声明此节点支持姿态监视
    virtual bool IsPoseWatchable() const override { return true; }

    // 返回关联的动画资产(可选)
    virtual UAnimationAsset* GetAnimationAsset() const override { return nullptr; }
};

SMyAnimGraphNode.h:

cpp 复制代码
#pragma once

#include "SAnimationGraphNode.h"

class SMyAnimGraphNode : public SAnimationGraphNode
{
public:
    SLATE_BEGIN_ARGS(SMyAnimGraphNode) {}
    SLATE_END_ARGS()

    void Construct(const FArguments& InArgs, UAnimGraphNode_Base* InNode);

    virtual void UpdateGraphNode() override;

private:
    // 可以在这里添加自定义的子控件
};

SMyAnimGraphNode.cpp:

cpp 复制代码
#include "SMyAnimGraphNode.h"
#include "Widgets/Text/STextBlock.h"

void SMyAnimGraphNode::Construct(const FArguments& InArgs, UAnimGraphNode_Base* InNode)
{
    SAnimationGraphNode::Construct(SAnimationGraphNode::FArguments(), InNode);
}

void SMyAnimGraphNode::UpdateGraphNode()
{
    // 先调用基类实现
    SAnimationGraphNode::UpdateGraphNode();

    // 移除默认的标题文本并添加自定义的
    if (LeftNodeBox.IsValid())
    {
        LeftNodeBox->AddSlot()
            .AutoHeight()
            [
                SNew(STextBlock)
                .Text(LOCTEXT("CustomOverlay", "Custom Overlay"))
            ];
    }
}

在 FAnimationGraphNodeFactory 中注册新节点(修改 AnimationGraphFactory.cpp):

cpp 复制代码
else if (UMyAnimGraphNode* MyCustomNode = Cast<UMyAnimGraphNode>(InNode))
{
    return SNew(SMyAnimGraphNode, MyCustomNode);
}

关键点:

  • SAnimationGraphNode 是通用动画图节点的 Slate 外观,适合作为自定义外观的基类
  • 必须在 FAnimationGraphNodeFactory::CreateNode 中添加新节点的类型判断------判断需要放在通用回退 SAnimationGraphNode 之前
  • UpdateGraphNode() 是 Slate 控件构建/重建外观的核心方法

示例3:通过模块扩展注册自定义调试对象

假设我们想在动画蓝图编辑器的调试下拉菜单中添加自定义的调试对象。

cpp 复制代码
// MyAnimBPEditorIntegration.cpp

void UMyEditorSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    
    FAnimationBlueprintEditorModule& Module = 
        FModuleManager::LoadModuleChecked<FAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
    
    // 注册自定义调试对象提供者
    Module.OnGetCustomDebugObjects().AddUObject(
        this, &UMyEditorSubsystem::OnGetCustomDebugObjects);
}

void UMyEditorSubsystem::Deinitialize()
{
    if (FModuleManager::Get().IsModuleLoaded("AnimationBlueprintEditor"))
    {
        FAnimationBlueprintEditorModule& Module = 
            FModuleManager::LoadModuleChecked<FAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
        Module.OnGetCustomDebugObjects().RemoveAll(this);
    }
    
    Super::Deinitialize();
}

void UMyEditorSubsystem::OnGetCustomDebugObjects(
    const IAnimationBlueprintEditor& AnimationBlueprintEditor,
    TArray<FCustomDebugObject>& DebugList)
{
    // 添加自定义的调试目标
    // 例如:从 World 中查找所有匹配的 AnimInstance
    if (UWorld* World = GEditor->GetEditorWorldContext().World())
    {
        for (TObjectIterator<UAnimInstance> It; It; ++It)
        {
            if (It->GetWorld() == World)
            {
                FCustomDebugObject CustomDebugObj(*It,
                    FString::Printf(TEXT("World Instance: %s"), *It->GetName()));
                DebugList.Add(CustomDebugObj);
            }
        }
    }
}

关键点:

  • OnGetCustomDebugObjects 委托在编辑器获取调试对象列表时被调用(见 FAnimationBlueprintEditor::GetCustomDebugObjects()
  • 预览实例始终是列表中的第一个调试对象
  • 自定义调试对象追加在预览实例之后

示例4:响应编辑器设置变更

假设我们要在编辑器设置变更时执行自定义逻辑:

cpp 复制代码
// 在合适的初始化位置(如编辑器子系统)注册
void UMyEditorSettingsObserver::StartObserving()
{
    UAnimationBlueprintEditorSettings* Settings = 
        GetMutableDefault<UAnimationBlueprintEditorSettings>();
    
    SettingsChangedHandle = Settings->RegisterOnUpdateSettings(
        UAnimationBlueprintEditorSettings::FOnUpdateSettingsMulticaster::FDelegate::CreateUObject(
            this, &UMyEditorSettingsObserver::OnAnimationBlueprintEditorSettingsChanged));
}

void UMyEditorSettingsObserver::OnAnimationBlueprintEditorSettingsChanged(
    const UAnimationBlueprintEditorSettings* Settings,
    EPropertyChangeType::Type ChangeType)
{
    if (ChangeType == EPropertyChangeType::ValueSet)
    {
        if (Settings->bPoseWatchSelectedNodes)
        {
            UE_LOG(LogTemp, Log, TEXT("Pose Watch on selected nodes enabled"));
        }
        else
        {
            UE_LOG(LogTemp, Log, TEXT("Pose Watch on selected nodes disabled"));
        }
    }
}

void UMyEditorSettingsObserver::StopObserving()
{
    UAnimationBlueprintEditorSettings* Settings = 
        GetMutableDefault<UAnimationBlueprintEditorSettings>();
    Settings->UnregisterOnUpdateSettings(SettingsChangedHandle);
}

5 总结与最佳实践

5.1 核心要点

  1. 继承而非重写

    • FAnimationBlueprintEditor 继承自 FBlueprintEditor,继承了所有蓝图编辑的基础能力
    • 如果要扩展编辑器功能,应该通过 FExtensibilityManager 和委托机制,而不是直接修改源代码
  2. 编辑模式管理

    • 三种编辑模式覆盖了动画蓝图、接口和模板三种使用场景
    • 每种模式有独立的标签页布局,通过 FTabManager::FLayout 定义
    • 模式切换时通过 PostActivateMode() 处理状态转换
  3. 编译管道的安全性

    • 编译前必须清理 AnimNotifyState(避免 UFunction 被清理导致崩溃)
    • 编译后必须重新初始化动画实例和重连调试对象
    • 通过 OnPostCompile() 确保编辑数据与运行时节点同步
  4. 实时预览机制

    • 引脚默认值变更通过 HandlePinDefaultValueChanged 实时传播到预览
    • 属性面板变更通过 NotifyPostChange 批量同步到预览
    • CopyNodeDataToPreviewNode 是连接编辑器和运行时实例的桥梁
  5. 姿态监视系统

    • 支持手动和自动两种模式
    • 临时 PoseWatch(ShouldDeleteOnDeselect)在节点取消选中时自动清理
    • 永久 PoseWatch 保存在蓝图中,随资产持久化

5.2 最佳实践

  1. 扩展编辑器

    • 通过 IAnimationBlueprintEditorModule::GetAllAnimationBlueprintEditorToolbarExtenders() 添加工具栏按钮
    • 通过 IAnimationBlueprintEditorModule::GetMenuExtensibilityManager() 添加菜单项
    • 通过 OnGetCustomDebugObjects 委托添加自定义调试对象
  2. 创建自定义动画图节点

    • 继承 UAnimGraphNode_Base 创建节点逻辑
    • 继承 SAnimationGraphNode 创建节点外观
    • FAnimationGraphNodeFactory 中注册新节点的外观映射
  3. 处理编译相关逻辑

    • OnBlueprintPreCompile 中执行清理(如结束正在运行的 NotifyState)
    • OnBlueprintPostCompile 中执行恢复(如重连调试对象)
    • OnPostCompile 中执行数据同步(如 CopyNodeDataToPreviewNode
  4. 使用姿态监视调试动画

    • 在编辑器设置中启用 bPoseWatchSelectedNodes 自动监视选中节点
    • 在姿态监视管理器中查看和管理所有 PoseWatch
    • 使用 ChoosePoseWatchColor() 自动选择合适的监视颜色
  5. 注意内存管理

    • 析构时显式清理 PersonaToolkit(强制销毁预览场景)
    • 清理 EditorModeManager 中的预览场景引用
    • 取消所有委托注册以避免悬空指针

5.3 常见问题解决

问题1:编译动画蓝图时崩溃

  • 检查是否在 OnBlueprintPreCompile 中正确清理了 AnimNotifyState
  • 确保编译前关闭了使用 AnimScriptInstance 的其他编辑器

问题2:预览视口不更新

  • 检查 HandlePinDefaultValueChanged 是否正确绑定(在 OnGraphEditorFocused 中绑定)
  • 确认 CopyNodeDataToPreviewNode 是否正确实现了数据复制
  • 检查 FocusedGraphEdPtr 是否有效

问题3:姿态监视不显示

  • 检查节点是否支持姿态监视(IsPoseWatchable() 返回 true)
  • 确认不是在子动画蓝图中操作(子蓝图不支持姿态监视)
  • 检查 UAnimationBlueprintEditorSettings::bPoseWatchSelectedNodes 是否启用

问题4:扩展的工具栏按钮不显示

  • 确认扩展器已正确注册到 GetAllAnimationBlueprintEditorToolbarExtenders()
  • 检查扩展钩子位置(EExtensionHook::After 等)是否正确
  • 确认扩展的工具栏段名称(如 "Asset"、"Debugging")是否存在

附录

A. 相关资源

  • Persona 模块文档
  • FBlueprintEditor 基类文档
  • AnimGraph 模块文档
  • AnimationStateMachineSchemaAnimationGraphSchema 文档
  • UE5 官方文档:Animation Blueprint Editor
  • UE5 官方文档:Pose Watch

B. 文件结构参考

复制代码
AnimationBlueprintEditor/
├── AnimationBlueprintEditor.Build.cs
├── Public/
│   ├── IAnimationBlueprintEditor.h          (编辑器公共接口)
│   ├── IAnimationBlueprintEditorModule.h     (模块公共接口)
│   ├── AnimationBlueprintEditorSettings.h    (编辑器用户设置)
│   └── AnimationNodes/
│       └── SAnimationGraphNode.h             (通用动画节点 Slate 外观)
└── Private/
    ├── AnimationBlueprintEditor.h            (编辑器主体声明)
    ├── AnimationBlueprintEditor.cpp          (编辑器主体实现 ~2600 行)
    ├── AnimationBlueprintEditorMode.h        (标准编辑模式声明)
    ├── AnimationBlueprintEditorMode.cpp      (标准编辑模式实现)
    ├── AnimationBlueprintEditorModule.h      (模块类声明)
    ├── AnimationBlueprintEditorModule.cpp    (模块类实现)
    ├── AnimationBlueprintInterfaceEditorMode.h/.cpp  (接口编辑模式)
    ├── AnimationBlueprintEditorSettings.cpp           (设置实现)
    ├── AnimationBlueprintToolMenuContext.h             (工具菜单上下文)
    ├── AnimationGraphFactory.h/.cpp          (Slate 节点/引脚/连接线工厂)
    ├── AnimGraphConnectionDrawingPolicy.h/.cpp        (动画图连接绘制)
    ├── StateMachineConnectionDrawingPolicy.h/.cpp     (状态机连接绘制)
    ├── BlendSpaceDocumentTabFactory.h/.cpp            (混合空间文档标签)
    ├── TabPayload_BlendSpaceGraph.h                   (混合空间标签负载)
    ├── AnimationNodes/
    │   ├── SAnimNodeReference.h / .cpp
    │   ├── SAnimationGraphNode.cpp
    │   ├── SBlendSpacePreview.h / .cpp
    │   ├── SGraphNodeAnimationResult.h / .cpp
    │   ├── SGraphNodeBlendSpaceGraph.h / .cpp
    │   ├── SGraphNodeBlendSpacePlayer.h / .cpp
    │   ├── SGraphNodeLayeredBoneBlend.h / .cpp
    │   ├── SGraphNodeLinkedLayer.h / .cpp
    │   ├── SGraphNodeSequencePlayer.h / .cpp
    │   ├── SGraphNodeStateMachineInstance.h / .cpp
    │   └── SPoseWatchOverlay.h / .cpp
    ├── AnimationPins/
    │   └── SGraphPinPose.h / .cpp
    └── AnimationStateNodes/
        ├── SGraphNodeAnimState.h / .cpp
        ├── SGraphNodeAnimStateAlias.h / .cpp
        ├── SGraphNodeAnimStateEntry.h / .cpp
        └── SGraphNodeAnimTransition.h / .cpp
相关推荐
远离UE42 小时前
UE5 不同贴图采样类型的区别
ue5
weixin_404679312 天前
虚幻5 如何打开关卡蓝图
ue5
dong1326974 天前
UE5FPS游戏开发教程(一)
ue5
朗迹 - 张伟5 天前
UE5.8 用Trae的AI开发功能
ue5
日月云棠7 天前
UE5 Lyra Teams模块深度分析:从队伍创建到伤害判定的完整链路
ue5
日月云棠10 天前
UE5 Lyra Inventory 物品系统模块深度分析——从数据定义到网络同步的完整链路
ue5
日月云棠11 天前
UE5 Lyra Weapons 模块深度分析——从装备实例到弹道散布的完整武器系统
ue5
清泓y11 天前
UE5程序化生成技术
ue5
清泓y12 天前
UE5--VR与AR开发技术
ue5·ar·vr