In this post we deal with advanced Node creation and pin connections between Nodes, but first of all, I want to remind you that I made a mistake in the post X, that is, UEdGraph, UEdGraphSchema, and UEdGraphNode are all Runtime. I wrote it in the Editor module .

Although I didn’t give a complete screenshot, experienced friends should have found it when they looked at the ADVANCEDALSEDITOR_API in the code I posted.

1. Correct the relationship between our Assets and Toolkit

Before, for convenience, I put EdGraph in Toolkit class, but everyone knows that this is wrong, now, let’s fix this. First, we put EdGraph as a member variable in UOurAssetObject:

UCLASS(Blueprintable, BlueprintType)
class ADVANCEDALS_API UOurAssetsObject : public UObject
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Our Assets")
	FName AssetsName;

#if WITH_EDITORONLY_DATA
	UPROPERTY()
	TObjectPtr<UEdGraph> EdGraph = {};
#endif
};

Here, I set EdGraph as an editor variable, and I want to manage the graph data structure by myself, such as adding something like TArray<RootNode>.

So how do we initialize EdGraphNode? Can it be done in the constructor? If so, its constructor is implemented as:

UOurAssetsObject::UOurAssetsObject()
{
	EdGraph = NewObject<UEdGraph>();
	EdGraph->Schema = UOurGraphSchema::StaticClass();
	EdGraph->AddToRoot();
}

The Outer of EdGraph can also be OurAssetsObject.

However, the problem is: I can create an asset and call the constructor without any problem.  But if I open the engine editor again, isn’t there nothing left?

So, the best solution is: We move the initializition into the Factory:

UObject* UOutAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags,
	UObject* Context, FFeedbackContext* Warn)
{
	UOurAssetsObject* AssetsObject = NewObject<UOurAssetsObject>(InParent, InClass, InName, Flags);
	UEdGraph* Graph = FBlueprintEditorUtils::CreateNewGraph(AssetsObject, NAME_None, UEdGraph::StaticClass(), UOurGraphSchema::StaticClass());
	AssetsObject->EdGraph = Graph;
	return AssetsObject;
}

In this way, the serialization problem is perfectly solved.

Afterwards, we modify the content of the EditorToolkit class. From now on, Toolkit is no longer responsible for creating EdGraph. It just keeps a reference to UEdGraph and points to the EdGraph of OurAssetsObject when the editor is opened.

class ADVANCEDALSEDITOR_API FOurAssetEditorToolkit final : public FAssetEditorToolkit
{
public:
	// 必须实现的接口
	virtual FName GetToolkitFName() const override { return FName("OurAssetsEditorToolkit"); }
	virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("EditorExtension", "Out Asset Toolkit Name", "我们的资产编辑器"); }
	virtual FString GetWorldCentricTabPrefix() const override { return NSLOCTEXT("EditorExtension", "Out Asset Toolkit Tab Prefix", "我们的资产").ToString(); }
	virtual FLinearColor GetWorldCentricTabColorScale() const override { return FLinearColor::Green; }
	
public:
	virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
	virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;

	// 这个函数并不是虚函数,也不含有模式匹配,为公开函数被外部调用
	void InitializeAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject* InAssets);

private:

	// 生成细节面板
	TSharedRef<SDockTab> SpawnDetailTab(const FSpawnTabArgs& SpawnTabArgs) const;
	TObjectPtr<UEdGraph> EdGraph = nullptr;

	inline static const FName GraphEditorTabName {"OurAssetsGraphEditor"};
	inline static const FName PropertyEditorTabName {"OurAssetsPropertyEditor"};
	
};

Which implementations is :

#include "OurAssetEditorToolkit.h"
#include "Tutorial/OurAssetsObject.h"

void FOurAssetEditorToolkit::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
	FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
	
	InTabManager->RegisterTabSpawner(FName("OurAssetPropertyTab"), FOnSpawnTab::CreateRaw(this, &FOurAssetEditorToolkit::SpawnDetailTab));
	InTabManager->RegisterTabSpawner(FName("OutAssetsGraphEditorTab"), FOnSpawnTab::CreateLambda([&](const FSpawnTabArgs& SpawnTabArgs)
	{
		return SNew(SDockTab)
		[
			SNew(SGraphEditor).GraphToEdit(EdGraph)
		];
	}));
}

