Published on

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

Authors
  • avatar
    Name
    东哥
    Twitter

前言

用过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出来, 比如

`TSharedPtr<SWidget>` FNLDEditor_ModeToolkit::GetInlineContent() const
{
	return ToolkitWidget; 
}
void FNLDEditor_ModeToolkit::Init(const `TSharedPtr<IToolkitHost>`& InitToolkitHost)
{	
    SAssignNew(ToolkitWidget, SNLDEditor_MainUI);
	FModeToolkit::Init(InitToolkitHost);
}

或者直接丢到GetInlineContent()中去

`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加进去就可以了, 如下

`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来运行

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

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方法直接显示出来