前言
Unreal 的学习浩瀚且博杂,有时候一个最小 Demo 就是很好的学习起点。 想起我以前翻阅 UE 的源码一大堆的文件,看得我是无比头疼。 偶然间发现 CSDN YakSue 写了好多篇 Unreal 工具开发的 介绍。 虽然没有配上 Github 链接,但是源码都在文章里面体现了。 对于工具开发的不同模块都大有裨益。 于是我将这些内容整合到一起,并且详细讲解其中实现的核心点。
Custom Asset https://yaksue.blog.csdn.net/article/details/107646900 https://github.com/FXTD-ODYSSEY/Unreal-Playground/tree/main/Plugins/Yaksue/TestAssetEditorPlg
创建一个自定义的 Asset 需要有三个类
Asset (UObject)
AssetFactory (UFactory)
AssetTypeActions (FAssetTypeActions_Base)
Asset 描述对象本身的数据 AssetFactory 描述如何创建对象 AssetTypeActions 返回对象显示的信息
AssetTypeActions
包含方法 GetName
GetTypeColor
GetSupportedClass
GetCategories
用来描述对应的信息。 GetCategories
会分配 Asset 所属的位置。
这个方式默认打开的窗口是 Details Panel. 如果想要自定义打开的窗口需要添加 FAssetEditorToolkit
类 AssetTypeActions
添加 OpenAssetEditor
方法将 Toolkit 生成并初始化。
1 2 3 4 5 6 7 FAssetEditorToolkit GetToolkitFName GetBaseToolkitName GetWorldCentricTabPrefix GetWorldCentricTabColorScale Initialize RegisterTabSpawners
RegisterTabSpawners
通过这个方法注册生产 Tab 的 ID 后续通过 Initialize
方法调用 AddTab 将 Register 的 Tab 生成。 最后通过 FAssetEditorToolkit::InitAssetEditor
完成 Toolkit 的初始化
如果不想将 Asset 放到 EAssetTypeCategories::Misc
的分类中。 也可以构建一个新的标签附上去。 只是需要将 factory 相关的 GetMenuCategories
放入去掉。 我之前没有去掉,一直很疑惑为啥自定义菜单没有生效。
1 2 3 4 5 6 7 8 9 10 11 FYaksueTestAssetTypeActions::FYaksueTestAssetTypeActions () { IAssetTools &AssetTools = FModuleManager::LoadModuleChecked <FAssetToolsModule>("AssetTools" ).Get (); AssetCategory = AssetTools.RegisterAdvancedAssetCategory (FName (TEXT ("Custom Assets" )), LOCTEXT ("CustomAssetCategory" , "Custom Assets" )); } uint32 FYaksueTestAssetTypeActions::GetCategories () { return AssetCategory; }
构造函数注册新的分类,头文件需要添加上定义 FYaksueTestAssetTypeActions();
EAssetTypeCategories::Type AssetCategory;
Custom Filter https://yaksue.blog.csdn.net/article/details/120929455 https://github.com/FXTD-ODYSSEY/Unreal-Playground/tree/main/Plugins/Yaksue/TestCustomFilter
继承 UContentBrowserFrontEndFilterExtension
可以通过 override AddFrontEndFilterExtensions
方法扩展 filter。 生成一个 FFrontendFilter
子类,然后通过 AddFrontEndFilterExtensions
将过滤对象添加到过滤列表里面。 FFrontendFilter
最核心的方法就是 PassesFilter
它会将每个 item 传到这个函数返回 bool 来决定是否显示。
Slate https://yaksue.blog.csdn.net/article/details/110084013
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 SNew (SOverlay)+ SOverlay::Slot () [ SNew (SHorizontalBox) + SHorizontalBox::Slot ().FillWidth (0.3f ) [ SNew (SButton)1 ] + SHorizontalBox::Slot ().FillWidth (0.7f ) [ SNew (SVerticalBox) + SVerticalBox::Slot ().FillHeight (0.5f ) [ SNew (SButton) ] + SVerticalBox::Slot ().FillHeight (0.5f ) [ SNew (SButton) ] ] ] + SOverlay::Slot () [ SNew (SHorizontalBox) + SHorizontalBox::Slot ().FillWidth (1.0f ) + SHorizontalBox::Slot ().AutoWidth () [ SNew (SVerticalBox) + SVerticalBox::Slot ().FillHeight (1.0f ) + SVerticalBox::Slot ().AutoHeight () [ SNew (SBox) .HeightOverride (128 ) .WidthOverride (128 ) [ SNew (SButton) ] ] + SVerticalBox::Slot ().FillHeight (1.0f ) ] + SHorizontalBox::Slot ().FillWidth (1.0f ) ]
使用 Unreal Slate 构建窗口,通过代码的属性结构来描述 UI 的构成和配置。
DockTab Layout https://yaksue.blog.csdn.net/article/details/109321869
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 void FTestLayoutWindowModule::StartupModule () { FTestLayoutWindowStyle::Initialize (); FTestLayoutWindowStyle::ReloadTextures (); FTestLayoutWindowCommands::Register (); PluginCommands = MakeShareable (new FUICommandList); PluginCommands->MapAction ( FTestLayoutWindowCommands::Get ().OpenLayoutWindow, FExecuteAction::CreateRaw (this , &FTestLayoutWindowModule::PluginButtonClicked), FCanExecuteAction ()); FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked <FLevelEditorModule>("LevelEditor" ); { TSharedPtr<FExtender> MenuExtender = MakeShareable (new FExtender ()); MenuExtender->AddMenuExtension ("WindowLayout" , EExtensionHook::After, PluginCommands, FMenuExtensionDelegate::CreateRaw (this , &FTestLayoutWindowModule::AddMenuExtension)); LevelEditorModule.GetMenuExtensibilityManager ()->AddExtender (MenuExtender); } { TSharedPtr<FExtender> ToolbarExtender = MakeShareable (new FExtender); ToolbarExtender->AddToolBarExtension ("Settings" , EExtensionHook::After, PluginCommands, FToolBarExtensionDelegate::CreateRaw (this , &FTestLayoutWindowModule::AddToolbarExtension)); LevelEditorModule.GetToolBarExtensibilityManager ()->AddExtender (ToolbarExtender); } FGlobalTabmanager::Get ()->RegisterNomadTabSpawner (TestLayoutWindowTabName, FOnSpawnTab::CreateRaw (this , &FTestLayoutWindowModule::OnSpawnPluginTab)) .SetDisplayName (LOCTEXT ("FTestLayoutWindowTabTitle" , "TestLayoutWindow" )) .SetMenuType (ETabSpawnerMenuType::Hidden); FGlobalTabmanager::Get ()->RegisterNomadTabSpawner (InnerTabName, FOnSpawnTab::CreateLambda ([](const FSpawnTabArgs& SpawnTabArgs) { return SNew (SDockTab) .TabRole (ETabRole::NomadTab) [ SNew (STextBlock) .Text (FText::FromString ("InnerTab" )) ]; })) .SetDisplayName (LOCTEXT ("InnerTab" , "InnerTab" )) .SetMenuType (ETabSpawnerMenuType::Hidden); FGlobalTabmanager::Get ()->RegisterNomadTabSpawner (InnerTabName2, FOnSpawnTab::CreateLambda ([](const FSpawnTabArgs& SpawnTabArgs) { return SNew (SDockTab) .TabRole (ETabRole::NomadTab) [ SNew (STextBlock) .Text (FText::FromString ("InnerTab2" )) ]; })) .SetDisplayName (LOCTEXT ("InnerTab2" , "InnerTab2" )) .SetMenuType (ETabSpawnerMenuType::Hidden); }
核心处理是在插件加载的时候 StartupModule
调用 RegisterNomadTabSpawner
注册 Tab
1 2 3 4 void FTestLayoutWindowModule::PluginButtonClicked () { FGlobalTabmanager::Get ()->InvokeTab (TestLayoutWindowTabName); }
点击 GUI 会触发 Tab 生成,调用 OnSpawnPluginTab
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 TSharedRef<SDockTab> FTestLayoutWindowModule::OnSpawnPluginTab (const FSpawnTabArgs& SpawnTabArgs) { const TSharedRef<SDockTab> NomadTab = SNew (SDockTab) .TabRole (ETabRole::NomadTab); if (!TabManager.IsValid ()) { TabManager = FGlobalTabmanager::Get ()->NewTabManager (NomadTab); } if (!TabManagerLayout.IsValid ()) { TabManagerLayout = FTabManager::NewLayout ("TestLayoutWindow" ) ->AddArea ( FTabManager::NewPrimaryArea () ->SetOrientation (Orient_Vertical) ->Split ( FTabManager::NewStack () ->SetSizeCoefficient (.4 f) ->AddTab (InnerTabName, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack () ->SetSizeCoefficient (.4 f) ->AddTab (InnerTabName2, ETabState::OpenedTab) ) ); } TSharedRef<SWidget> TabContents = TabManager->RestoreFrom (TabManagerLayout.ToSharedRef (), TSharedPtr <SWindow>()).ToSharedRef (); NomadTab->SetContent ( TabContents ); return NomadTab; }
这里将之前注册的 Tab 唤起。
Viewport https://yaksue.blog.csdn.net/article/details/109258860
引入默认的 SEditorViewport
类 然后 override 方法 MakeEditorViewportClient
1 2 3 4 5 TSharedRef<FEditorViewportClient> STestLevelEditorViewport::MakeEditorViewportClient () { TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable (new FEditorViewportClient (nullptr )); return EditorViewportClient.ToSharedRef (); }
然后Slate 代码直接使用 SNew(STestLevelEditorViewport)
初始化界面即可。 不过这个方式沿用了 Viewport ,如何构建一个自定义 Viewport 呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TSharedRef<FEditorViewportClient> STestEditorViewport::MakeEditorViewportClient () { PreviewScene = MakeShareable (new FPreviewScene ()); { UStaticMesh* SM = LoadObject <UStaticMesh>(NULL , TEXT ("StaticMesh'/Engine/EngineMeshes/Cube.Cube'" ), NULL , LOAD_None, NULL ); UStaticMeshComponent* SMC = NewObject <UStaticMeshComponent>(); SMC->SetStaticMesh (SM); PreviewScene->AddComponent (SMC, FTransform::Identity); } TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable (new FEditorViewportClient (nullptr , PreviewScene.Get ())); return EditorViewportClient.ToSharedRef (); }
新建一个自定义的 FPreviewScene
,可以将物体实例化添加到场景当中。 将 PreviewScene
传入到 FEditorViewportClient
中,这样 Viewport 就显示独立的场景。
1 2 3 4 5 TSharedPtr<SWidget> STestEditorViewport::MakeViewportToolbar () { return SNew (SCommonEditorViewportToolbarBase, SharedThis (this )); }
使用上面的代码可以构建出默认 Viewport 的 Toolbar。
GraphEditor https://yaksue.blog.csdn.net/article/details/107945507 https://yaksue.blog.csdn.net/article/details/108020797 https://yaksue.blog.csdn.net/article/details/108227439 https://yaksue.blog.csdn.net/article/details/109347063
EditorMode