void FOurAssetEditorToolkit::UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
	InTabManager->UnregisterTabSpawner(FName("OurAssetPropertyTab"));
	InTabManager->UnregisterTabSpawner(FName("OutAssetsGraphEditorTab"));
	FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
}

void FOurAssetEditorToolkit::InitializeAssetEditor(const EToolkitMode::Type Mode,
	const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject* InAssets)
{
	const TSharedRef<FTabManager::FLayout> StandaloneOurAssetEditor = FTabManager::NewLayout("OutAssetEditor")->AddArea
			(
				FTabManager::NewPrimaryArea()->SetOrientation(EOrientation::Orient_Horizontal)
				->Split(FTabManager::NewStack()->AddTab(FName("OutAssetsGraphEditorTab"), ETabState::OpenedTab))
			);

	EdGraph = Cast<UOurAssetsObject>(InAssets)->EdGraph;
	check(EdGraph != nullptr)
	InitAssetEditor(Mode, InitToolkitHost, FName("OurAssetEditor"), StandaloneOurAssetEditor, true, true, InAssets);
	RegenerateMenusAndToolbars();
}

TSharedRef<SDockTab> FOurAssetEditorToolkit::SpawnDetailTab(const FSpawnTabArgs& SpawnTabArgs) const
{
	//加载属性编辑器模块
	FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	const FDetailsViewArgs DetailsViewArgs;
	//创建属性编辑器的Slate
	const TSharedRef<IDetailsView> AssetPropertyView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
	//将对象传入,这样就是自动生成对象的属性面板
	AssetPropertyView->SetObject(GetEditingObject());
	return SNew(SDockTab)
	[
		AssetPropertyView
	];
}

From now on, the EdGraph can be saved correctly.

2. Advanced Nodes

First of all, we have to achieve the goal we said in the previous issue: the right-click menu can completely create all subclasses of a class.

To do this, we create a graph node base class:

UCLASS(Abstract)
class ADVANCEDALS_API UOurBaseGraphNode : public UEdGraphNode
{
	GENERATED_BODY()
protected:

	inline static FName OurGraphNodePinCategory {"OurGraphNodePinCategory"};
	inline static FName OurGraphNodePinSubCategory {"OurGraphNodePinSubCategory"};
	
public:

	/** Begin UEdGraphNode Interface */
#if WITH_EDITOR
	FORCEINLINE virtual void AllocateDefaultPins() override {};
	FORCEINLINE virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override {return Super::GetNodeTitle(TitleType);}
	FORCEINLINE virtual FLinearColor GetNodeTitleColor() const override {return GetNodeTitleColor();}
	FORCEINLINE virtual FText GetTooltipText() const override {return GetTooltipText();}
#endif
	/** End UEdGraphNode Interface */

	FORCEINLINE virtual FText GetNodeDisplayName() const {return NSLOCTEXT("EditorExtension", "Base Node Display Name", "基类节点");}
};

At the same time, we also moved the category variable to this base class and modified it as Protected so that its subclasses can access it normally.

Add a GetNodeDisplayName method, which will define how to display its create button name in the node create menu.

Use the Abstract modifier to modify it in UCLASS to prove that he is a virtual base class. UObject cannot contain pure virtual functions. Therefore, we have to implement every function.

Since we have moved it out of the Editor module, we use the WITH_EDITOR macro to isolate it.

For our last Node, we implemented GetNodeDisplayName:

UCLASS()
class ADVANCEDALS_API UOurGraphNode : public UOurBaseGraphNode
{
	GENERATED_BODY()

public:
	
	/** Begin UEdGraphNode Interface */
#if WITH_EDITOR
	// 分配默认的引脚
	virtual void AllocateDefaultPins() override;
	// 节点的Title
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	// 节点Title的颜色
	virtual FLinearColor GetNodeTitleColor() const override;
	// Tooltip内容
	virtual FText GetTooltipText() const override;

	/** End UEdGraphNode Interface */

	FORCEINLINE virtual FText GetNodeDisplayName() const override {return NSLOCTEXT("EditorExtension", "Our Graph Node DisplayName", "我们的节点");}
#endif
};

