对UE的编辑器实现有点好奇,于是从比较熟悉的行为树编辑器着手分析。以下为阅读UE源码后的个人理解,如有错误请指正。
编辑器基础
扩展编辑器的几种方式
- MenuBar 菜单栏
- ToolBar 工具栏
- DetailCustomization 自定义细节面板,支持两种方式:IDetailCustomization自定义整个细节面板、 IPropertyTypeCustomization只对特定类型字段进行自定义。
- GraphNode 节点的扩展。例如对现有蓝图节点的扩充。
- Custom Asset Editor 自定义一个新的资源类型并拥有独立的编辑器。
- Custom Edit Mode 自定义编辑模式。类似于:地形编辑模式、植被编辑模式......
- Editor Utility Widgets
- Scripted Actions:Create Blueprints that you can invoke by right-clicking an Asset in the Content Browser or an Actor in the Level.
推荐视频教程:
- 虚幻引擎 4 的编辑器扩展 柴云天
- Extending the Editor: Making the Most of Unreal Engine's Existing Framework | Unreal Fest 2024
编辑器核心概念及关键类
-
Menu 菜单栏
-
ToolBar 工具栏
-
Tab tab页
-
TabManager tab页管理器
-
Layout 布局
- FLayoutNode 布局节点,有一个尺寸系数。
- FSplitter 布局切分,可横向或者纵向进行切分。继承自FLayoutNode
- FArea 继承自FSplitter
- FStack 可以承载多个tab页,但只有一个在前台(激活态)。继承自FLayoutNode
-
FToolkitManager
单例,管理所有编辑器实例。主要函数:FindEditorForAsset尝试找到资源所对应的编辑器。
-
UAssetEditorSubsystem
资源编辑器子系统,记录了当前所有正在编辑的资源。主要函数:OpenEditorForAsset打开资源编辑器、FindEditorForAsset查找资源对应的编辑器、GetAllEditedAssets获取所有当前编辑中的资源......
其它
- UEditorAssetSubsystem:暴露资源相关工具方法给蓝图侧
- UEditorActorSubsystem:暴露关卡中Actor相关工具方法给蓝图侧
行为树编辑器代码分析
行为树编辑器代码目录结构如下:
- FBehaviorTreeEditorModule
行为树编辑器模块类,提供了创建行为树编辑器的接口,在模块启动的时候注册了对detail面板的自定义、注册了行为树节点对应可视节点的工厂方法、创建了菜单扩展管理器以及工具栏扩展管理器。 - FBehaviorTreeDebugger
行为树调试器:提供断点的添加、移除,单步步进等。待进一步详细分析。
行为树编辑器逻辑层(Model)
- 资源定义
- UAssetDefinition_BehaviorTree:行为树的资源定义,定义了资源在Content Drawer中的名字的显示、颜色、分类、被双击所执行的操作、文件对比。
- UAssetDefinition_Blackboard:黑板的资源定义,同上。
- 工厂类
- UBehaviorTreeFactory行为树工厂类
- UBlackboardDataFactory黑板工厂类
- UBehaviorTreeGraph行为树图,也就是我们平常编辑的行为树的逻辑表示。
- UBehaviorTreeGraphNode行为树图中节点的基类
- UBehaviorTreeGraphNode_Root行为树图的根节点
- UBehaviorTreeGraphNode_Composite行为树图中复合节点
- UBehaviorTreeGraphNode_SimpleParallel
- UBehaviorTreeGraphNode_Decorator
- UBehaviorTreeGraphNode_Service
- UBehaviorTreeGraphNode_Task
- UBehaviorTreeGraphNode_SubtreeTask
- UBehaviorTreeGraphNode_CompositeDecorator复合装饰器节点,里面包含了一个Graph,从而可以通过与或非来组合重利用已有装饰器
- UBehaviorTreeDecoratorGraph复合装饰器节点的Graph
- UEdGraphSchema_BehaviorTreeDecorator定义了UBehaviorTreeDecoratorGraph的行为模式
- UBehaviorTreeDecoratorGraphNode复合装饰器Graph中节点基类
- UBehaviorTreeDecoratorGraphNode_Logic逻辑节点(与、或、非),继承自UBehaviorTreeDecoratorGraphNode
- UBehaviorTreeDecoratorGraphNode_Decorator装饰器节点,继承自UBehaviorTreeDecoratorGraphNode
- UEdGraphSchema_BehaviorTree定义了UBehaviorTreeGraph的行为模式,例如:右键菜单有哪些选项、两个pin是否能够连接、节点的右键菜单列表、连接两个pin......
行为树编辑器控制层(Controller)
- FBehaviorTreeEditor
- BehaviorTreeEditorCommands文件中定义了一些命令:行为树的搜索、新建黑板、调试相关的命令(步进、暂停、恢复、停止......)等。
- BehaviorTreeEditorModes:行为树编辑器的两个工作模式(行为树模式、黑板模式)。模式类中定义了当前模式下Tab的默认布局、注册模式中所有Tab的工厂方法、菜单及工具栏的扩展器。
- 行为树模式类FBehaviorTreeEditorApplicationMode
- 黑板模式类FBlackboardEditorApplicationMode
- BehaviorTreeEditorTabs定义了所有Tab的标识符:行为树Tab、行为树属性面板Tab、行为树搜索面板Tab、黑板属性面板Tab、黑板Key面板。
- BehaviorTreeEditorTabFactories文件:包含了几个Tab的工厂方法类。
- FBlackboardEditorSummoner黑板编辑页的工厂类
- FBlackboardDetailsSummoner黑板detail面板的工厂类
- FBehaviorTreeDetailsSummoner行为树detail面板的工厂类
- FBehaviorTreeSearchSummoner行为树搜索面板的工厂类
- FBTGraphEditorSummoner行为树编辑面板的工厂类
- FBlackboardSummoner黑板View面板(方便运行中查看黑板的值)的工厂类
- FBehaviorTreeEditorToolbar负责工具栏
行为树编辑器UI层(View)
- SBehaviorTreeBlackboardEditor黑板Key编辑面板。
- SBehaviorTreeBlackboardView黑板条目展示。
- SGraphNode_BehaviorTree负责单个行为树节点的绘制
- SGraphNode_Decorator负责复合UBehaviorTreeDecoratorGraphNode_Decorator节点的绘制。
- BehaviorTreeColors定义了各类节点的颜色、Pin在不同状态下的颜色、调试相关的颜色...
- BehaviorTreeConnectionDrawingPolicy:行为树种节点之间连线的绘制策略(是否绘制流动的泡泡效果、线的粗线、颜色等)
- DetailCustomizations文件夹:定制的一些细节面板(黑板detail面板、BlackboardKeySelector)
- FindInBT负责行为树中的搜索,包括搜索逻辑以及UI。
- SBehaviorTreeDiff行为树对比界面。
实现细节
入口
从行为树资源被双击打开编辑器作为切入口。
UAssetDefinition_BehaviorTree
c++
EAssetCommandResult UAssetDefinition_BehaviorTree::OpenAssets(const FAssetOpenArgs& OpenArgs) const
{
// 省略其余代码.....
if (!bFoundExisting)
{
FBehaviorTreeEditorModule& BehaviorTreeEditorModule = FModuleManager::GetModuleChecked<FBehaviorTreeEditorModule>("BehaviorTreeEditor");
BehaviorTreeEditorModule.CreateBehaviorTreeEditor(Mode, OpenArgs.ToolkitHost, BehaviorTree); // 向行为树编辑器模块儿求情创建行为树编辑器
}
}
return EAssetCommandResult::Handled;
}
行为树编辑器模块创建了FBehaviorTreeEditor
c++
TSharedRef<IBehaviorTreeEditor> FBehaviorTreeEditorModule::CreateBehaviorTreeEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, UObject* Object )
{
if (!ClassCache.IsValid())
{
// 监听行为树蓝图节点类UBTTask_BlueprintBase、UBTDecorator_BlueprintBase、UBTService_BlueprintBase
}
TSharedRef< FBehaviorTreeEditor > NewBehaviorTreeEditor( new FBehaviorTreeEditor() );
NewBehaviorTreeEditor->InitBehaviorTreeEditor( Mode, InitToolkitHost, Object );
return NewBehaviorTreeEditor;
}
FBehaviorTreeEditor定义
c++
class BEHAVIORTREEEDITOR_API FBehaviorTreeEditor : public IBehaviorTreeEditor,
public FAIGraphEditor, public FNotifyHook
{
//...
}
编辑器初始化
FBehaviorTreeEditor的初始化函数InitBehaviorTreeEditor
c++
void FBehaviorTreeEditor::InitBehaviorTreeEditor( const EToolkitMode::Type Mode,
const TSharedPtr< class IToolkitHost >& InitToolkitHost, UObject* InObject ){
// DocumentManager初始化(DocumentManager主要记录了双击行为树节点打开的蓝图面板)
// 工具栏构建器ToolbarBuilder的创建
// 命令注册(行为树相关命令、行为树调试相关命令、黑板相关命令)
// 基类FAssetEditorToolkit的初始化InitAssetEditor
// 预创建Detail面板
// 行为树调试器的创建和初始化
// 扩展菜单栏、工具栏
// 创建并添加黑板模式FBlackboardEditorApplicationMode、行为树模式FBehaviorTreeEditorApplicationMode
// 预创建黑板编辑器面板
// 命令到回调的映射
// 设置当前的工作模式(行为树模型、黑板模式),注册TabFactory,从编辑器布局ini配置文件中读取布局信息,恢复UI布局(这里会调用前面注册的各个Tab的工厂方法)。
// 重新生成菜单栏、工具栏
}
Tab工厂类被调用时的堆栈:
设置当前运行模式的时候会恢复模式对应的编辑器UI布局,而布局包含了一组tab标识符,根据tab标识符找到之前注册的tab工厂类,再由工厂类的CreateTabBody函数来创建tab面板。当所有tab面板都创建完毕的时候,编辑器也就完成了创建。
行为树编辑面板
行为树编辑面板的创建代码在FBehaviorTreeEditor::CreateGraphEditorWidget。
C++
/** Create new tab for the supplied graph - don't call this directly, call SExplorer->FindTabForGraph.*/
TSharedRef<SGraphEditor> FBehaviorTreeEditor::CreateGraphEditorWidget(UEdGraph* InGraph)
{
check(InGraph != NULL);
if (!GraphEditorCommands.IsValid())
{
CreateCommandList();
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().RemoveExecutionPin,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnRemoveInputPin ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanRemoveInputPin )
);
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().AddExecutionPin,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnAddInputPin ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanAddInputPin )
);
// Debug actions
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().AddBreakpoint,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnAddBreakpoint ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanAddBreakpoint ),
FIsActionChecked(),
FIsActionButtonVisible::CreateSP( this, &FBehaviorTreeEditor::CanAddBreakpoint )
);
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().RemoveBreakpoint,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnRemoveBreakpoint ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanRemoveBreakpoint ),
FIsActionChecked(),
FIsActionButtonVisible::CreateSP( this, &FBehaviorTreeEditor::CanRemoveBreakpoint )
);
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().EnableBreakpoint,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnEnableBreakpoint ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanEnableBreakpoint ),
FIsActionChecked(),
FIsActionButtonVisible::CreateSP( this, &FBehaviorTreeEditor::CanEnableBreakpoint )
);
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().DisableBreakpoint,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnDisableBreakpoint ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanDisableBreakpoint ),
FIsActionChecked(),
FIsActionButtonVisible::CreateSP( this, &FBehaviorTreeEditor::CanDisableBreakpoint )
);
GraphEditorCommands->MapAction( FGraphEditorCommands::Get().ToggleBreakpoint,
FExecuteAction::CreateSP( this, &FBehaviorTreeEditor::OnToggleBreakpoint ),
FCanExecuteAction::CreateSP( this, &FBehaviorTreeEditor::CanToggleBreakpoint ),
FIsActionChecked(),
FIsActionButtonVisible::CreateSP( this, &FBehaviorTreeEditor::CanToggleBreakpoint )
);
}
SGraphEditor::FGraphEditorEvents InEvents;
InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FBehaviorTreeEditor::OnSelectedNodesChanged);
InEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &FBehaviorTreeEditor::OnNodeDoubleClicked);
InEvents.OnTextCommitted = FOnNodeTextCommitted::CreateSP(this, &FBehaviorTreeEditor::OnNodeTitleCommitted);
// Make title bar
TSharedRef<SWidget> TitleBarWidget =
SNew(SBorder)
.BorderImage( FAppStyle::GetBrush( TEXT("Graph.TitleBackground") ) )
.HAlign(HAlign_Fill)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.HAlign(HAlign_Center)
.FillWidth(1.f)
[
SNew(STextBlock)
.Text(TitleText)
.TextStyle( FAppStyle::Get(), TEXT("GraphBreadcrumbButtonText") )
]
];
// Make full graph editor
const bool bGraphIsEditable = InGraph->bEditable;
return SNew(SGraphEditor)
.AdditionalCommands(GraphEditorCommands)
.IsEditable(this, &FBehaviorTreeEditor::InEditingMode, bGraphIsEditable)
.Appearance(this, &FBehaviorTreeEditor::GetGraphAppearance)
.TitleBar(TitleBarWidget)
.GraphToEdit(InGraph)
.GraphEvents(InEvents)
.AutoExpandActionMenu(true);
}
可以看到行为树编辑面板复用的SGraphEditor。
节点以及连线的绘制在SGraphEditor子UI SGraphPanel的OnPaint中,代码比较长这里就不放了。
SGraphNode_BehaviorTree负责行为树中单个节点的绘制。