前言 游戏中经常会有需要一些调试菜单或者GM菜单,如下
功能上没有太大的问题,但是大面积的UI会非常影响游戏内容的观察,甚至并不能与游戏画面的同时运行
那么整理并记录一下更便捷的UI方案
纯粹的编辑器UI
方案是打开一个EU(EditorUtility),然后监听或者执行游戏内的逻辑和信息
当然也可以不是EU, 一般的UMG也可以,但是既然是仅编辑器运行的,用EU也可以方便蓝图调用很多编辑器API,偶尔还是需要的
这里主要有2个注意点, 一个是如何方便的打开EU, 还有是EU内如何正确的获取游戏世界的信息
打开EU 最粗暴的方法是找到EU资源然后右键打开,但是太不方便
如果调试功能比较独立,建议封装成插件使用,然后插件本身去创建一个toolbar或者window button,通过这些编辑器按钮打开EU是比较方便的
注册按钮创建插件是自带的,这里略过
核心就是用EU蓝图资源来填充窗口,简单的代码如下
获取正确的世界 默认情况下, EU内的蓝图逻辑获取的世界都是Editor的世界,比如你可以访问放在场景中的actor对象,对齐进行修改操作,但这不是我们想要的,我们需要的是游戏运行以后的世界对象的信息
所以必须得用cpp封装一些方便蓝图使用的方法
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 void UAaDebuggerWidgetBase::NativeConstruct () { Super::NativeConstruct (); FEditorDelegates::PostPIEStarted.AddUObject (this , &UAaDebuggerWidgetBase::HandlePIEStarted); FEditorDelegates::EndPIE.AddUObject (this , &UAaDebuggerWidgetBase::HandlePIEEnded); } void UAaDebuggerWidgetBase::NativeDestruct () { FEditorDelegates::BeginPIE.RemoveAll (this ); FEditorDelegates::EndPIE.RemoveAll (this ); Super::NativeDestruct (); } UWorld* UAaDebuggerWidgetBase::GetPIEWorld () const { if (GEditor) { if (GEditor->GetPIEWorldContext ()) { return GEditor->GetPIEWorldContext ()->World (); } } return nullptr ; } UWorld* UAaDebuggerWidgetBase::GetEditorWorld () const { return GEditor ? GEditor->GetEditorWorldContext ().World () : nullptr ; } bool UAaDebuggerWidgetBase::IsInPIE () const { return GetPIEWorld () != nullptr ; } void UAaDebuggerWidgetBase::HandlePIEStarted (bool bIsSimulating) { OnPIEStarted (bIsSimulating); } void UAaDebuggerWidgetBase::HandlePIEEnded (bool bIsSimulating) { OnPIEEnded (bIsSimulating); }
这样在蓝图中就能正确的获取actor信息,也可以监听到PIE运行的时机了
同时支持运行时的UI 如果UI在打包环境下还是需要支持的,那么EU就不够了,但是又不想UI充满整个屏幕,那么就要用到另外的方法
核心的类就是SWindow
我们在某个具体的类里面加一个可以方法
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 UUserWidget* URGUIManager::OpenUMGInNewWindow (TSubclassOf<UUserWidget> WidgetClass,FText Title, int32 Width, int32 Height, APlayerController* OwningPC) { if (!GetWorld () || !WidgetClass) return nullptr ; if (!OwningPC) { if (ULocalPlayer* LP = GetWorld ()->GetFirstLocalPlayerFromController ()) OwningPC = LP->GetPlayerController (GetWorld ()); } UUserWidget* Widget = CreateWidget<UUserWidget>(OwningPC, WidgetClass); if (!Widget) return nullptr ; TSharedRef<SWindow> NewWindow = SNew (SWindow) .Title (Title) .ClientSize (FVector2D (Width, Height)) .SupportsMaximize (true ) .SupportsMinimize (true ) .HasCloseButton (true ) .IsTopmostWindow (false ); TSharedRef<SWidget> SlateWidget = Widget->TakeWidget (); NewWindow->SetContent (SlateWidget); NewWindow->SetOnWindowClosed (FOnWindowClosed::CreateUObject (this , &ThisClass::OnWindowClosed)); FSlateApplication::Get ().AddWindow (NewWindow); FRuntimeWindowEntry Entry; Entry.Window = NewWindow; Entry.Widget = Widget; OpenedWindows.Add (MoveTemp (Entry)); SpawnedWidgets.Add (Widget); return Widget; }
因为要保存起来,所以建议不要用函数库的方法,可以是某个subsystem
这样GM菜单就是独立存在的(可以放到副屏幕上)
需要注意的是要正确的管理好GC,需要在合适的时机移除掉UI
比如
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 FGameDelegates::Get ().GetEndPlayMapDelegate ().AddUObject (this , &ThisClass::CloseAllRuntimeWindows); void URGUIManager::DetachSlateFromWindow (const TSharedRef<SWindow>& InWindow, UUserWidget* Widget) { InWindow->SetContent (SNullWidget::NullWidget); if (Widget) { Widget->ReleaseSlateResources (true ); } } void URGUIManager::CloseAllRuntimeWindows () { if (!FSlateApplication::IsInitialized ()) return ; TArray<FRuntimeWindowEntry> ToClose = OpenedWindows; for (FRuntimeWindowEntry& E : ToClose) { if (E.Window.IsValid ()) { E.Window->SetOnWindowClosed (FOnWindowClosed ()); DetachSlateFromWindow (E.Window.ToSharedRef (), E.Widget.Get ()); } } for (FRuntimeWindowEntry& E : ToClose) { if (E.Window.IsValid ()) { FSlateApplication::Get ().RequestDestroyWindow (E.Window.ToSharedRef ()); } } OpenedWindows.Reset (); SpawnedWidgets.Reset (); }