目标
UE4编辑器中的蓝图编辑器、材质编辑器等都是继承自UEdGraph
的图表编辑器。插件中是可以拓展一种新的图表编辑器以实现特定目的的界面的,例如官方内建插件中的Niagara
和AssetManagerEditor
都拓展了新的图表编辑器。
本篇的目标是观察相关的基础概念并创建一个最简单的图表窗口。我主要参考了AssetManagerEditor
中的ReferenceViewer(引用关系查看器),因为这是我目前发现的最简单的图表编辑器。
图表编辑器相关的基本概念
UEdGraph
UCLASS()
class ENGINE_API UEdGraph : public UObject
UEdGraph
代表了一个图表对象。
不出意外,可以看到节点列表是它的成员:
/** Set of all nodes in this graph */
UPROPERTY()
TArray<class UEdGraphNode*> Nodes;
为了创建一个新的图表,需要定义一个新的UEdGraph
。
UEdGraphNode
UCLASS()
class ENGINE_API UEdGraphNode : public UObject
UEdGraphNode
代表了图表对象中的一个节点。
它有节点上的引脚信息:
TArray<UEdGraphPin*> Pins;
为了创建一个新的图表,需要定义一个新的UEdGraphNode
。
UEdGraphSchema
UCLASS(abstract)
class ENGINE_API UEdGraphSchema : public UObject
暂时没有想到准确的对于Schema的翻译,我暂时的理解是它描述了图表对象的一种“规则”,例如:
/**
* Get all actions that can be performed when right clicking on a graph or drag-releasing on a graph from a pin
*
* @param [in,out] ContextMenuBuilder The context (graph, dragged pin, etc...) and output menu builder.
*/
virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const;
GetGraphContextActions
得到了所有“鼠标右键点击”或者“拖拽一个引脚并释放”所能进行的操作。
每个UEdGraph
都必须指定一个UEdGraphSchema
。
SGraphNode
class GRAPHEDITOR_API SGraphNode : public SNodePanel::SNode
SGraphNode
是节点在图表编辑器中具体的UI界面。想要改变节点的界面,就重新定义一个新的SGraphNode
。
SGraphEditor
class SGraphEditor : public SCompoundWidget
SGraphEditor
封装了图表编辑器的界面。
创建控件时需要指定图表对象:
SLATE_ARGUMENT( UEdGraph*, GraphToEdit )
0.新建插件
以Editor Standalone Window为模板创建一个插件,这将提供一个编辑器独立窗口,方便之后的实验。
插件名为YaksueGraphPlg
随后关卡编辑器上方工具栏会有一个新的按钮:
点击后会创建窗口:
正如文字中显示,之后的界面内容可以放在FYaksueGraphPlgModule::OnSpawnPluginTab
中
1.空白的图表界面
1.1 定义一个空白的UEdGraph
EdGraph_Yaksue.h
:
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph_Yaksue.generated.h"
UCLASS()
class UEdGraph_Yaksue : public UEdGraph
{
GENERATED_UCLASS_BODY()
};
EdGraph_Yaksue.cpp
:
#include "EdGraph_Yaksue.h"
UEdGraph_Yaksue::UEdGraph_Yaksue(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
1.2 定义一个空白的UEdGraphSchema
YaksueGraphSchema.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "EdGraph/EdGraphSchema.h"
#include "YaksueGraphSchema.generated.h"
UCLASS(MinimalAPI)
class UYaksueGraphSchema : public UEdGraphSchema
{
GENERATED_UCLASS_BODY()
};
YaksueGraphSchema.cpp
:
#include "YaksueGraphSchema.h"
UYaksueGraphSchema::UYaksueGraphSchema(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
1.3 定义一个窗口界面
SYaksueGraphWindow.h
:
#pragma once
#include "CoreMinimal.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SCompoundWidget.h"
#include "GraphEditor.h"
class SYaksueGraphWindow : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SYaksueGraphWindow){}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct( const FArguments& InArgs );
//图表编辑器控件
TSharedPtr<SGraphEditor> GraphEditorPtr;
//图表对象
class UEdGraph_Yaksue* GraphObj;
};
其中GraphEditorPtr
是SGraphEditor
控件。而GraphObj
是图表对象。
SYaksueGraphWindow.cpp
:
#include "SYaksueGraphWindow.h"
#include "EdGraph_Yaksue.h"
#include "YaksueGraphSchema.h"
void SYaksueGraphWindow::Construct(const FArguments& InArgs)
{
//创建图表对象
GraphObj = NewObject<UEdGraph_Yaksue>();
GraphObj->Schema = UYaksueGraphSchema::StaticClass();
GraphObj->AddToRoot();
//创建图表编辑器控件
GraphEditorPtr = SNew(SGraphEditor)
.GraphToEdit(GraphObj);
//指定本控件的UI:
ChildSlot
[
GraphEditorPtr.ToSharedRef()
];
}
1.4 将窗口界面加入Tab界面中
#include"SYaksueGraphWindow.h"
TSharedRef<SDockTab> FYaksueGraphPlgModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
// Put your tab content here!
SNew(SYaksueGraphWindow)
];
}
效果:
2.创建一个图表节点
2.1 定义一个空白的图表节点
EdGraphNode_Yaksue.h
:
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraphNode_Yaksue.generated.h"
UCLASS()
class UEdGraphNode_Yaksue : public UEdGraphNode
{
GENERATED_UCLASS_BODY()
};
EdGraphNode_Yaksue.cpp
:
#include "EdGraphNode_Yaksue.h"
UEdGraphNode_Yaksue::UEdGraphNode_Yaksue(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
2.2在图表中加入节点
在UEdGraph_Yaksue
中加入一个新的接口:
//重新构建图表
void RebuildGraph();
RebuildGraph
所做的就是创建一个新的节点:
void UEdGraph_Yaksue::RebuildGraph()
{
//创建一个节点
CreateNode(UEdGraphNode_Yaksue::StaticClass());
}
2.3 在窗口中调用RebuildGraph
GraphObj->RebuildGraph();
效果:
3.自定义节点UI
虽然没有SGraphNode
与EdGraphNode_Yaksue
对应,但是再上一步中已经能看到节点的界面了,我想这大概是因为在没有对应SGraphNode
时,会给一个默认的SGraphNode
。
而在这一步中,我将创建一个新的SGraphNode
来自定义节点UI。
3.1 定义一个新的节点控件SGraphNode
SYaksueGraphNode.h
:
#pragma once
#include "CoreMinimal.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "SGraphNode.h"
class UEdGraphNode_Yaksue;
class SYaksueGraphNode : public SGraphNode
{
public:
SLATE_BEGIN_ARGS(SYaksueGraphNode){}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct( const FArguments& InArgs, UEdGraphNode_Yaksue* InNode );
// SGraphNode implementation
virtual void UpdateGraphNode() override;
// End SGraphNode implementation
};
SYaksueGraphNode.cpp
:
#include "SYaksueGraphNode.h"
#include "EdGraphNode_Yaksue.h"
#include "Widgets/Input/SButton.h"
void SYaksueGraphNode::Construct( const FArguments& InArgs, UEdGraphNode_Yaksue* InNode )
{
GraphNode = InNode;
UpdateGraphNode();
}
void SYaksueGraphNode::UpdateGraphNode()
{
GetOrAddSlot( ENodeZone::Center )
[
SNew(SButton).Text(FText::FromString("Hello Yaksue"))
];
}
UpdateGraphNode()
中将指定界面内容,这里很简单,就是一个按钮,写着"Hello Yaksue"。
3.2 将节点UI和节点类型对应
先定义一个FGraphPanelNodeFactory
(图表节点工厂),将UEdGraphNode_Yaksue
与SYaksueGraphNode
对应起来:
//定义图表节点工厂
class FYaksueGraphNodeFactory : public FGraphPanelNodeFactory
{
virtual TSharedPtr<class SGraphNode> CreateNode(UEdGraphNode* Node) const override
{
if (UEdGraphNode_Yaksue* YaksueGraphNode = Cast<UEdGraphNode_Yaksue>(Node))
{
return SNew(SYaksueGraphNode, YaksueGraphNode);
}
return nullptr;
}
};
然后在插件模块的启动函数中注册这个工厂:
//注册图表节点工厂
FEdGraphUtilities::RegisterVisualNodeFactory(MakeShareable(new FYaksueGraphNodeFactory()));
现在,节点的界面变成了:
总结
至此,“创建一个最简单的图表编辑器”这一目标已经达成,不过关于UE4的图表编辑器的学习只是一个开始,后续还有很多问题,例如:
- 节点之间的连接关系是怎样的系统?
- 如何安排右键菜单中的选择?
- 等等。。。