前言

  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 需要有三个类

  1. Asset (UObject)
  2. AssetFactory (UFactory)
  3. 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()
{
// NOTE: 注册新的分类
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
// Put your tab content here!
SNew(SOverlay)
+ SOverlay::Slot()//底层
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().FillWidth(0.3f)//占30%
[
SNew(SButton)1
]
+ SHorizontalBox::Slot().FillWidth(0.7f)//占70%
[
SNew(SVerticalBox)
+ SVerticalBox::Slot().FillHeight(0.5f)//占50%
[
SNew(SButton)
]
+ SVerticalBox::Slot().FillHeight(0.5f)//占50%
[
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 的构成和配置。

alt

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()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module

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

// ! InnerTab的内容:
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);

// ! InnerTab2的内容:
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);

//创建TabManager
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