We implement an additional node, which has an input and an output pin:

UCLASS()
class ADVANCEDALS_API UOurInOutGraphNode : public UOurBaseGraphNode
{
	GENERATED_BODY()

public:

#if WITH_EDITOR
	/** Begin UEdGraphNode Interface */
	
	// 分配默认的引脚
	virtual void AllocateDefaultPins() override;
	// 节点的Title
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	// 节点Title的颜色
	virtual FLinearColor GetNodeTitleColor() const override;
	// Tooltip内容
	virtual FText GetTooltipText() const override;
	
	/** End UEdGraphNode Interface */

	virtual FText GetNodeDisplayName() const override{return NSLOCTEXT("EditorExtension", "Our In Out Node DisplayName", "输入输出节点");}
#endif
};

Which is implemented as:

#if WITH_EDITOR
void UOurInOutGraphNode::AllocateDefaultPins()
{
	CreatePin(EGPD_Input, OurGraphNodePinCategory, OurGraphNodePinSubCategory, nullptr, TEXT("In"));
	CreatePin(EGPD_Output, OurGraphNodePinCategory, OurGraphNodePinSubCategory, nullptr, TEXT("Out"));
}

FText UOurInOutGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	return NSLOCTEXT("EditorExtension", "Our In Out Node Title", "输入输出");
}

FLinearColor UOurInOutGraphNode::GetNodeTitleColor() const
{
	return FLinearColor::Green;
}

FText UOurInOutGraphNode::GetTooltipText() const
{
	return NSLOCTEXT("EditorExtension", "Our In Out Node Tooltip", "这是一个输入输出节点");
}
#endif

3. Add these nodes in the create nodes menu

Schema has also been moved to runtime module, so we have to do some isolation with WITH_EDITORDATA_ONLY and WITH_EDITOR:

UCLASS()
class ADVANCEDALS_API UOurGraphSchema : public UEdGraphSchema
{
	GENERATED_BODY()
	
protected:

#if WITH_EDITORONLY_DATA
	inline static FName OurNodeContextMenuSection {"OurNodeContextMenuSection"};
	inline static FName OurPinContextMenuSection {"OurPinContextMenuSection"};
#endif

public:

	/** Begin UEdGraphSchema Interface */

#if WITH_EDITOR
	// 右键空白处或者拖拽引脚调用函数
	virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override;
	// 右键一个节点或者引脚出现的函数
	virtual void GetContextMenuActions(UToolMenu* Menu, UGraphNodeContextMenuContext* Context) const override;
#endif
	
	
	/** End UEdGraphSchema Interface */
};

Among them, we mainly modified the content of the GetGraphContextActions function:

void UOurGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const
{
	const TSharedPtr<FPrintHelloGraphSchemaAction> Action = MakeShareable(new FPrintHelloGraphSchemaAction);
	ContextMenuBuilder.AddAction(Action);

	const UClass* BaseNodeClass = UOurBaseGraphNode::StaticClass();
	TArray<UClass*> NodeClasses;
	GetDerivedClasses(BaseNodeClass, NodeClasses);

	// 因为已经有NodeClass变量了,所以这里命名为NodeClassIterator
	for (const auto& NodeClassIterator : NodeClasses)
	{
		const TSharedPtr<FOurGraphSchemaAction_NewNode> CreateNodeAction = MakeShareable(new FOurGraphSchemaAction_NewNode);
		const UOurBaseGraphNode* NodeCDO = Cast<UOurBaseGraphNode>(NodeClassIterator->ClassDefaultObject);
		CreateNodeAction->UpdateSearchData(NodeCDO->GetNodeDisplayName(), FText(),
			NSLOCTEXT("EditionExtension", "Create Node Category", "创建新节点"), FText());
		CreateNodeAction->SetNodeClass(NodeClassIterator);
		ContextMenuBuilder.AddAction(CreateNodeAction);
	}
	
	Super::GetGraphContextActions(ContextMenuBuilder);
}

Now, we register all his subclasses here.

 

4. Pin connection

Pin connection is actually not difficult, if we need to establish a connection between two pins, only need to deal with a function, CanCreateConnection:

