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

AnimationEditorWidgets 模块全面系统分析


目录

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

1 模块概述

1.1 基本信息

属性
模块名称AnimationEditorWidgets
类型Editor(仅编辑器)
描述提供 Schematic Graph(示意图编辑器)面板,用于在动画编辑器中以可视化节点图的方式展示和编辑骨骼、控制点等结构

这个模块是 UE5 动画编辑器工具链中的一个重要组成部分。它提供了一套完整的示意图形编辑面板(Schematic Graph Panel),类似于引擎中常见的蓝图节点图、材质编辑器节点图,但专门为动画编辑器场景设计,用于展示动画骨骼、控制点(Controls)之间的拓扑关系和连接。

整个模块的核心思想是:用可视化的节点图来直观展示动画系统的结构。无论是在 Control Rig 编辑器、Skeletal Mesh 编辑器还是其他动画相关工具中,只要涉及"展示节点之间的连接关系",都可以利用这套面板。

1.2 模块依赖关系

从 Build.cs 可以看到,这个模块的依赖分了两层:

Public 依赖(对外暴露的接口层):

  • CoreSlateSlateCoreInputCore — Slate UI 基础框架
  • ToolWidgets — 工具控件的公共基类(注意 build.cs 特别强调 不依赖 UnrealEd
  • AnimationCore — 动画核心类型
  • ApplicationCore — 应用程序核心

Private 依赖(内部实现层):

  • EngineCoreUObject — 引擎核心
  • UnrealEdGraphEditorPropertyEditor — 编辑器专用模块

关键设计决策:Build.cs 中有一条醒目的注释——THIS MODULE SHOULD NOT EVER DEPEND ON UNREALED。这意味着这个模块被设计为可以被其他非 UnrealEd 模块引用,是作为一个"编辑器通用组件"而存在的,而不是绑定在某个特定编辑器上。


2 模块整体架构解析

2.1 架构图

AnimationEditorWidgets 采用经典的 MVC(Model-View-Controller) 架构模式,将数据模型与视图渲染清晰分离:

┌─────────────────────────────────────────────────────────────────────────────┐
│                     FAnimationEditorWidgetsModule (模块入口)                  │
│                     - 初始化 FSchematicGraphStyle                           │
│                     - 定义 LogSchematicGraph 日志分类                       │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          FSchematicGraphStyle (样式层)                       │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │  SVG 笔刷资源:                                                          │  │
│  │  - Schematic.Background / Schematic.Group / Schematic.Outline         │  │
│  │  - Schematic.Dot.Small / Medium / Large / Group                       │  │
│  │  - Schematic.Tag.Background / Schematic.Label.Background              │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
          ┌───────────────────────────┼───────────────────────────┐
          ▼                           ▼                           ▼
┌─────────────────────┐   ┌─────────────────────┐   ┌─────────────────────┐
│  FSchematicGraphModel│   │  FSchematicGraphNode│   │  FSchematicGraphLink│
│  (数据模型)          │   │  (节点数据)          │   │  (连线数据)          │
├──────────────────────┤   ├──────────────────────┤   ├──────────────────────┤
│ - Nodes 数组        │   │ - Guid 唯一标识      │   │ - Guid 唯一标识      │
│ - Links 数组        │   │ - Position 位置      │   │ - SourceNodeGuid     │
│ - 增删查改 API      │   │ - Label 标签文本     │   │ - TargetNodeGuid     │
│ - 委托通知机制      │   │ - Colors/Brushes 图层│   │ - Color/Thickness    │
│ - 父子关系管理      │   │ - Tags 标记系统      │   │ - Min/Max 范围       │
│ - 选中状态管理      │   │ - 父子层级关系       │   │ - 可见性控制         │
│ - Tick 维护         │   │ - 鼠标交互回调       │   │                      │
└─────────────────────┘   └──────────────────────┘   └─────────────────────┘
          │                           │
          │          ┌────────────────┼────────────────┐
          │          ▼                ▼                ▼
          │  ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
          │  │FSchematicGraph│ │FSchematicGraph│ │FSchematicGraph   │
          │  │GroupNode     │ │AutoGroupNode │ │Tag / GroupTag    │
          │  │(可展开组节点) │ │(自动分组节点) │ │(节点标记)        │
          │  └──────────────┘ └──────────────┘ └──────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                        SSchematicGraphPanel (视图层)                         │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │  继承自 SNodePanel + FTickableEditorObject                             │  │
│  │  - 管理所有 SSchematicGraphNode 子 Widget                            │  │
│  │  - 数据模型观察者:绑定 Model 的增删通知                              │  │
│  │  - 绘制连线(Lines / RotatedBox)                                     │  │
│  │  - 自动分组逻辑(UpdateAutoGroupingForNodes)                         │  │
│  │  - 自动缩放逻辑(UpdateAutoScalingForNodes)                          │  │
│  │  - 拖拽事件管理(Drag & Drop)                                        │  │
│  │  - 每帧 Tick 更新动画状态                                              │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                      │                                       │
│          ┌───────────────────────────┼───────────────────────────┐           │
│          ▼                           ▼                           ▼           │
│  ┌──────────────┐   ┌──────────────────────┐   ┌──────────────────────┐   │
│  │SSchematicGraph│   │FSchematicGraphNode   │   │FSchematicViewport    │   │
│  │Node (节点Widget)│ │DragDropOp (拖拽操作)  │   │TabSummoner (Tab集成) │   │
│  └──────────────┘   └──────────────────────┘   └──────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 模块间依赖关系

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

FAnimationEditorWidgetsModule (最外层)
    ↓ 初始化
FSchematicGraphStyle (样式单例)
    ↓ 被使用
SSchematicGraphPanel (UI 面板)
    ↓ 读写
FSchematicGraphModel (数据模型)
    ↓ 包含
FSchematicGraphNode ──→ FSchematicGraphTag (标记)
    ↓ 派生
FSchematicGraphGroupNode ──→ FSchematicGraphGroupTag
    ↓ 派生
FSchematicGraphAutoGroupNode
    ↓ 连接
FSchematicGraphLink (节点间连线)
    ↓ 渲染为
SSchematicGraphNode (节点 Widget)
    ↓ 拖拽时创建
FSchematicGraphNodeDragDropOp
    ↓ 集成到编辑器
FSchematicViewportTabSummoner (Workflow Tab)

这层分层的设计意图非常清晰:数据层和视图层完全解耦FSchematicGraphModel 及其包含的 Node、Link、Tag 都是纯数据类,不依赖任何 Slate 渲染代码。而 SSchematicGraphPanelSSchematicGraphNode 作为视图层,通过观察者模式订阅数据模型的变化通知,自动同步 UI。

这种设计带来的好处是:

  • 可以单独测试数据模型,不依赖 Slate 渲染
  • 可以替换视图实现,比如将来换成 UMG 渲染,数据模型一行不用改
  • 数据模型可以序列化,方便做 Undo/Redo、保存/加载

2.3 模块划分

1. 模块入口层

职责:

  • 注册/注销 Slate 样式
  • 定义日志分类

包含文件:

  • AnimationEditorWidgetsModule.cpp

2. 样式定义层

职责:

  • 定义 Schematic Graph 的所有视觉样式资源
  • 提供 SVG 笔刷(背景、轮廓、圆点、标签等)

包含类:

  • FSchematicGraphStyle

3. 基础设施层

职责:

  • 提供自定义 RTTI 宏系统(SCHEMATICGRAPHELEMENT_BODY_BASE / SCHEMATICGRAPHELEMENT_BODY
  • 定义可见性枚举和事件委托类型

包含文件:

  • SchematicGraphDefines.h

4. 数据模型层

职责:

  • 管理所有节点和连线的增删改查
  • 维护父子节点层级关系
  • 管理节点选中状态
  • 通过委托通知视图层变更
  • 提供虚拟方法供子类覆写(位置、颜色、大小、可见性等)

包含类:

  • FSchematicGraphModel — 数据模型核心
  • FSchematicGraphNode — 节点数据
  • FSchematicGraphGroupNode — 可展开组节点
  • FSchematicGraphAutoGroupNode — 自动分组节点
  • FSchematicGraphLink — 连线数据
  • FSchematicGraphTag / FSchematicGraphGroupTag — 节点标记

5. 视图渲染层

职责:

  • 将数据模型渲染为 Slate Widget
  • 处理用户交互(点击、拖拽、右键菜单)
  • 实现自动分组和自动缩放
  • 管理动画插值状态

包含类:

  • SSchematicGraphPanel — 面板 Widget
  • SSchematicGraphNode — 节点 Widget
  • FSchematicGraphNodeDragDropOp — 拖拽操作

6. 编辑器集成层

职责:

  • 将面板集成到 Workflow 风格的编辑器 Tab 中

包含类:

  • FSchematicViewportTabSummoner

2.4 数据流走向

正常添加节点流程:

1. 外部调用 FSchematicGraphModel::AddNode<T>()
   ↓
2. 创建 TSharedPtr<FSchematicGraphNode>,设置 Model 引用
   ↓
3. 添加到 Nodes 数组和 NodeByGuid Map
   ↓
4. 触发 OnNodeAddedDelegate.Broadcast()
   ↓
5. SSchematicGraphPanel::AddNode() 收到通知
   ↓
6. 创建 SSchematicGraphNode Widget,绑定 Position/Size/Scale/Color 等动画属性
   ↓
7. 调用 SNodePanel::AddGraphNode() 添加到面板
   ↓
8. Widget 出现在视图中,动画属性从初始值插值到目标值

自动分组流程:

1. SSchematicGraphPanel::Tick() 每帧执行
   ↓
2. UpdatePerNodeCaches(true) — 重置缓存,移除旧的自动分组关系
   ↓
3. UpdateAutoGroupingForNodes() — 根据位置计算分组
   ├─ 遍历所有无父节点的可见节点
   ├─ 按整数坐标 hash 分组(坐标 / AutoGroupingDistance)
   ├─ 过滤掉元素数 ≤1 的分组
   ├─ 为每个多元素分组创建/复用 FSchematicGraphAutoGroupNode
   └─ 调用 SetParentNode() 建立父子关系
   ↓
4. UpdatePerNodeCaches(false) — 重新计算缓存(含新的父子关系)
   ↓
5. UpdateAutoScalingForNodes() — 检测重叠节点并缩小

拖拽流程:

1. 用户在 SSchematicGraphNode 上按下左键并拖动
   ↓
2. OnDragDetected() 创建 FSchematicGraphNodeDragDropOp
   ↓
3. SSchematicGraphPanel::Tick() 检测到拖拽状态
   ├─ 更新被拖拽节点的 PositionDuringDrag
   └─ 检测 DragDropOp 进入/离开面板区域
   ↓
4. 用户释放鼠标 → OnDropEvent() 触发
   ├─ 记录 DropTarget Guid
   └─ 执行 OnDropDelegate 回调

2.5 核心技术栈

技术/类用途
SNodePanelSlate 节点面板基类,提供节点布局、缩放、平移等基础能力
FTickableEditorObject编辑器 Tick 接口,让面板每帧更新
TAnimatedAttribute<T>动画插值属性,实现节点位置、大小、颜色的平滑过渡
TEasingAttributeInterpolator<T>缓动插值器(CubicEaseOut),控制动画曲线
TSharedFromThis智能指针支持,Node/Link/Tag 都继承自它
FGuid全局唯一标识符,用作节点/连线/标记的 ID
FWorkflowTabFactory编辑器 Tab 工厂,集成到 Workflow 编辑器
FSlateStyleSet自定义 Slate 样式集
IMAGE_BRUSH_SVGSVG 笔刷宏,支持矢量图形
FSlateRoundedBoxBrush圆角矩形笔刷
FMenuBuilder右键菜单构建器
FDragDropOperation拖拽操作基类
DECLARE_DELEGATE委托声明宏
DECLARE_EVENT事件声明宏

2.6 架构设计优势

  1. MVC 分离:数据模型(FSchematicGraphModel)与视图(SSchematicGraphPanel)完全解耦,模型层不依赖 Slate
  2. 观察者模式:通过委托(FOnSchematicGraphNodeAdded 等)实现模型到视图的自动同步
  3. 自定义 RTTI 系统:通过宏 SCHEMATICGRAPHELEMENT_BODY_BASE 实现的轻量运行时类型识别,支持安全的 Cast 操作,且不依赖 UObject 反射
  4. 动画过渡:所有视觉属性(位置、大小、颜色、透明度)都通过 TAnimatedAttribute 实现平滑插值,用户体验流畅
  5. 可扩展的节点类型:通过继承 FSchematicGraphNode 可以创建自定义节点类型,通过覆写虚方法控制位置、大小、颜色、可见性等
  6. 自动分组与缩放:面板内置了自动分组(将靠近的节点自动归入一个 GroupNode)和自动缩放(防止节点重叠)功能
  7. 编辑器无关:模块不依赖 UnrealEd,可以被任何编辑器工具复用
  8. 拖拽支持:完整实现了从内部拖拽到外部拖入的全流程

2.7 潜在改进点

  1. 连线绘制被注释掉了SSchematicGraphPanel::OnPaint() 中连线的绘制代码整个被 /* */ 注释掉了,目前连线数据虽然可以添加和管理,但不会在视图中渲染
  2. 鼠标滚轮被禁用OnMouseWheel() 直接返回 Unhandled,缩放功能不可用
  3. 缺少序列化:数据模型目前没有实现序列化接口,无法保存/加载图表布局
  4. Undo/Redo 缺失:没有集成 UE 的 Transaction 系统,编辑操作不可撤销
  5. 性能优化空间UpdateAutoScalingForNodes() 使用 O(n²) 的暴力距离检测,注释中也标注了"todo: use a faster distance algorithm"
  6. RTTI 宏的局限性:自定义 RTTI 系统不支持多重继承的灵活类型判断,Cast 链只能沿着单继承链查找

3 类级代码注释详解

3.1 FAnimationEditorWidgetsModule — 模块入口

概述

FAnimationEditorWidgetsModule 是整个模块的入口点,实现了 IModuleInterface 接口。它的职责非常精简——在模块启动时初始化 Slate 样式,在模块关闭时无需额外清理。

class FAnimationEditorWidgetsModule : public IModuleInterface
{
public:
    virtual void StartupModule() override
    {
        (void)FSchematicGraphStyle::Get();  // 触发样式单例的构造
    }
    virtual void ShutdownModule() override {}
};

设计要点:

  • FSchematicGraphStyle::Get() 是一个静态方法,返回一个函数内部的 static 局部变量。调用它时,如果样式尚未构造,会触发构造函数,在构造函数中注册到 FSlateStyleRegistry。这就是懒汉式单例的经典实现。
  • 模块关闭时不需要手动清理样式,因为 FSchematicGraphStyle 的析构函数会自动从注册表中注销。

3.2 FSchematicGraphStyle — 样式定义

概述

FSchematicGraphStyle 继承自 FSlateStyleSet,在构造函数中注册了 Schematic Graph 所需的所有视觉资源。所有笔刷都使用 SVG 格式,支持任意缩放而不失真。

定义的笔刷资源:

笔刷名称用途
Schematic.Background节点背景
Schematic.Group分组展开时的圆形区域
Schematic.Outline.Single单层轮廓
Schematic.Outline.Double双层轮廓
Schematic.Dot.Small小圆点(节点中心装饰)
Schematic.Dot.Medium中圆点
Schematic.Dot.Large大圆点
Schematic.Dot.Group分组节点圆点
Schematic.Tag.Background标记背景
Schematic.Label.Background标签背景(半透明圆角矩形)

所有的笔刷资源路径在 Engine/Content/Editor/Slate/SchematicGraph/ 目录下,使用 SVG 格式。


3.3 SchematicGraphDefines — 自定义 RTTI 宏系统

概述

这是整个模块中最体现设计巧思的部分。UE 的 UObject 系统自带反射和类型判断,但 FSchematicGraphNodeFSchematicGraphLinkFSchematicGraphTag 都是纯 C++ 类(不继承 UObject),无法使用 UE 的 Cast<>。因此,引擎开发者自己实现了一套轻量级的运行时类型识别系统。

核心宏:SCHEMATICGRAPHELEMENT_BODY_BASE

这个宏用于类型继承树的根部。它做了以下几件事:

  1. 定义一个静态 Type 成员(FName),存储类名
  2. 提供虚函数 GetType()IsA()
  3. 提供模板化的 Cast<T>()CastChecked<T>() 友元函数
#define SCHEMATICGRAPHELEMENT_BODY_BASE(ClassName) \
virtual ~ClassName() {} \
inline static const FName& Type = TEXT(#ClassName); \
virtual const FName& GetType() const { return ClassName::Type; } \
virtual bool IsA(const FName& InType) const { return Type == InType; } \
template<typename ElementType> bool IsA() const { return IsA(ElementType::Type); } \
// ... Cast 友元函数 ...

核心宏:SCHEMATICGRAPHELEMENT_BODY

这个宏用于继承链中的子类。它覆写了 IsA(),使其沿继承链向上查找:

virtual bool IsA(const FName& InType) const
{
    if(ClassName::Type == InType) { return true; }
    return SuperClass::IsA(InType);  // 向父类递归
}

使用示例:

// 基类
class FSchematicGraphNode {
    SCHEMATICGRAPHELEMENT_BODY_BASE(FSchematicGraphNode)
};

// 子类
class FSchematicGraphGroupNode : public FSchematicGraphNode {
    SCHEMATICGRAPHNODE_BODY(FSchematicGraphGroupNode, FSchematicGraphNode)
    // 展开后等价于 SCHEMATICGRAPHELEMENT_BODY(FSchematicGraphGroupNode, FSchematicGraphNode, FSchematicGraphNode)
};

为什么需要第三个参数 BaseClass

注意到 SCHEMATICGRAPHELEMENT_BODY 宏有三个参数:ClassNameSuperClassBaseClass。第三个参数用于 Cast 友元函数中的类型转换——因为 Cast 函数必须定义在继承链的基类中,而不是当前类中。例如 FSchematicGraphGroupNodeCast 友元实际上转发到 FSchematicGraphNodeCast

template<typename T>
friend const T* Cast(const FSchematicGraphGroupNode* InElement) {
    return Cast<T>((const FSchematicGraphNode*) InElement);  // 转发到基类的 Cast
}

这套系统的设计非常精巧,在不依赖 UObject 反射的前提下,实现了类似于 UE 原生 Cast<> 的类型安全转换。


3.4 FSchematicGraphModel — 数据模型核心

概述

FSchematicGraphModel 是整个 Schematic Graph 的数据中心。它管理着所有节点、连线、父子关系、选中状态,并通过委托机制通知视图层更新。

核心数据结构

TArray<TSharedPtr<FSchematicGraphNode>> Nodes;          // 所有节点
TMap<FGuid, TSharedPtr<FSchematicGraphNode>> NodeByGuid; // Guid → 节点
TArray<TSharedPtr<FSchematicGraphLink>> Links;           // 所有连线
TMap<FGuid, TSharedPtr<FSchematicGraphLink>> LinkByGuid; // Guid → 连线
TMap<uint32, TSharedPtr<FSchematicGraphLink>> LinkByHash; // Hash → 连线
TMap<FGuid, TTuple<TArray<FGuid>, TArray<FGuid>>> NodeGuidToLinkGuids; // 节点 → 连线

设计亮点:

  • 使用 TMap<FGuid, ...> 做 Guid 索引,O(1) 查找
  • LinkByHashHashCombine(SourceGuid, TargetGuid) 做键,实现 O(1) 的"按源-目标对查找连线"
  • NodeGuidToLinkGuidsTTuple 中,Get<0>() 是作为源节点的连线列表,Get<1>() 是作为目标节点的连线列表

节点管理

AddNode() — 模板方法,支持创建任意 FSchematicGraphNode 子类:

template<typename NodeType = FSchematicGraphNode>
NodeType* AddNode(bool bNotify = true)
{
    const TSharedPtr<FSchematicGraphNode> NewNode = MakeShareable(new NodeType);
    NewNode->Model = this;  // 设置反向引用
    Nodes.Add(NewNode);
    NodeByGuid.Add(NewNode->GetGuid(), NewNode);
    if (bNotify && OnNodeAddedDelegate.IsBound())
        OnNodeAddedDelegate.Broadcast(NewNode.Get());
    return static_cast<NodeType*>(NewNode.Get());
}

RemoveNode() — 删除节点时自动清理关联的连线和父子关系:

  1. 通过 NodeGuidToLinkGuids 找到所有关联连线并删除
  2. 清理 LastExpandedNode 引用
  3. 递归移除所有子节点
  4. 从父节点中移除自己

连线管理

AddLink() — 双向索引,同时建立 Guid 索引和 Hash 索引:

LinkByGuid.Add(NewLink->GetGuid(), NewLink);
LinkByHash.Add(NewLink->GetLinkHash(), NewLink);
NodeGuidToLinkGuids.FindOrAdd(InSourceNodeGuid).Get<0>().Add(NewLink->GetGuid());
NodeGuidToLinkGuids.FindOrAdd(InTargetNodeGuid).Get<1>().Add(NewLink->GetGuid());

父子关系管理

SetParentNode() — 建立父子关系时:

  • 如果子节点已有父节点,先移除旧关系
  • 如果父节点为 nullptr,移除所有父子关系
  • 当父节点只剩一个子节点时,如果是 GroupNode,自动折叠

Tick 维护

void FSchematicGraphModel::Tick(float InDeltaTime)
{
    // 1. 清理无效的 LastExpandedNode 引用
    // 2. 清理无效的父子关系(父节点已删除但子节点仍持有引用)
    // 3. 清理子节点列表中不匹配的条目
}

委托系统

委托触发时机
OnNodeAddedDelegate添加节点后
OnNodeRemovedDelegate删除节点前
OnLinkAddedDelegate添加连线后
OnLinkRemovedDelegate删除连线前
OnTagAddedDelegate添加标记后
OnTagRemovedDelegate删除标记前
OnGraphResetDelegate图表重置时

3.5 FSchematicGraphNode — 节点数据

概述

FSchematicGraphNode 是图表中节点的数据载体。每个节点有唯一 Guid、可选的文本标签、位置、颜色图层、笔刷图层、标记系统,以及父子层级关系。

核心属性

FGuid Guid;                          // 唯一标识
FText Label;                         // 显示标签
FGuid ParentNodeGuid;                // 父节点 Guid
TArray<FGuid> ChildNodeGuids;        // 子节点 Guid 列表
FVector2d Position;                  // 位置
FVector2d PositionOffset;            // 位置偏移
float ScaleOffset;                   // 缩放偏移(默认 1.0)
TArray<FLinearColor> Colors;         // 颜色图层
TArray<const FSlateBrush*> Brushes;  // 笔刷图层
TArray<TSharedPtr<FSchematicGraphTag>> Tags;  // 标记列表

图层系统

每个节点可以有多个渲染图层,每个图层由"颜色 + 笔刷"组成。默认构造时创建三层:

图层笔刷颜色用途
0Schematic.Background白色 40% 透明节点背景
1Schematic.Outline.Single白色节点轮廓
2Schematic.Dot.Small蓝色节点中心装饰

关键方法

GetPosition() — 优先返回父节点指定的位置,否则返回自身 Position:

FVector2d FSchematicGraphNode::GetPosition() const
{
    if(const FSchematicGraphNode* ParentNode = GetParentNode())
    {
        const TOptional<FVector2d> ParentPosition = Model->GetPositionForChildNode(ParentNode, this);
        if(ParentPosition.IsSet()) return ParentPosition.GetValue();
    }
    return Position;
}

GetVisibility() — 复杂的可见性判断逻辑:

  1. 如果自身 Visibility == Hidden,直接返回 Hidden
  2. 如果存在 LastExpandedNode 且它不是自己或父节点,返回 FadedOut
  3. 如果父节点指定了子节点可见性,返回父节点的指定值
  4. 否则返回自身 Visibility

鼠标交互

  • OnMouseEnter() — 缩放偏移设为 1.25(放大)
  • OnMouseLeave() — 缩放偏移恢复为 1.0
  • OnDragOver() — 缩放偏移设为 0.75(缩小),并展开所在 GroupNode
  • OnDragLeave() — 恢复缩放,折叠所在 GroupNode

3.6 FSchematicGraphGroupNode — 可展开组节点

概述

FSchematicGraphGroupNode 继承自 FSchematicGraphNode,增加了展开/折叠功能。当展开时,子节点会以圆形排列在组节点周围;折叠时,子节点隐藏,组节点以一个特殊圆点显示。

展开/折叠动画

使用 TAnimatedAttribute<float> 实现展开状态的平滑过渡:

TSharedPtr<TAnimatedAttribute<float>> ExpansionState;
// 0.0 = 完全折叠
// 1.0 = 完全展开

SetExpanded() 的逻辑:

  1. 展开时,如果子节点数为 0,直接返回
  2. 折叠时且已完全折叠,直接停止动画
  3. 正常切换时,设置目标值(0 或 1),并设置延迟(drag 时有延迟,click 时无延迟)
  4. 递归处理父组节点
  5. 折叠时,递归折叠所有子组节点

子节点圆形布局

TOptional<FVector2d> GetPositionForChildNode(const FSchematicGraphNode* InChildNode) const
{
    if(IsExpanded() && Model)
    {
        const int32 ChildNodeIndex = ChildNodeGuids.Find(InChildNode->GetGuid());
        const float Distance = ExpansionRadius * GetExpansionState();
        const float Angle = 360.f * float(ChildNodeIndex) / float(ChildNodeGuids.Num());
        const FVector2d Offset = FVector2d(Distance, 0).GetRotated(Angle);
        return Model->GetPositionForNode(this) + Offset;
    }
    return Super::GetPositionForChildNode(InChildNode);
}

子节点等角度均匀分布在以组节点为圆心、ExpansionRadius * ExpansionState 为半径的圆周上。

延迟机制

float GetDelayDuration(bool bEnter) const
{
    if (FSlateApplication::Get().IsDragDropping())
        return bEnter ? EnterDelayDuration : LeaveDelayDuration;  // 0.25s / 0.75s
    return 0.f;  // 点击时无延迟
}

拖拽悬停时,展开有 0.25 秒延迟(防止误触),折叠有 0.75 秒延迟(给用户反应时间)。


3.7 FSchematicGraphAutoGroupNode — 自动分组节点

概述

FSchematicGraphAutoGroupNode 继承自 FSchematicGraphGroupNode,是 SSchematicGraphPanel::UpdateAutoGroupingForNodes() 自动创建的。当多个节点在空间上彼此靠近时,它们会被自动归入一个 AutoGroupNode。

GetLabel() — 折叠时自动拼接所有子节点的标签文本:

const FText& FSchematicGraphAutoGroupNode::GetLabel() const
{
    if(SuperLabel.IsEmpty() && !IsExpanded())
    {
        // 遍历所有子节点,拼接标签
        for(int32 Index = 0; Index < GetNumChildNodes(); Index++)
        {
            // 格式: "Label1\nLabel2\nLabel3"
        }
        return AutoGroupLabel;
    }
    return SuperLabel;
}

3.8 FSchematicGraphLink — 连线数据

概述

FSchematicGraphLink 表示两个节点之间的连接。它存储了源节点和目标节点的 Guid、连线范围(Min/Max)、颜色、粗细、笔刷等视觉属性。

注意:当前版本中,连线的渲染代码在 SSchematicGraphPanel::OnPaint() 中被整个注释掉了,所以连线数据虽然能正常管理,但不会在视图中显示。


3.9 FSchematicGraphTag — 节点标记

概述

FSchematicGraphTag 是附加在节点上的可视化标记。每个标记有自己的背景色/前景色/笔刷/标签文本/摆放角度,在 SSchematicGraphNode::OnPaint() 中渲染在节点周围。

FSchematicGraphGroupTag — 继承自 FSchematicGraphTag

  • GetLabel() — 返回子节点数量
  • GetVisibility() — 子节点数为 0 时隐藏
  • 当 AutoGroupNode 的子节点数 < 3 或已展开时,在 Model 层被隐藏

3.10 SSchematicGraphPanel — 视图面板

概述

SSchematicGraphPanel 是整个模块中最复杂的类,同时继承了 SNodePanel(Slate 节点面板)和 FTickableEditorObject(编辑器 Tick)。它负责:

  1. 作为数据模型的观察者,响应增删通知
  2. 管理所有 SSchematicGraphNode 子 Widget
  3. 每帧 Tick 更新动画状态
  4. 实现自动分组和自动缩放
  5. 处理拖拽事件

数据模型绑定

void SSchematicGraphPanel::SetSchematicGraph(FSchematicGraphModel* InGraphData)
{
    // 1. 解绑旧模型的委托
    if (GraphData) { ... RemoveAll ... }
    // 2. 设置新模型
    GraphData = InGraphData;
    // 3. 绑定新模型的委托
    if (GraphData) {
        GraphData->OnNodeAdded().AddSP(this, &SSchematicGraphPanel::AddNode);
        GraphData->OnNodeRemoved().AddSP(this, &SSchematicGraphPanel::RemoveNode);
        GraphData->OnLinkAdded().AddSP(this, &SSchematicGraphPanel::AddLink);
        GraphData->OnLinkRemoved().AddSP(this, &SSchematicGraphPanel::RemoveLink);
        GraphData->OnGraphReset().AddSP(this, &SSchematicGraphPanel::RebuildPanel);
    }
}

AddNode() — 创建节点 Widget

这是最体现"动画属性"设计的地方。每个节点 Widget 的 Position、Size、Scale、Color 都绑定到 TAnimatedAttribute

const auto Position = FVector2dAttribute::CreateWithGetter(
    Vector2DInterpolationSettings,  // CubicEaseOut, 0.2s
    FVector2dAttribute::FGetter::CreateSP(this, &SSchematicGraphPanel::GetPositionForNode, Guid)
);
const auto Size = FVector2dAttribute::CreateWithGetter(
    Vector2DInterpolationSettings,
    FVector2dAttribute::FGetter::CreateRaw(GraphData, &FSchematicGraphModel::GetSizeForNode, InNodeToAdd)
);
const auto Scale = FFloatAttribute::CreateWithGetter(
    FloatInterpolationSettings,
    FFloatAttribute::FGetter::CreateSP(this, &SSchematicGraphPanel::GetScaleForNode, Guid), 0.f
);

这意味着当数据模型中的 Position 发生变化时,Widget 不会立即跳变,而是以 CubicEaseOut 曲线在 0.2 秒内平滑过渡到新位置。

Tick() — 每帧更新

void SSchematicGraphPanel::Tick(float DeltaTime)
{
    // 1. 重置 DPI 缓存
    // 2. 调用数据模型的 Tick
    // 3. 检测外部拖拽操作进入/离开面板
    // 4. 更新每个子节点的拖拽状态和动画
    // 5. 运行自动分组和自动缩放
}

自动分组算法

UpdateAutoGroupingForNodes() 的核心逻辑:

  1. 遍历所有无父节点的可见节点
  2. 将节点位置除以 AutoGroupingDistance(默认 4.0),取整得到整数坐标
  3. 相同整数坐标的节点归为一组(HashCombine 生成 hash)
  4. 过滤掉只有 0 或 1 个节点的组
  5. 为每个有效组创建/复用 FSchematicGraphAutoGroupNode
  6. 对组内节点组合计算 CombinedHash,用于判断组是否发生变化
  7. 清理不再需要的组节点

自动缩放算法

UpdateAutoScalingForNodes() 的核心逻辑:

  1. 对每对启用了自动缩放的节点,计算它们之间的距离
  2. 如果距离小于两个节点半径之和(含 padding),计算需要缩小的比例
  3. 取每个节点所有重叠计算中的最大缩小比例
  4. 将缩小比例 clamp 到 0.4 以上,设置到节点的 AutoScale 属性

3.11 SSchematicGraphNode — 节点 Widget

概述

SSchematicGraphNode 继承自 SNodePanel::SNode,是单个节点的 Slate 渲染 Widget。它负责:

  • 绘制节点的多层图层(背景 + 轮廓 + 装饰圆点)
  • 绘制节点标记(Tags)
  • 绘制节点标签(文本 + 半透明背景)
  • 绘制分组展开的圆形区域
  • 处理鼠标交互(悬停、点击、拖拽)

OnPaint() 渲染流程

1. 调用 SNode::OnPaint() 基类渲染
2. 计算当前尺寸(Size * Scale)
3. 如果节点是 GroupNode 且处于展开状态,绘制展开圆形区域
4. 遍历 LayerColors 和 BrushGetter,绘制每个图层
5. 遍历节点 Tags,绘制每个标记的背景、前景和文本
6. 绘制节点标签(含半透明背景)

关键交互

OnMouseButtonDown()

  • 左键 → 触发 OnClickedDelegate,然后 DetectDrag 准备拖拽
  • 右键 → 调用 GetContextMenuForNode() 构建并弹出右键菜单

OnDragDetected()

  • 检查节点是否支持拖拽(IsDragSupportedForNode
  • 创建 FSchematicGraphNodeDragDropOp,设置 OffsetDuringDrag
  • 调用 OnBeginDragDelegate

GetNodeVisibility()

  • 如果正在被拖拽 → HitTestInvisible(不响应点击,但可见)
  • 如果数据模型说 Hidden → Hidden
  • 否则 → Visible

3.12 FSchematicGraphNodeDragDropOp — 拖拽操作

概述

FSchematicGraphNodeDragDropOp 继承自 FDragDropOperation,封装了节点拖拽的所有数据。拖拽装饰器显示一个简单的文本标签,内容是所有被拖拽节点的 GetDragDropDecoratorLabel() 拼接。


3.13 FSchematicViewportTabSummoner — 编辑器 Tab 集成

概述

FSchematicViewportTabSummoner 继承自 FWorkflowTabFactory,将 SSchematicGraphPanel 集成为一个编辑器 Tab。

Tab 复用策略RegisterTabSpawner() 中重写了 SetReuseTabMethod,始终返回空指针,意味着每次都会创建新 Tab,不复用旧 Tab

CreateTabBody() — 创建面板 Widget,设置边距和节点间距,并通过 OnViewportCreated 委托通知外部。


4 功能使用示例编写

示例1:创建基础 Schematic Graph 面板

// MySchematicViewController.h
#pragma once

#include "CoreMinimal.h"
#include "SchematicGraphPanel/SchematicGraphModel.h"
#include "SchematicGraphPanel/SSchematicGraphPanel.h"

class FMySchematicViewController
{
public:
    FMySchematicViewController()
    {
        // 创建数据模型
        GraphModel = MakeShared<FSchematicGraphModel>();
    }

    /** 构建 UI */
    TSharedRef<SWidget> CreateWidget()
    {
        return SNew(SSchematicGraphPanel)
            .GraphData(GraphModel.Get())
            .IsOverlay(false)
            .PaddingLeft(50)
            .PaddingRight(50)
            .PaddingTop(50)
            .PaddingBottom(50)
            .PaddingInterNode(10);
    }

    /** 添加几个节点 */
    void AddSomeNodes()
    {
        // 添加节点 A
        FSchematicGraphNode* NodeA = GraphModel->AddNode();
        NodeA->SetLabel(FText::FromString(TEXT("Spine")));
        NodeA->SetPosition(FVector2d(200, 200));

        // 添加节点 B
        FSchematicGraphNode* NodeB = GraphModel->AddNode();
        NodeB->SetLabel(FText::FromString(TEXT("Head")));
        NodeB->SetPosition(FVector2d(200, 100));

        // 添加节点 C
        FSchematicGraphNode* NodeC = GraphModel->AddNode();
        NodeC->SetLabel(FText::FromString(TEXT("LeftArm")));
        NodeC->SetPosition(FVector2d(100, 250));

        // 创建连线
        GraphModel->AddLink(NodeA->GetGuid(), NodeB->GetGuid());
        GraphModel->AddLink(NodeA->GetGuid(), NodeC->GetGuid());
    }

private:
    TSharedPtr<FSchematicGraphModel> GraphModel;
};

示例2:创建自定义节点类型

// MyBoneNode.h
#pragma once

#include "SchematicGraphPanel/SchematicGraphNode.h"

// 自定义骨骼节点,重写颜色和笔刷
class FMyBoneNode : public FSchematicGraphNode
{
    SCHEMATICGRAPHNODE_BODY(FMyBoneNode, FSchematicGraphNode)

public:
    FMyBoneNode()
    {
        // 使用中号圆点 + 绿色
        static const FSlateBrush* BackgroundBrush = FSchematicGraphStyle::Get().GetBrush("Schematic.Background");
        static const FSlateBrush* OutlineBrush = FSchematicGraphStyle::Get().GetBrush("Schematic.Outline.Double");
        static const FSlateBrush* DotBrush = FSchematicGraphStyle::Get().GetBrush("Schematic.Dot.Medium");
        Brushes = { BackgroundBrush, OutlineBrush, DotBrush };
        Colors = { FLinearColor::White * 0.4f, FLinearColor::Green, FLinearColor::Green };
    }

    virtual FVector2d GetPosition() const override
    {
        // 自定义位置逻辑
        return Position;
    }

    virtual FReply OnClicked(const FPointerEvent& InMouseEvent) override
    {
        UE_LOG(LogSchematicGraph, Log, TEXT("Bone node clicked: %s"), *GetLabel().ToString());
        return FReply::Handled();
    }
};

// 使用时:
// FMyBoneNode* BoneNode = GraphModel->AddNode<FMyBoneNode>();

示例3:使用 Tag 标记系统

void AddTaggedNodes(TSharedPtr<FSchematicGraphModel> Model)
{
    FSchematicGraphNode* ControlNode = Model->AddNode();
    ControlNode->SetLabel(FText::FromString(TEXT("IK_Foot_R")));
    ControlNode->SetPosition(FVector2d(300, 300));

    // 添加一个标记,表示这是一个 IK 控制点
    FSchematicGraphTag* IKTag = ControlNode->AddTag();
    IKTag->SetLabel(FText::FromString(TEXT("IK")));
    IKTag->SetBackgroundColor(FLinearColor::Red);
    IKTag->SetPlacementAngle(45.f);  // 放在节点右上方

    // 再添加一个标记,表示锁定状态
    FSchematicGraphTag* LockTag = ControlNode->AddTag();
    LockTag->SetLabel(FText::FromString(TEXT("L")));
    LockTag->SetBackgroundColor(FLinearColor::Yellow);
    LockTag->SetPlacementAngle(-45.f);  // 放在节点右下方
}

示例4:响应数据模型变更

void BindModelEvents(TSharedPtr<FSchematicGraphModel> Model)
{
    // 监听节点添加
    Model->OnNodeAdded().AddLambda([](const FSchematicGraphNode* Node)
    {
        UE_LOG(LogSchematicGraph, Log, TEXT("Node added: %s (Guid: %s)"),
            *Node->GetLabel().ToString(), *Node->GetGuid().ToString());
    });

    // 监听节点移除
    Model->OnNodeRemoved().AddLambda([](const FSchematicGraphNode* Node)
    {
        UE_LOG(LogSchematicGraph, Log, TEXT("Node removed: %s"), *Node->GetLabel().ToString());
    });

    // 监听连线添加
    Model->OnLinkAdded().AddLambda([Model](const FSchematicGraphLink* Link)
    {
        const FSchematicGraphNode* Source = Model->FindNode(Link->GetSourceNodeGuid());
        const FSchematicGraphNode* Target = Model->FindNode(Link->GetTargetNodeGuid());
        UE_LOG(LogSchematicGraph, Log, TEXT("Link: %s → %s"),
            Source ? *Source->GetLabel().ToString() : TEXT("?"),
            Target ? *Target->GetLabel().ToString() : TEXT("?"));
    });
}

示例5:集成到编辑器 Tab

// 在 AssetEditorToolkit 中注册 Schematic Viewport Tab
void FMyEditorToolkit::RegisterSchematicTab()
{
    FSchematicViewportArgs Args;
    Args.SchematicGraph = MyGraphModel.Get();
    Args.OnViewportCreated = FOnSchematicViewportCreated::CreateLambda(
        [this](const TSharedRef<SSchematicGraphPanel>& Panel)
        {
            // 保存面板引用,后续可以操作
            MySchematicPanel = Panel;
        }
    );

    TSharedPtr<FSchematicViewportTabSummoner> TabSummoner = 
        MakeShared<FSchematicViewportTabSummoner>(SharedThis(this), Args);
    TabSummoner->RegisterTabSpawner(GetTabManager(), GetCurrentApplicationMode().Get());
}

5 总结与最佳实践

5.1 核心要点

  1. MVC 分离是核心设计理念:数据模型(FSchematicGraphModel / FSchematicGraphNode / FSchematicGraphLink / FSchematicGraphTag)完全不依赖 Slate,视图层(SSchematicGraphPanel / SSchematicGraphNode)通过委托订阅数据变更。这种设计让模型可以独立测试、序列化、复用。

  2. 自定义 RTTI 是精巧的工程实践:在不依赖 UObject 反射的前提下,通过宏实现了一套完整的运行时类型识别系统。SCHEMATICGRAPHELEMENT_BODY_BASE / SCHEMATICGRAPHELEMENT_BODY 这套宏值得在类似的纯 C++ 类继承体系中使用。

  3. 动画属性提升用户体验TAnimatedAttribute<T> 配合 TEasingAttributeInterpolator<T> 实现了节点位置、大小、颜色、透明度的平滑过渡,让整个面板的操作体验非常流畅。所有"硬切"的视觉变化都被替换成了"软过渡"。

  4. 自动分组是智能化的体现UpdateAutoGroupingForNodes()UpdateAutoScalingForNodes() 让面板具备了"感知"节点空间关系的能力,自动将密集聚集的节点折叠成组,自动缩小重叠的节点。

  5. 父子层级关系灵活而强大:通过 ParentNodeGuid / ChildNodeGuids 建立的父子关系,配合虚方法 GetPositionForChildNode() / GetScaleForChildNode() / GetVisibilityForChildNode(),让 GroupNode 可以完全控制子节点的视觉表现。

5.2 最佳实践

  1. 创建自定义节点类型:继承 FSchematicGraphNode 并覆写虚方法,而不是在外部代码中通过 if-else 判断节点类型来处理差异。

  2. 使用 Tag 系统传递元信息:不要把所有信息都塞进节点 Label,善用 Tag 来标记节点的状态、类型、属性等。

  3. 通过委托响应变更:使用 OnNodeAdded / OnNodeRemoved 等委托来响应数据模型变更,而不是轮询。

  4. 合理设置 AutoGroupingDistance:根据节点的实际大小和间距需求调整这个值,默认 4.0 像素可能在不同 DPI 下表现不同。

  5. 注意 DPI 缩放AdjustPositionWithDPIScale() 在内部处理了 DPI 缩放,但自定义节点位置时需要注意是否需要手动调用。

5.3 常见问题

问题1:连线不显示

  • 当前版本中连线的渲染代码在 SSchematicGraphPanel::OnPaint() 中被注释掉了,这是已知的限制。如果需要连线功能,需要取消注释并适配。

问题2:节点位置不更新

  • 检查 Position 属性是否设置了动画插值(TAnimatedAttribute),如果动画未触发,检查 EnablePositionAnimation() 是否被调用。

问题3:自定义节点类型无法被 Cast 识别

  • 确保使用了正确的宏:SCHEMATICGRAPHNODE_BODY(ClassName, SuperClass),并确保第三个参数 BaseClass 指向正确的基类。

问题4:自动分组不符合预期

  • 调整 AutoGroupingDistance(默认 4.0),这个值控制"多近才算近"。如果节点视觉上很大但距离值设置太小,可能导致本应分组的节点没有分组。

附录

A. 文件结构参考

AnimationEditorWidgets/
├── AnimationEditorWidgets.Build.cs
├── Public/
│   └── SchematicGraphPanel/
│       ├── SSchematicGraphPanel.h       — 面板 Widget + 节点 Widget + 拖拽操作
│       ├── SchematicGraphDefines.h      — 自定义 RTTI 宏 + 委托声明
│       ├── SchematicGraphLink.h         — 连线数据
│       ├── SchematicGraphModel.h        — 数据模型(核心)
│       ├── SchematicGraphNode.h         — 节点数据 + 组节点 + 自动组节点
│       ├── SchematicGraphStyle.h        — 样式定义
│       ├── SchematicGraphTag.h          — 标记数据 + 组标记
│       └── SchematicViewportTabSummoner.h — 编辑器 Tab 集成
└── Private/
    ├── AnimationEditorWidgetsModule.cpp — 模块入口
    └── SchematicGraphPanel/
        ├── SSchematicGraphPanel.cpp     — 面板 + 节点 Widget 实现
        ├── SchematicGraphModel.cpp      — 数据模型实现
        ├── SchematicGraphNode.cpp       — 节点数据实现
        ├── SchematicGraphTag.cpp        — 标记数据实现
        └── SchematicViewportTabSummoner.cpp — Tab 集成实现

B. 相关模块

  • ToolWidgets — 本模块的公共依赖,提供基础工具 Widget 类
  • AnimationCore — 动画核心类型,本模块被设计为动画编辑器的辅助组件
  • GraphEditor — 引擎的通用图表编辑器框架,本模块可以与其配合使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值