上一节实现了一个简单的预览视口,接下来学习如何编写一个节点编辑器
EdGraph
一个最简单的节点编辑器由以下四大件组成
1、UEdGraph
数据容器,记录了所有的节点数据和图规则 (Schema)
2、UEdGraphNode
节点数据,记录了节点的名称、引脚、坐标和内嵌数据等
3、UEdGraphSchema
定义这张图的类型规则和交互规则
4、SGraphEditor
Slate 控件,用来显示和编辑 EdGraph

实现
接下来实现一个简单的节点编辑器并支持导出为JSON文件
MyEdGraph.h
套一层,暂无需要实现的内容
cpp
#pragma once
#include "CoreMinimal.h"
#include "EdGraph/EdGraph.h"
#include "MyEdGraph.generated.h"
UCLASS()
class UMyEdGraph : public UEdGraph
{
GENERATED_BODY()
};
MyEdGraphNode.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "EdGraph/EdGraphNode.h"
#include "MyEdGraphNode.generated.h"
UCLASS()
class UMyEdGraphNode : public UEdGraphNode
{
GENERATED_BODY()
public:
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual void AllocateDefaultPins() override PURE_VIRTUAL(UMyEdGraphNode::AllocateDefaultPins, );
virtual FLinearColor GetNodeTitleColor() const override { return FLinearColor(0.1f, 0.4f, 0.8f); }
virtual bool CanUserDeleteNode() const override { return true; }
virtual bool CanDuplicateNode() const override { return true; }
virtual TSharedRef<FJsonObject> ToJSON() const;
protected:
UPROPERTY(EditAnywhere, Category = "Node")
FString DisplayName = TEXT("Node");
};
/**
* 输出整数
*/
UCLASS()
class UMyEdGraphNode_Int : public UMyEdGraphNode
{
GENERATED_BODY()
public:
UMyEdGraphNode_Int();
virtual void AllocateDefaultPins() override;
virtual FLinearColor GetNodeTitleColor() const override { return FLinearColor(1.f, 0.f, 0.f); }
virtual TSharedRef<FJsonObject> ToJSON() const override;
UPROPERTY(EditAnywhere, Category = "Value")
int32 Value = 0;
};
/**
* 整数加和
*/
UCLASS()
class UMyEdGraphNode_Add : public UMyEdGraphNode
{
GENERATED_BODY()
public:
UMyEdGraphNode_Add();
virtual void AllocateDefaultPins() override;
virtual FLinearColor GetNodeTitleColor() const override { return FLinearColor(0.f, 1.f, 0.f); }
};
/**
* 打印
*/
UCLASS()
class UMyEdGraphNode_Print : public UMyEdGraphNode
{
GENERATED_BODY()
public:
UMyEdGraphNode_Print();
virtual void AllocateDefaultPins() override;
virtual FLinearColor GetNodeTitleColor() const override { return FLinearColor(0.f, 0.f, 1.f); }
virtual TSharedRef<FJsonObject> ToJSON() const override;
UPROPERTY(EditAnywhere, Category = "Print")
FString Prefix = TEXT("Log:");
};
MyEdGraphNode.cpp
cpp
#include "MyEdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
namespace MyPinCategory
{
const FName PC_Int = TEXT("int");
const FName PC_Exec = TEXT("exec");
}
FText UMyEdGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
return FText::FromString(DisplayName);
}
TSharedRef<FJsonObject> UMyEdGraphNode::ToJSON() const
{
TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
JsonObject->SetStringField(TEXT("DisplayName"), *DisplayName);
return JsonObject;
}
UMyEdGraphNode_Int::UMyEdGraphNode_Int()
{
DisplayName = TEXT("Int");
}
void UMyEdGraphNode_Int::AllocateDefaultPins()
{
CreatePin(EGPD_Output, MyPinCategory::PC_Int, TEXT("Value"));
}
TSharedRef<FJsonObject> UMyEdGraphNode_Int::ToJSON() const
{
TSharedRef<FJsonObject> JsonObject = UMyEdGraphNode::ToJSON();
JsonObject->SetNumberField(TEXT("IntValue"), Value);
return JsonObject;
}
UMyEdGraphNode_Add::UMyEdGraphNode_Add()
{
DisplayName = TEXT("Add");
}
void UMyEdGraphNode_Add::AllocateDefaultPins()
{
CreatePin(EGPD_Input, MyPinCategory::PC_Int, TEXT("A"));
CreatePin(EGPD_Input, MyPinCategory::PC_Int, TEXT("B"));
CreatePin(EGPD_Output, MyPinCategory::PC_Int, TEXT("Result"));
}
UMyEdGraphNode_Print::UMyEdGraphNode_Print()
{
DisplayName = TEXT("Print");
}
void UMyEdGraphNode_Print::AllocateDefaultPins()
{
CreatePin(EGPD_Input, MyPinCategory::PC_Exec, TEXT("Exec"));
CreatePin(EGPD_Input, MyPinCategory::PC_Int, TEXT("Value"));
}
TSharedRef<FJsonObject> UMyEdGraphNode_Print::ToJSON() const
{
TSharedRef<FJsonObject> JsonObject = UMyEdGraphNode::ToJSON();
JsonObject->SetStringField(TEXT("Prefix"), *Prefix);
return JsonObject;
}
MyEdGraphSchema.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "EdGraph/EdGraphSchema.h"
#include "MyEdGraphSchema.generated.h"
class UMyEdGraphNode;
USTRUCT()
struct FMyEdGraphSchemaAction_NewNode : public FEdGraphSchemaAction
{
GENERATED_BODY()
UPROPERTY()
TSubclassOf<UMyEdGraphNode> NodeClass;
FMyEdGraphSchemaAction_NewNode() = default;
FMyEdGraphSchemaAction_NewNode(
const FText& InCategory,
const FText& InMenuDesc,
const FText& InToolTip,
int32 InGrouping,
TSubclassOf<UMyEdGraphNode> InNodeClass)
: FEdGraphSchemaAction(InCategory, InMenuDesc, InToolTip, InGrouping)
, NodeClass(InNodeClass)
{
}
// 响应Action
virtual UEdGraphNode* PerformAction(
UEdGraph* ParentGraph,
UEdGraphPin* FromPin,
const FVector2D Location,
bool bSelectNewNode = true) override;
};
UCLASS()
class UMyEdGraphSchema : public UEdGraphSchema
{
GENERATED_BODY()
public:
// 检查两根引脚是否可以连接
virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const override;
// 获取引脚类型颜色
virtual FLinearColor GetPinTypeColor(const FEdGraphPinType& PinType) const override;
// 断开单个引脚连接
virtual void BreakSinglePinLink(UEdGraphPin* SourcePin, UEdGraphPin* TargetPin) const override;
// 空白处右键菜单
virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override;
};
MyEdGraphSchema.cpp
cpp
#include "MyEdGraphSchema.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraph/EdGraphNode.h"
#include "MyEdGraphNode.h"
#define LOCTEXT_NAMESPACE "MyEdGraphSchema"
UEdGraphNode* FMyEdGraphSchemaAction_NewNode::PerformAction(
UEdGraph* ParentGraph,
UEdGraphPin* FromPin,
const FVector2D Location,
bool bSelectNewNode)
{
if (!ParentGraph || !*NodeClass)
{
return nullptr;
}
UMyEdGraphNode* NewNode = NewObject<UMyEdGraphNode>(ParentGraph, NodeClass, NAME_None, RF_Transactional);
NewNode->CreateNewGuid();
NewNode->AllocateDefaultPins();
NewNode->NodePosX = static_cast<int32>(Location.X);
NewNode->NodePosY = static_cast<int32>(Location.Y);
ParentGraph->Modify();
ParentGraph->AddNode(NewNode, true, bSelectNewNode);
if (FromPin)
{
for (UEdGraphPin* Pin : NewNode->Pins)
{
if (Pin && Pin->Direction != FromPin->Direction &&
Pin->PinType.PinCategory == FromPin->PinType.PinCategory)
{
FromPin->MakeLinkTo(Pin);
break;
}
}
}
ParentGraph->NotifyGraphChanged();
return NewNode;
}
const FPinConnectionResponse UMyEdGraphSchema::CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const
{
if (!A || !B)
{
return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Invalid pin"));
}
if (A->GetOwningNode() == B->GetOwningNode())
{
return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Cannot connect to self"));
}
if (A->Direction == B->Direction)
{
return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Directions must differ"));
}
if (A->PinType.PinCategory != B->PinType.PinCategory)
{
return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Types don't match"));
}
const UEdGraphPin* InputPin = (A->Direction == EGPD_Input) ? A : B;
if (InputPin->LinkedTo.Num() > 0)
{
return FPinConnectionResponse(
(InputPin == A) ? CONNECT_RESPONSE_BREAK_OTHERS_A : CONNECT_RESPONSE_BREAK_OTHERS_B,
TEXT("Replace existing input"));
}
return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, TEXT(""));
}
FLinearColor UMyEdGraphSchema::GetPinTypeColor(const FEdGraphPinType& PinType) const
{
if (PinType.PinCategory == TEXT("int"))
return FLinearColor(1.f, 1.f, 0.f);
if (PinType.PinCategory == TEXT("exec"))
return FLinearColor::White;
if (PinType.PinCategory == TEXT("string"))
return FLinearColor(0.f, 1.f, 1.f);
return FLinearColor::Gray;
}
void UMyEdGraphSchema::BreakSinglePinLink(UEdGraphPin* SourcePin, UEdGraphPin* TargetPin) const
{
SourcePin->BreakLinkTo(TargetPin);
}
void UMyEdGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const
{
auto Add = [](FGraphContextMenuBuilder& Builder,
TSubclassOf<UMyEdGraphNode> NodeClass,
const FText& Category,
const FText& MenuDesc,
const FText& Tooltip)
{
TSharedPtr<FMyEdGraphSchemaAction_NewNode> Action = MakeShared<FMyEdGraphSchemaAction_NewNode>(
Category, MenuDesc, Tooltip, 0, NodeClass);
Builder.AddAction(Action);
};
const FText Category = LOCTEXT("MyCustomNode", "MyCustomNode");
Add(ContextMenuBuilder,
UMyEdGraphNode_Int::StaticClass(),
Category,
LOCTEXT("NewIntNode", "New Int Node"),
LOCTEXT("NewIntNodeTip", "Create an integer literal node"));
Add(ContextMenuBuilder,
UMyEdGraphNode_Add::StaticClass(),
Category,
LOCTEXT("NewAddNode", "New Add Node"),
LOCTEXT("NewAddNodeTip", "Create an add node"));
Add(ContextMenuBuilder,
UMyEdGraphNode_Print::StaticClass(),
Category,
LOCTEXT("NewPrintNode", "New Print Node"),
LOCTEXT("NewPrintNodeTip", "Create a print node"));
}
#undef LOCTEXT_NAMESPACE
MyWidgetTool.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
class UMyEdGraph;
class UMyEdGraphNode;
class FUICommandList;
struct FPaletteItem
{
FString Name;
TSubclassOf<UMyEdGraphNode> NodeClass;
};
class SMyWidgetTool : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMyWidgetTool) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
TSharedRef<SWidget> BuildPalette();
TSharedRef<SWidget> BuildGraphEditor();
TSharedRef<SWidget> BuildToolbar();
TSharedRef<SWidget> BuildDetailPanel();
void AddNodeToGraph(TSubclassOf<UMyEdGraphNode>);
void DeleteSelectedNodes();
bool CanDeleteNodes() const;
FReply OnExportJsonClicked();
private:
UMyEdGraph* NodeGraph = nullptr;
TSharedPtr<SGraphEditor> GraphEditor;
TSharedPtr<FUICommandList> GraphEditorCommands;
TSharedPtr<IDetailsView> DetailsView;
TArray<TSharedPtr<FPaletteItem>> PaletteItemList;
};
MyWidgetTool.cpp
cpp
#include "MyWidgetTool.h"
#include "NodeEditor/MyEdGraph.h"
#include "NodeEditor/MyEdGraphNode.h"
#include "NodeEditor/MyEdGraphSchema.h"
#include "Framework/Commands/UICommandList.h"
#include "Framework/Commands/GenericCommands.h"
#include "DesktopPlatformModule.h"
#define LOCTEXT_NAMESPACE "SMyWidgetTool"
void SMyWidgetTool::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
.Padding(8)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(0.2f)
.Padding(0, 0, 4, 0)
[
BuildPalette()
]
+ SHorizontalBox::Slot()
.FillWidth(0.6f)
.Padding(0, 0, 4, 0)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot().AutoHeight()
[
BuildToolbar()
]
+ SVerticalBox::Slot()
[
BuildGraphEditor()
]
]
+ SHorizontalBox::Slot()
.FillWidth(0.2f)
[
BuildDetailPanel()
]
]
];
}
TSharedRef<SWidget> SMyWidgetTool::BuildPalette()
{
PaletteItemList.Add(MakeShared<FPaletteItem>(TEXT("Int"), UMyEdGraphNode_Int::StaticClass()));
PaletteItemList.Add(MakeShared<FPaletteItem>(TEXT("Add"), UMyEdGraphNode_Add::StaticClass()));
PaletteItemList.Add(MakeShared<FPaletteItem>(TEXT("Print"), UMyEdGraphNode_Print::StaticClass()));
return SNew(SVerticalBox)
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 4)
[
SNew(STextBlock)
.Text(LOCTEXT("PaletteTitle", "Node Palette"))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
]
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 4)
[
SNew(STextBlock)
.Text(LOCTEXT("PaletteHint", "Double-click to add"))
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
]
+ SVerticalBox::Slot().FillHeight(1.f)
[
SNew(SListView<TSharedPtr<FPaletteItem>>)
.ListItemsSource(&PaletteItemList)
.SelectionMode(ESelectionMode::Single)
.OnGenerateRow_Lambda([this](TSharedPtr<FPaletteItem> Item, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(STableRow<TSharedPtr<FPaletteItem>>, OwnerTable)
.Padding(FMargin(4.f))
[
SNew(STextBlock).Text(FText::FromString(Item->Name))
];
})
.OnMouseButtonDoubleClick(SListView<TSharedPtr<FPaletteItem>>::FOnMouseButtonDoubleClick::CreateLambda([this](TSharedPtr<FPaletteItem> Item)
{
AddNodeToGraph(Item->NodeClass);
}))
];
}
TSharedRef<SWidget> SMyWidgetTool::BuildGraphEditor()
{
// 创建Graph
NodeGraph = NewObject<UMyEdGraph>(
GetTransientPackage(),
UMyEdGraph::StaticClass(),
NAME_None,
RF_Transactional
);
// 设置Graph Schema
NodeGraph->Schema = UMyEdGraphSchema::StaticClass();
// 指令映射
GraphEditorCommands = MakeShared<FUICommandList>();
GraphEditorCommands->MapAction(
FGenericCommands::Get().Delete,
FExecuteAction::CreateSP(this, &SMyWidgetTool::DeleteSelectedNodes),
FCanExecuteAction::CreateSP(this, &SMyWidgetTool::CanDeleteNodes));
// Graph事件
SGraphEditor::FGraphEditorEvents Events;
Events.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateLambda([this](const TSet<UObject*>& NewSelection)
{
if (!DetailsView.IsValid())
{
return;
}
TArray<UObject*> Selected;
Selected.Reserve(NewSelection.Num());
for (UObject* Obj : NewSelection)
{
Selected.Add(Obj);
}
DetailsView->SetObjects(Selected);
});
//创建Graph Editor
GraphEditor = SNew(SGraphEditor)
.AdditionalCommands(GraphEditorCommands)
.GraphToEdit(NodeGraph)
.GraphEvents(Events)
.IsEditable(true)
.ShowGraphStateOverlay(false);
return GraphEditor.ToSharedRef();
}
TSharedRef<SWidget> SMyWidgetTool::BuildToolbar()
{
return SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder"))
.Padding(4.f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("ExportJson", "Export JSON"))
.ToolTipText(LOCTEXT("ExportJsonTip", "Serialize the graph to a JSON file"))
.OnClicked(this, &SMyWidgetTool::OnExportJsonClicked)
]
+ SHorizontalBox::Slot()
.FillWidth(1.f)
[
SNew(SSpacer)
]
];
}
TSharedRef<SWidget> SMyWidgetTool::BuildDetailPanel()
{
// 创建Detail面板
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs Args;
Args.NameAreaSettings = FDetailsViewArgs::HideNameArea;
Args.bHideSelectionTip = true;
Args.bShowScrollBar = true;
DetailsView = PropertyEditorModule.CreateDetailView(Args);
return SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder"))
.Padding(4.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 4)
[
SNew(STextBlock)
.Text(LOCTEXT("DetailsTitle", "Details"))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
]
+ SVerticalBox::Slot().FillHeight(1.f)
[
DetailsView.ToSharedRef()
]
];
}
void SMyWidgetTool::AddNodeToGraph(TSubclassOf<UMyEdGraphNode> NodeClass)
{
if (!NodeGraph || !*NodeClass)
{
return;
}
UMyEdGraphNode* NewNode = NewObject<UMyEdGraphNode>(NodeGraph, NodeClass, NAME_None, RF_Transactional);
NewNode->CreateNewGuid();
// 分配引脚
NewNode->AllocateDefaultPins();
if (GraphEditor.IsValid())
{
// 获取粘贴推荐位置
const FVector2D Pos = GraphEditor->GetPasteLocation2f();
NewNode->NodePosX = static_cast<int32>(Pos.X);
NewNode->NodePosY = static_cast<int32>(Pos.Y);
}
NodeGraph->Modify();
NodeGraph->AddNode(NewNode, true, true);
NodeGraph->NotifyGraphChanged();
}
void SMyWidgetTool::DeleteSelectedNodes()
{
if (!GraphEditor.IsValid() || !NodeGraph)
{
return;
}
// 获取所有选中
const FGraphPanelSelectionSet SelectedNodes = GraphEditor->GetSelectedNodes();
if (SelectedNodes.Num() == 0)
{
return;
}
// 设置事务
const FScopedTransaction Transaction(LOCTEXT("DeleteNodesTx", "Delete Selected Nodes"));
// 标脏
NodeGraph->Modify();
// 清空选中
GraphEditor->ClearSelectionSet();
for (UObject* Obj : SelectedNodes)
{
UEdGraphNode* Node = Cast<UEdGraphNode>(Obj);
if (!Node || !Node->CanUserDeleteNode())
{
continue;
}
Node->Modify();
// 断开连接
Node->BreakAllNodeLinks();
// 移除
NodeGraph->RemoveNode(Node);
}
NodeGraph->NotifyGraphChanged();
}
bool SMyWidgetTool::CanDeleteNodes() const
{
if (!GraphEditor.IsValid())
{
return false;
}
const FGraphPanelSelectionSet& SelectedNodes = GraphEditor->GetSelectedNodes();
for (UObject* Obj : SelectedNodes)
{
if (UEdGraphNode* Node = Cast<UEdGraphNode>(Obj))
{
if (Node->CanUserDeleteNode())
{
return true;
}
}
}
return false;
}
FReply SMyWidgetTool::OnExportJsonClicked()
{
if (!NodeGraph)
{
return FReply::Handled();
}
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
if (!DesktopPlatform)
{
return FReply::Handled();
}
// 获取当前 Slate 顶层窗口的原生句柄,作为对话框的父窗口
void* ParentWindowHandle = nullptr;
if (FSlateApplication::IsInitialized())
{
const TSharedPtr<SWindow> ActiveWindow = FSlateApplication::Get().GetActiveTopLevelWindow();
if (ActiveWindow.IsValid() && ActiveWindow->GetNativeWindow().IsValid())
{
ParentWindowHandle = ActiveWindow->GetNativeWindow()->GetOSWindowHandle();
}
}
TArray<FString> OutFilenames;
const bool bPicked = DesktopPlatform->SaveFileDialog(
ParentWindowHandle,
TEXT("Export Node Graph As JSON"), // 对话框标题
FPaths::ProjectSavedDir(), // 默认路径
TEXT("MyEdGraph.json"), // 默认文件名
TEXT("JSON files (*.json)|*.json"), // 过滤器
EFileDialogFlags::None,
OutFilenames);
if (!bPicked || OutFilenames.Num() == 0)
{
return FReply::Handled();
}
const FString TargetPath = OutFilenames[0];
// 构建JSON
const TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
JsonObject->SetStringField(TEXT("GraphName"), NodeGraph->GetName());
JsonObject->SetNumberField(TEXT("NodeCount"), NodeGraph->Nodes.Num());
for (TObjectPtr<UEdGraphNode> Node : NodeGraph->Nodes)
{
if (UMyEdGraphNode* MyNode = Cast<UMyEdGraphNode>(Node))
{
TSharedRef<FJsonObject> NodeJson = MyNode->ToJSON();
JsonObject->SetObjectField(MyNode->NodeGuid.ToString(), NodeJson);
}
}
FString OutString;
const TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer = TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&OutString);
FJsonSerializer::Serialize(JsonObject, Writer);
const bool bWritten = FFileHelper::SaveStringToFile(OutString, *TargetPath);
UE_LOG(LogTemp, Display, TEXT("[MyWidgetTool] Export JSON %s to %s"), bWritten ? TEXT("SUCCESS") : TEXT("FAILED"), *TargetPath);
return FReply::Handled();
}
#undef LOCTEXT_NAMESPACE
MyTestVI.Build.cs
cpp
using UnrealBuildTool;
public class MyTestVI : ModuleRules
{
public MyTestVI(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput",
"Json", "JsonUtilities" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"UnrealEd",
"AdvancedPreviewScene",
"EditorFramework",
"EditorStyle",
"LevelEditor",
"ToolMenus",
"ToolWidgets",
"GraphEditor",
"PropertyEditor",
"EditorWidgets",
"DesktopPlatform"
});
}
}
}
重新生成项目,编译运行
添加节点
右键菜单

Export Json
json
{
"GraphName": "MyEdGraph_0",
"NodeCount": 4,
"E7BBDD814B4B67F4082E0BA6E4639C5E":
{
"DisplayName": "Int",
"IntValue": 5
},
"74E22A07432D1D2285720EB814D70A62":
{
"DisplayName": "Int",
"IntValue": 12
},
"0A7EBD4147EA254AD418938B43DB0B98":
{
"DisplayName": "Add"
},
"55FA9B504ED31A5C7BB509AD44706733":
{
"DisplayName": "Print",
"Prefix": "Log:"
}
}
下一篇将实现一个简单的 Sequence/Timeline 轨道编辑器