用UMG代替Slate编辑常见插件的界面

前言

用过Slate的同学都知道, 这玩意儿有多难用, 可读性差, 编译还贼慢, 那么能不能用UMG来代替Slate扩展和编写编辑器界面呢? 在一定程度上是可以实现的, 我们以UE默认的几种插件来简单介绍一下

image-20230117105145775

UE4与UE5在编辑器扩展方面有很大区别, 以下会提到一些, 本文以目前最新的UE5.1作为案例

EditorMode

EditorMode相对而言是最复杂的, 而且也有很特殊的作用, 比如在进入一个EditorMode以后, 场景里的某些Actor都不能选取, 这个基本上只有在这里才能灵活的使用(通过重写IsSelectionAllowed函数)

这里有兴趣的可以去看一下源码, 这个UE4和UE5差别很大

UE4在 UUnrealEdEngine::CanSelectActor里面, UE5在FActorElementLevelEditorSelectionCustomization::CanSelectActorElement

EditorMode插件在UE4和UE5的实现也是天差地别, 主要是UE4的Mode继承自FEdMode, 而UE5改成了继承自UEdMode;

可以通过创建一个默认的EditorMode插件看一下

个人觉得, UE4.27是当中的一个过渡版本, 因为看了下源码, BSP编辑使用的是FEdMode, 而Landscape已经改成了UEdMode, 想要研究EditorMode的同学还是找个合适的版本开始写, 不然这个跨版本的改动真的挺大的

回归正题, 不过两者在界面中加入UMG的方法没有什么本质差别

工具集类FModeToolkit的内容大差不差, 可以重写GetInlineContent方法直接塞进去UMG, 或者在这个函数中返回一个SWidget对象, 然后在初始化函数Init()中SNew出来, 比如

1
2
3
4
5
6
7
8
9
TSharedPtr<SWidget> FNLDEditor_ModeToolkit::GetInlineContent() const
{
return ToolkitWidget;
}
void FNLDEditor_ModeToolkit::Init(const TSharedPtr<IToolkitHost>& InitToolkitHost)
{
SAssignNew(ToolkitWidget, SNLDEditor_MainUI);
FModeToolkit::Init(InitToolkitHost);
}

或者直接丢到GetInlineContent()中去

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
TSharedPtr<SWidget> FEdModeTestEditorModeToolkit::GetInlineContent() const
{
auto WidgetClass = LoadClass<UObject>(NULL, TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/EU_Test.EU_Test_C'"));
UUserWidget* EdUI = nullptr;
if (WidgetClass)
{
UWorld* WidgetWorld = GEditor->GetEditorWorldContext().World();
if (WidgetWorld)
{
EdUI = CreateWidget<UUserWidget>(WidgetWorld, WidgetClass);
}
}
if (EdUI)
{
TSharedPtr<SWidget> WidgetTemp = FModeToolkit::GetInlineContent();
return SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
EdUI->TakeWidget()
]
+ SVerticalBox::Slot()
.AutoHeight()
[
WidgetTemp.ToSharedRef()
];
}
else
{
return FModeToolkit::GetInlineContent();
}
}

看一下UE5中的效果

image-20230116205002923

用蓝图UMG来代替slate, 好处当然就是方便, 不用每次调个尺寸都编译一下, 效率杠杠的. 不过蓝图中修改以后需要重新初始化一下插件, 也就是你需要切换至其他Mode然后再切回来

另外, 针对一些蓝图中没有的编辑器功能, 还是需要一个蓝图库来支持一下

UMG的Class类型可以丢到一个配置中, 比如游戏ini设置文件, 直接字符串写死不容易维护, 下同

Standalone Window

这个比较简单, 原本就有一个创建SDockTab的流程, 在SDockTab内把UMG加进去就可以了, 如下

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
TSharedRef<SDockTab> FWindowTestModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{


auto WidgetClass = LoadClass<UObject>(NULL, TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/EU_Test.EU_Test_C'"));
UUserWidget* EdUI = nullptr;
if (WidgetClass)
{
UWorld* WidgetWorld = GEditor->GetEditorWorldContext().World();
if (WidgetWorld)
{
EdUI = CreateWidget<UUserWidget>(WidgetWorld, WidgetClass);
}
}
if (EdUI)
{
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
// Put your tab content here!
SNew(SBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
EdUI->TakeWidget()
]
];

}

点击以后效果如下

image-20230117105608635

Toolbar Button

这个稍微特殊一点, 他其实完全可以照着前者创建一个SDockTab来弹出一个窗口, 但是我们这里做一个区别, 可不可以点击以后直接弹出UMG窗口呢? 答案是可以的, 之前有过一篇文章关于EditorUtilityWiget用cpp来运行

我们讲里面的方法再优化一下

1
2
3
4
5
6
7
8
9
void FToolbarButtonTestModule::PluginButtonClicked()
{
UEditorUtilitySubsystem* Subsys = GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>();
auto BP = UEditorAssetLibrary::LoadAsset(TEXT("/Game/EU_Test.EU_Test"));
if (auto EdBp = Cast<UEditorUtilityWidgetBlueprint>(BP))
{
Subsys->SpawnAndRegisterTab(EdBp);
}
}

点击以后直接找到蓝图, 通过Subsystem方法直接显示出来