We have discussed about how to create a custom asset, this post we are going to learn how to create a custom editor.

1. Editor Tool Kit

Last time, wo uses the editor from the super class:

void FAssetTypeActions_OurAssets::OpenAssetEditor(const TArray<UObject*>& InObjects,
	TSharedPtr<IToolkitHost> EditWithinLevelEditor)
{
	FAssetTypeActions_Base::OpenAssetEditor(InObjects, EditWithinLevelEditor);
}

The base class of the editor is FAssetEditorToolkit, inherits from it and implements a new class, first, there are some interface that are must implememt.

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; }
};

Most of them deal with name or something else, it is not very complex, the core methods in the class are RegisterTabSpawners, UnregisterTabSpawners and a custom initialize function:

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);
};

Note that the initialize function is not a virtual function, and has no mode matching like FRunnable, your can give it any name you like, and the parameters are also customizable.

Call the initialize method in AssetAction:

const EToolkitMode::Type ToolKitModeType = EditWithinLevelEditor ? EToolkitMode::WorldCentric : EToolkitMode::Standalone;
	
for (auto ObjectIterator = InObjects.CreateConstIterator(); ObjectIterator; ++ObjectIterator)
{
		if (UOurAssetsObject* OurAsset = Cast<UOurAssetsObject>(*ObjectIterator))
		{
			  const TSharedRef<FOurAssetEditorToolkit> RecoilAssetEditorToolKit = MakeShareable(new FOurAssetEditorToolkit());
			  RecoilAssetEditorToolKit->InitializeAssetEditor(ToolKitModeType, EditWithinLevelEditor, OurAsset);
		}
}

Let’s implement the initialize function,  we only do one thing for now: Let the editor know what it is now editing.

void FOurAssetEditorToolkit::InitializeAssetEditor(const EToolkitMode::Type Mode,
	const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject* InAssets)
{
	InitAssetEditor(Mode, InitToolkitHost, FName("OurAssetEditor"), StandaloneOurAssetEditor, true, true, InAssets);
	RegenerateMenusAndToolbars();
}

When called InitAssetEditor function, we passed a parameter InAsset, which will set the EdittingObject of the editor to InAsset.

Fow now, we have create a simplest editor, and combine it with our assets.

2. Add detail panel to our editor

A empty editor panel is meanless for us, we need to add something to it, simplestly, we can add the detail panel which should be shown default.

Add a detail panel requires PropertyEditor module, remember to add it, and the, add a method in FOurAssetEdtiorToolkit:

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;
};

Which implemation is:

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
	];
}

And then, dealing with it in RegisterTabSpawner and UnregisterTabSpawner:

void FOurAssetEditorToolkit::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
	FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
	InTabManager->RegisterTabSpawner(FName("OurAssetPropertyTab"), FOnSpawnTab::CreateRaw(this, &FOurAssetEditorToolkit::SpawnDetailTab));
}

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

Restart the editor, and you shall see it.

3. Add a graph editor

Now, we add a graph editor for it. Graph editor is a little special, in Unreal Engine, there are three runtime classes for this: UEdGraph, UEdGraphPin and UEdGraphNode, there are also three editor classes: SGraphEditor, SGraphPin, SGraphNode, they are the editor visiable widget for the runtime classes.

Specially, there exists a class called UEdGraphSchema, which decribes the rules of creating nodes, connect nodes etc.

A UEdGraph member should be add into our asset in runtime module, but here, we just need to show it, so it’s not that offical, But we will implement a official one in the following posts.

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;
};

Note that here I treat UEdGraph as a member of editor class, it’s not correct, I just need to support the SGraphEditor, so I do that for convinience. If it’s a correct requestment you cannot do that, it should be a member of assets in runtime module.

Then, modify the Register and Unregister method:

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);
}

I use slate to construct a SGraphEditor now, and give it a EdGraph, I initialize the EdGraph in Initialize method, but the best place to init it is the factory, we will see that at Post XI. Now, it is for convinience:

void FOurAssetEditorToolkit::InitializeAssetEditor(const EToolkitMode::Type Mode,
	const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject* InAssets)
{
	// 这里我只是为了让他简单的显示出来才这么做的。
	// 如果真的要编辑一个可以被持久化存储的图,图应该存在自定义资产中,随自定义产的创建初始化。
	
	EdGraph = NewObject<UEdGraph>();
	EdGraph->Schema = UEdGraphSchema::StaticClass();
	EdGraph->AddToRoot();

	InitAssetEditor(Mode, InitToolkitHost, FName("OurAssetEditor"), StandaloneOurAssetEditor, true, true, InAssets);
	RegenerateMenusAndToolbars();
}

Restart the editor, open our asset:

4. Set a default layout

Set default layout in out initialize method:

void FOurAssetEditorToolkit::InitializeAssetEditor(const EToolkitMode::Type Mode,
	const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject* InAssets)
{
	// 这里我只是为了让他简单的显示出来才这么做的。
	// 如果真的要编辑一个可以被持久化存储的图,图应该存在自定义资产中,随自定义产的创建初始化。
	
	EdGraph = NewObject<UEdGraph>();
	EdGraph->Schema = UEdGraphSchema::StaticClass();
	EdGraph->AddToRoot();
	
	const TSharedRef<FTabManager::FLayout> StandaloneOurAssetEditor = FTabManager::NewLayout("OurAssetEditor")->AddArea
			(
				FTabManager::NewPrimaryArea()->SetOrientation(EOrientation::Orient_Horizontal)
				->Split(FTabManager::NewStack()->AddTab(FName("OurAssetsGraphEditorTab"), ETabState::OpenedTab))
			);
	InitAssetEditor(Mode, InitToolkitHost, FName("OurAssetEditor"), StandaloneOurAssetEditor, true, true, InAssets);
	RegenerateMenusAndToolbars();
}

We have a default layout now, Tab is identified exactly by its FName, so, you can prepare some static member for them, much better than using string directly:

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

Creating a graph editor is not as easy not doing that in Unity, we will discuss it officially in the following posts

 

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 *