【UE】Slate 编辑器工具开发03 - 节点编辑器 (EdGraph)

上一节实现了一个简单的预览视口,接下来学习如何编写一个节点编辑器

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 轨道编辑器

相关推荐
郝学胜-神的一滴2 小时前
[简化版 GAMES 104] 现代游戏引擎 02:拆解现代游戏引擎5+1层级架构,吃透引擎底层核心逻辑
c++·unity·架构·游戏引擎·图形渲染·unreal engine·系统设计
张太行_5 小时前
vscode在open folder如何设置默认路径
ide·vscode·编辑器
●VON1 天前
鸿蒙 PC Markdown 编辑器界面语言切换:跟随系统、简体中文与 English 的完整状态闭环
华为·编辑器·harmonyos·鸿蒙
●VON1 天前
鸿蒙 PC Markdown 编辑器内部隐私与安全评审
安全·华为·编辑器·harmonyos·鸿蒙
directx3d_beginner1 天前
26,怪物受击接口改为C++
ue5
●VON1 天前
鸿蒙 PC Markdown 编辑器离线专业渲染管线
华为·编辑器·harmonyos·鸿蒙
这就是佬们吗1 天前
Python入门③-运算符、条件与循环
开发语言·数据库·vscode·python·pycharm·编辑器
●VON1 天前
鸿蒙 PC Markdown 编辑器链接补全与离线校验:中文路径、标题锚点与授权边界
华为·编辑器·harmonyos·鸿蒙
郝学胜-神的一滴1 天前
中级OpenGL教程 023:Assimp模型加载全解——从源码到架构的骈文探秘
c++·unity·游戏引擎·cmake·unreal engine·opengl