AnimationEditorWidgets 模块全面系统分析
目录
1 模块概述
1.1 基本信息
| 属性 | 值 |
|---|---|
| 模块名称 | AnimationEditorWidgets |
| 类型 | Editor(仅编辑器) |
| 描述 | 提供 Schematic Graph(示意图编辑器)面板,用于在动画编辑器中以可视化节点图的方式展示和编辑骨骼、控制点等结构 |
这个模块是 UE5 动画编辑器工具链中的一个重要组成部分。它提供了一套完整的示意图形编辑面板(Schematic Graph Panel),类似于引擎中常见的蓝图节点图、材质编辑器节点图,但专门为动画编辑器场景设计,用于展示动画骨骼、控制点(Controls)之间的拓扑关系和连接。
整个模块的核心思想是:用可视化的节点图来直观展示动画系统的结构。无论是在 Control Rig 编辑器、Skeletal Mesh 编辑器还是其他动画相关工具中,只要涉及"展示节点之间的连接关系",都可以利用这套面板。
1.2 模块依赖关系
从 Build.cs 可以看到,这个模块的依赖分了两层:
Public 依赖(对外暴露的接口层):
Core、Slate、SlateCore、InputCore— Slate UI 基础框架ToolWidgets— 工具控件的公共基类(注意 build.cs 特别强调 不依赖 UnrealEd)AnimationCore— 动画核心类型ApplicationCore— 应用程序核心
Private 依赖(内部实现层):
Engine、CoreUObject— 引擎核心UnrealEd、GraphEditor、PropertyEditor— 编辑器专用模块
关键设计决策: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 渲染代码。而 SSchematicGraphPanel 和 SSchematicGraphNode 作为视图层,通过观察者模式订阅数据模型的变化通知,自动同步 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— 面板 WidgetSSchematicGraphNode— 节点 WidgetFSchematicGraphNodeDragDropOp— 拖拽操作
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 核心技术栈
| 技术/类 | 用途 |
|---|---|
SNodePanel | Slate 节点面板基类,提供节点布局、缩放、平移等基础能力 |
FTickableEditorObject | 编辑器 Tick 接口,让面板每帧更新 |
TAnimatedAttribute<T> | 动画插值属性,实现节点位置、大小、颜色的平滑过渡 |
TEasingAttributeInterpolator<T> | 缓动插值器(CubicEaseOut),控制动画曲线 |
TSharedFromThis | 智能指针支持,Node/Link/Tag 都继承自它 |
FGuid | 全局唯一标识符,用作节点/连线/标记的 ID |
FWorkflowTabFactory | 编辑器 Tab 工厂,集成到 Workflow 编辑器 |
FSlateStyleSet | 自定义 Slate 样式集 |
IMAGE_BRUSH_SVG | SVG 笔刷宏,支持矢量图形 |
FSlateRoundedBoxBrush | 圆角矩形笔刷 |
FMenuBuilder | 右键菜单构建器 |
FDragDropOperation | 拖拽操作基类 |
DECLARE_DELEGATE | 委托声明宏 |
DECLARE_EVENT | 事件声明宏 |
2.6 架构设计优势
- MVC 分离:数据模型(FSchematicGraphModel)与视图(SSchematicGraphPanel)完全解耦,模型层不依赖 Slate
- 观察者模式:通过委托(FOnSchematicGraphNodeAdded 等)实现模型到视图的自动同步
- 自定义 RTTI 系统:通过宏
SCHEMATICGRAPHELEMENT_BODY_BASE实现的轻量运行时类型识别,支持安全的 Cast 操作,且不依赖 UObject 反射 - 动画过渡:所有视觉属性(位置、大小、颜色、透明度)都通过
TAnimatedAttribute实现平滑插值,用户体验流畅 - 可扩展的节点类型:通过继承
FSchematicGraphNode可以创建自定义节点类型,通过覆写虚方法控制位置、大小、颜色、可见性等 - 自动分组与缩放:面板内置了自动分组(将靠近的节点自动归入一个 GroupNode)和自动缩放(防止节点重叠)功能
- 编辑器无关:模块不依赖 UnrealEd,可以被任何编辑器工具复用
- 拖拽支持:完整实现了从内部拖拽到外部拖入的全流程
2.7 潜在改进点
- 连线绘制被注释掉了:
SSchematicGraphPanel::OnPaint()中连线的绘制代码整个被/* */注释掉了,目前连线数据虽然可以添加和管理,但不会在视图中渲染 - 鼠标滚轮被禁用:
OnMouseWheel()直接返回Unhandled,缩放功能不可用 - 缺少序列化:数据模型目前没有实现序列化接口,无法保存/加载图表布局
- Undo/Redo 缺失:没有集成 UE 的 Transaction 系统,编辑操作不可撤销
- 性能优化空间:
UpdateAutoScalingForNodes()使用 O(n²) 的暴力距离检测,注释中也标注了"todo: use a faster distance algorithm" - 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 系统自带反射和类型判断,但 FSchematicGraphNode、FSchematicGraphLink、FSchematicGraphTag 都是纯 C++ 类(不继承 UObject),无法使用 UE 的 Cast<>。因此,引擎开发者自己实现了一套轻量级的运行时类型识别系统。
核心宏:SCHEMATICGRAPHELEMENT_BODY_BASE
这个宏用于类型继承树的根部。它做了以下几件事:
- 定义一个静态
Type成员(FName),存储类名 - 提供虚函数
GetType()和IsA() - 提供模板化的
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 宏有三个参数:ClassName、SuperClass、BaseClass。第三个参数用于 Cast 友元函数中的类型转换——因为 Cast 函数必须定义在继承链的基类中,而不是当前类中。例如 FSchematicGraphGroupNode 的 Cast 友元实际上转发到 FSchematicGraphNode 的 Cast:
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) 查找 LinkByHash用HashCombine(SourceGuid, TargetGuid)做键,实现 O(1) 的"按源-目标对查找连线"NodeGuidToLinkGuids的TTuple中,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() — 删除节点时自动清理关联的连线和父子关系:
- 通过
NodeGuidToLinkGuids找到所有关联连线并删除 - 清理
LastExpandedNode引用 - 递归移除所有子节点
- 从父节点中移除自己
连线管理
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; // 标记列表
图层系统
每个节点可以有多个渲染图层,每个图层由"颜色 + 笔刷"组成。默认构造时创建三层:
| 图层 | 笔刷 | 颜色 | 用途 |
|---|---|---|---|
| 0 | Schematic.Background | 白色 40% 透明 | 节点背景 |
| 1 | Schematic.Outline.Single | 白色 | 节点轮廓 |
| 2 | Schematic.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() — 复杂的可见性判断逻辑:
- 如果自身 Visibility == Hidden,直接返回 Hidden
- 如果存在
LastExpandedNode且它不是自己或父节点,返回 FadedOut - 如果父节点指定了子节点可见性,返回父节点的指定值
- 否则返回自身 Visibility
鼠标交互:
OnMouseEnter()— 缩放偏移设为 1.25(放大)OnMouseLeave()— 缩放偏移恢复为 1.0OnDragOver()— 缩放偏移设为 0.75(缩小),并展开所在 GroupNodeOnDragLeave()— 恢复缩放,折叠所在 GroupNode
3.6 FSchematicGraphGroupNode — 可展开组节点
概述
FSchematicGraphGroupNode 继承自 FSchematicGraphNode,增加了展开/折叠功能。当展开时,子节点会以圆形排列在组节点周围;折叠时,子节点隐藏,组节点以一个特殊圆点显示。
展开/折叠动画
使用 TAnimatedAttribute<float> 实现展开状态的平滑过渡:
TSharedPtr<TAnimatedAttribute<float>> ExpansionState;
// 0.0 = 完全折叠
// 1.0 = 完全展开
SetExpanded() 的逻辑:
- 展开时,如果子节点数为 0,直接返回
- 折叠时且已完全折叠,直接停止动画
- 正常切换时,设置目标值(0 或 1),并设置延迟(drag 时有延迟,click 时无延迟)
- 递归处理父组节点
- 折叠时,递归折叠所有子组节点
子节点圆形布局
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)。它负责:
- 作为数据模型的观察者,响应增删通知
- 管理所有
SSchematicGraphNode子 Widget - 每帧 Tick 更新动画状态
- 实现自动分组和自动缩放
- 处理拖拽事件
数据模型绑定
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() 的核心逻辑:
- 遍历所有无父节点的可见节点
- 将节点位置除以
AutoGroupingDistance(默认 4.0),取整得到整数坐标 - 相同整数坐标的节点归为一组(HashCombine 生成 hash)
- 过滤掉只有 0 或 1 个节点的组
- 为每个有效组创建/复用
FSchematicGraphAutoGroupNode - 对组内节点组合计算 CombinedHash,用于判断组是否发生变化
- 清理不再需要的组节点
自动缩放算法
UpdateAutoScalingForNodes() 的核心逻辑:
- 对每对启用了自动缩放的节点,计算它们之间的距离
- 如果距离小于两个节点半径之和(含 padding),计算需要缩小的比例
- 取每个节点所有重叠计算中的最大缩小比例
- 将缩小比例 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 核心要点
-
MVC 分离是核心设计理念:数据模型(FSchematicGraphModel / FSchematicGraphNode / FSchematicGraphLink / FSchematicGraphTag)完全不依赖 Slate,视图层(SSchematicGraphPanel / SSchematicGraphNode)通过委托订阅数据变更。这种设计让模型可以独立测试、序列化、复用。
-
自定义 RTTI 是精巧的工程实践:在不依赖 UObject 反射的前提下,通过宏实现了一套完整的运行时类型识别系统。
SCHEMATICGRAPHELEMENT_BODY_BASE/SCHEMATICGRAPHELEMENT_BODY这套宏值得在类似的纯 C++ 类继承体系中使用。 -
动画属性提升用户体验:
TAnimatedAttribute<T>配合TEasingAttributeInterpolator<T>实现了节点位置、大小、颜色、透明度的平滑过渡,让整个面板的操作体验非常流畅。所有"硬切"的视觉变化都被替换成了"软过渡"。 -
自动分组是智能化的体现:
UpdateAutoGroupingForNodes()和UpdateAutoScalingForNodes()让面板具备了"感知"节点空间关系的能力,自动将密集聚集的节点折叠成组,自动缩小重叠的节点。 -
父子层级关系灵活而强大:通过
ParentNodeGuid/ChildNodeGuids建立的父子关系,配合虚方法GetPositionForChildNode()/GetScaleForChildNode()/GetVisibilityForChildNode(),让 GroupNode 可以完全控制子节点的视觉表现。
5.2 最佳实践
-
创建自定义节点类型:继承
FSchematicGraphNode并覆写虚方法,而不是在外部代码中通过 if-else 判断节点类型来处理差异。 -
使用 Tag 系统传递元信息:不要把所有信息都塞进节点 Label,善用 Tag 来标记节点的状态、类型、属性等。
-
通过委托响应变更:使用
OnNodeAdded/OnNodeRemoved等委托来响应数据模型变更,而不是轮询。 -
合理设置 AutoGroupingDistance:根据节点的实际大小和间距需求调整这个值,默认 4.0 像素可能在不同 DPI 下表现不同。
-
注意 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 — 引擎的通用图表编辑器框架,本模块可以与其配合使用

213

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



