前言
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(.4f) ->AddTab(InnerTabName, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(.4f) ->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