UCLASS()
class ADVANCEDALS_API UOurGraphSchema : public UEdGraphSchema
{
	GENERATED_BODY()
	
protected:

#if WITH_EDITORONLY_DATA
	inline static FName OurNodeContextMenuSection {"OurNodeContextMenuSection"};
	inline static FName OurPinContextMenuSection {"OurPinContextMenuSection"};
#endif

public:

	/** Begin UEdGraphSchema Interface */

#if WITH_EDITOR
	// 右键空白处或者拖拽引脚调用函数
	virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override;
	// 右键一个节点或者引脚出现的函数
	virtual void GetContextMenuActions(UToolMenu* Menu, UGraphNodeContextMenuContext* Context) const override;
	// 创建引脚链接
	virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const override;
#endif
	
	
	/** End UEdGraphSchema Interface */
};

which is actually implemented as:

const FPinConnectionResponse UOurGraphSchema::CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const
{
	return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, TEXT("OK!"));
	// return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Cannot Connect"));
}

You can add some pin type judgments and the like in it

For convenience, we allow the created nodes to be automatically connected. For this, we need to rewrite:

UEdGraphNode* FOurGraphSchemaAction_NewNode::PerformAction(UEdGraph* ParentGraph, UEdGraphPin* FromPin,
                                                           const FVector2D Location, bool bSelectNewNode)
{
	UEdGraphNode* GraphNodeTemplate = Cast<UEdGraphNode>(NodeClass->ClassDefaultObject);
	UEdGraphNode* NewGraphNode = CreateNode(ParentGraph, FromPin, Location, GraphNodeTemplate);

	const UEdGraphSchema* GraphSchema = ParentGraph->GetSchema();

	if (FromPin)
	{
		for (const auto& Pin : NewGraphNode->Pins)
		{
			if (GraphSchema->TryCreateConnection(FromPin, Pin))
			{
				break;
			}
		}
	}
	return nullptr;
}

Pay attention to this empty judgment, if it is not judged empty, the creation of no previous node will cause the engine to crash.

5. When the Node is pressed, its properties should be displayed in the details panel

To deal with this, what we need to modify is SGraphEditor, let’s take a look at when it is created:

void FOurAssetEditorToolkit::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
	FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
	
	InTabManager->RegisterTabSpawner(FName("OurAssetPropertyTab"), FOnSpawnTab::CreateRaw(this, &FOurAssetEditorToolkit::SpawnDetailTab));
	InTabManager->RegisterTabSpawner(FName("OutAssetsGraphEditorTab"), FOnSpawnTab::CreateLambda([&](const FSpawnTabArgs& SpawnTabArgs)
	{
		return SNew(SDockTab)
		[
			SNew(SGraphEditor)
			.GraphToEdit(EdGraph)
			.GraphEvents(?)
		];
	}));
}

As you can see, it is implemented by passing in a GraphEvents parameter through Slate.

The incoming parameter is a structure of type SGraphEditor::FGraphEditorEvents:

void FOurAssetEditorToolkit::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
 	FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
 
 	SGraphEditor::FGraphEditorEvents InEvents;
 	InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FOurAssetEditorToolkit::OnSelectedNodesChanged);
 	InEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &FOurAssetEditorToolkit::OnNodeDoubleClicked);
 
 	GraphEditor = SNew(SGraphEditor)
 				.GraphToEdit(EdGraph)
 				.GraphEvents(InEvents);
 	
 	InTabManager->RegisterTabSpawner(FName("OurAssetPropertyTab"), FOnSpawnTab::CreateRaw(this, &FOurAssetEditorToolkit::SpawnDetailTab));
 	InTabManager->RegisterTabSpawner(FName("OutAssetsGraphEditorTab"), FOnSpawnTab::CreateLambda([&](const FSpawnTabArgs& SpawnTabArgs)
 	{
 		return SNew(SDockTab)
 		[
 			GraphEditor.ToSharedRef()
 		];
 	}));
}

Here, I promoted GraphEditor to TSharedRef<SGraphEditor> pointer, the reason will be seen later. In the same way, the details panel has also been promoted.

