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