Next, we only need to implement the OnSelectedNodesChanged and OnNodeDoubleClicked functions. Here, I leave OnNodeDoubleClicked empty, and we focus on implementing OnSelectionNodesChanged:

class ADVANCEDALSEDITOR_API FOurAssetEditorToolkit final : public FAssetEditorToolkit
{
public:
	// 必须实现的接口
	virtual FName GetToolkitFName() const override { return FName("OurAssetsEditorToolkit"); }
	virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("EditorExtension", "Out Asset Toolkit Name", "我们的资产编辑器"); }
	virtual FString GetWorldCentricTabPrefix() const override { return NSLOCTEXT("EditorExtension", "Out Asset Toolkit Tab Prefix", "我们的资产").ToString(); }
	virtual FLinearColor GetWorldCentricTabColorScale() const override { return FLinearColor::Green; }
	
public:
	virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
	virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;

	// 这个函数并不是虚函数,也不含有模式匹配,为公开函数被外部调用
	void InitializeAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject* InAssets);

private:

	// 生成细节面板
	TSharedRef<SDockTab> SpawnDetailTab(const FSpawnTabArgs& SpawnTabArgs);
	
	TObjectPtr<UEdGraph> EdGraph = nullptr;
	TSharedPtr<SGraphEditor> GraphEditor;
	TSharedPtr<IDetailsView> AssetPropertyView;

	inline static const FName GraphEditorTabName {"OurAssetsGraphEditor"};
	inline static const FName PropertyEditorTabName {"OurAssetsPropertyEditor"};

	void OnSelectedNodesChanged(const TSet<UObject*>& NewSelection);
	void OnNodeDoubleClicked(UEdGraphNode* Node);
	
};
void FOurAssetEditorToolkit::OnSelectedNodesChanged(const TSet<UObject*>& NewSelection)
{
	TArray<UObject*> Selection;

	for (UObject* SelectionEntry : NewSelection)
	{
		Selection.Add(SelectionEntry);
	}
	if (Selection.Num() == 0) 
	{
		AssetPropertyView->SetObject(EdGraph);
	}
	else
	{
		AssetPropertyView->SetObjects(Selection);
	}
}

void FOurAssetEditorToolkit::OnNodeDoubleClicked(UEdGraphNode* Node)
{
}

TSharedRef<SDockTab> FOurAssetEditorToolkit::SpawnDetailTab(const FSpawnTabArgs& SpawnTabArgs)
{
	//加载属性编辑器模块
	FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	const FDetailsViewArgs DetailsViewArgs;
	//创建属性编辑器的Slate
	AssetPropertyView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
	//将对象传入,这样就是自动生成对象的属性面板
	AssetPropertyView->SetObject(GetEditingObject());
	return SNew(SDockTab)
	[
		AssetPropertyView.ToSharedRef()
	];
}

For convenience, we add a variable member to the UOurInOutGraphNode class

UCLASS()
class ADVANCEDALS_API UOurInOutGraphNode : public UOurBaseGraphNode
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	int32 Value;

#if WITH_EDITOR
	/** Begin UEdGraphNode Interface */
	
	// 分配默认的引脚
	virtual void AllocateDefaultPins() override;
	// 节点的Title
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	// 节点Title的颜色
	virtual FLinearColor GetNodeTitleColor() const override;
	// Tooltip内容
	virtual FText GetTooltipText() const override;
	
	/** End UEdGraphNode Interface */

	virtual FText GetNodeDisplayName() const override{return NSLOCTEXT("EditorExtension", "Our In Out Node DisplayName", "输入输出节点");}
#endif
};

Restart the editor, click on the Node, and you can see the properties

Pay attention here, that is, when it is not selected, who are you editing? I am editing EdGraph here, in actual project it may be a subclass of EdGraph, such as UFlowGraph.

But you may also say: what I want to edit is not this graph, but the asset which contains this graph, then you store it in a variable during Init, and then edit this asset.

The related code is:

if (Selection.Num() == 0) 
{
	AssetPropertyView->SetObject(EdGraph);
}
else
{
	AssetPropertyView->SetObjects(Selection);
}

6. Recommand Reading

Generic Graph

Flow Graph

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

Leave a Reply

Your email address will not be published. Required fields are marked *