我们先来理一理这其中有多少个类
UIManagerSubsystem
通过ini配置 ULyraUIManagerSubsystem
[/Script/LyraGame.LyraUIManagerSubsystem]
DefaultUIPolicyClass=/Game/UI/B_LyraUIPolicy.B_LyraUIPolicy_C
同时要配置一个 UGameUIPolicy 的子类 B_LyraUIPolicy_C
这个policy是必须的,否则你用了lyra的插件会崩
ULyraUIManagerSubsystem=>UGameUIManagerSubsystem=>UGameInstanceSubsystem
lyra项目=>lyra插件=>引擎
这个类存在的意义就是配置 UGameUIPolicy 这个类,然后NewObject,并在
NotifyPlayerAdded,NotifyPlayerRemoved,NotifyPlayerDestroyed 转发事件
这三个事件是从 UCommonGameInstance 转发过来的(lyra common game插件)
UCommonGameInstance重写了GameInstance的AddLocalPlayer,RemoveLocalPlayer做了转发
除此之外UCommonGameInstance处理了game session的逻辑,此时我们关注UI先忽略
UCLASS(MinimalAPI, Abstract, config = Game)
class UGameUIManagerSubsystem : public UGameInstanceSubsystem
{
public:
UE_API virtual void NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer);
UE_API virtual void NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer);
UE_API virtual void NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer);
private:
UPROPERTY(Transient)
TObjectPtr<UGameUIPolicy> CurrentPolicy = nullptr;
UPROPERTY(config, EditAnywhere)
TSoftClassPtr<UGameUIPolicy> DefaultUIPolicyClass;
lyra这个只多了一个逻辑,在tick中检查。
根据HUD->bShowHUD修改 RootLayout 的可见性
bShowHUD 什么时候会改呢?比如 ADebugCameraController::ToggleDisplay
UCLASS()
class ULyraUIManagerSubsystem : public UGameUIManagerSubsystem
{
private:
bool Tick(float DeltaTime);
void SyncRootLayoutVisibilityToShowHUD();
}
去掉一堆check,HUD->bShowHUD 变成 bShouldShowUI
if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked<UCommonLocalPlayer>(LocalPlayer)))
{
const ESlateVisibility DesiredVisibility = bShouldShowUI ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed;
if (DesiredVisibility != RootLayout->GetVisibility())
{
RootLayout->SetVisibility(DesiredVisibility);
}
}
UPrimaryGameLayout* RootLayout是个啥东西呢?

他指的是这个界面。
这个界面蓝图是 UPrimaryGameLayout 的子类
空的,只有4个 UCommonActivatableWidgetContainerBase,分别是四个层级的名字
他是由 UGameUIPolicy 关联的
UCLASS(MinimalAPI, Abstract, Blueprintable, Within = GameUIManagerSubsystem)
class UGameUIPolicy : public UObject
{
GENERATED_BODY()
private:
UPROPERTY(EditAnywhere)
TSoftClassPtr<UPrimaryGameLayout> LayoutClass;
};
那么 UPrimaryGameLayout 是什么呢?额,就是上面那个界面。他的父类是 UCommonUserWidget
UCommonUserWidget 是CommonUI 这个引擎插件的内容,他的父类是 UUserWidget
是这个插件下UI类的基类
UCommonUserWidget
分析
看一个类先看属性,其他方法一般是针对属性的操作
UCLASS(MinimalAPI, ClassGroup = UI, meta = (Category = "Common UI", DisableNativeTick))
class UCommonUserWidget : public UUserWidget
{
// ...
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = true))
bool bDisplayInActionBar = false;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = true))
bool bConsumePointerInput = false;
TArray<FUIActionBindingHandle> ActionBindings;
TArray<TWeakObjectPtr<const UWidget>> ScrollRecipients;
};
bDisplayInActionBar:
在 CommonUI 中,通常会有一个 操作栏(Common Bound Action Bar),位于屏幕底部或其他位置,自动显示当前可用的按键提示(例如:[A] 确认 [B] 返回)。例如lyra的设置界面就true了。
比如你去看 /Game/UI/Settings/W_LyraSettingScreen.W_LyraSettingScreen 这个设置界面的类设置
具体还要看看 UCommonBoundActionBar 的逻辑
bConsumePointerInput:
如果设置了,大多数事件不会继续向父类传递,达到屏蔽按键的功能。这种函数有若干
FReply UCommonUserWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
return bConsumePointerInput ? FReply::Handled() : Super::NativeOnMouseButtonUp(InGeometry, InMouseEvent);
}
ScrollRecipients 滚动接受者,当玩家使用手柄右摇杆(或配置的其他滚动轴)时,CommonUI 会自动让这个子控件进行滚动,而不需要你手动去 Tick 里写滚动逻辑。
ActionBindings:
这个我感觉还是比较常用的,在lyra中
RegisterUIActionBinding(FBindUIActionArgs(FUIActionTag::ConvertChecked(TAG_UI_ACTION_ESCAPE), false, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleEscapeAction)));
RegisterUIActionBinding(FBindUIActionArgs(按键的gameplay tag, bDisplayInActionBar, 回调函数);
比如在项目设置-> Common UI Input Settings 设置了ESC会触发这个tag,注册了这个就会跑这个回调函数
总结
UserWidget是UE所有UI的基类
UCommonUserWidget 是 CommonUI 概念中所有UI的基类。封转了
- 按键直接触发 gameplay tag 事件,再到回调的流程
- 比较方便的UI输入屏蔽,不再传递给父类
- 滚动控件功能的强化,原来只能拖动,现在手柄也能触发
回到他的子类 UPrimaryGameLayout
UPrimaryGameLayout
我们叫他根布局吧 RootLayout
前面我们看到这是一个几乎空的蓝图,里面是4个层级栈
UCLASS(MinimalAPI, Abstract, meta = (DisableNativeTick))
class UPrimaryGameLayout : public UCommonUserWidget
{
GENERATED_BODY()
private:
// 是否休眠,逻辑是注释的,先忽略这个变量吧
bool bIsDormant = false;
// Lets us keep track of all suspended input tokens so that multiple async UIs can be loading and we correctly suspend
// for the duration of all of them.
TArray<FName> SuspendInputTokens;
// The registered layers for the primary layout.
UPROPERTY(Transient, meta = (Categories = "UI.Layer"))
TMap<FGameplayTag, TObjectPtr<UCommonActivatableWidgetContainerBase>> Layers;
};
Layer的管理
SuspendInputTokens 是一个Tarray
TMap<FGameplayTag, TObjectPtr
前面我们说过这个 UI 上有4个栈控件。他的蓝图会调用注册

void UPrimaryGameLayout::RegisterLayer(FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget)
{
if (!IsDesignTime())
{
LayerWidget->OnTransitioningChanged.AddUObject(this, &UPrimaryGameLayout::OnWidgetStackTransitioning);
LayerWidget->SetTransitionDuration(0.0);
Layers.Add(LayerTag, LayerWidget);
}
}
绑定了回调到 OnWidgetStackTransitioning
然后加到这个TArray,这个代理什么时候触发呢?
OnTransitioningChanged 是 CommonUI 的容器控件(UCommonActivatableWidgetContainerBase 或其底层的 Switcher)暴露的一个事件委托。
它通知外界“界面切换动画的状态发生了改变”。
它携带一个 bool 参数:过度动画开始和结束
在lyra中
PushContentToLayer
触发 OnTransitioningChanged(true) -> 动画开始播放(例如淡入淡出、滑动)。
等待动画播放完毕。
触发 OnTransitioningChanged(false) -> 动画结束,新界面完全就绪。
这里设置了SetTransitionDuration(0.0),瞬切,但是流程是有的。
这个操作就是过度中不许玩家操作
void UPrimaryGameLayout::OnWidgetStackTransitioning(UCommonActivatableWidgetContainerBase* Widget, bool bIsTransitioning)
{
if (bIsTransitioning)
{
const FName SuspendToken = UCommonUIExtensions::SuspendInputForPlayer(GetOwningLocalPlayer(), TEXT("GlobalStackTransion"));
SuspendInputTokens.Add(SuspendToken);
}
else
{
if (ensure(SuspendInputTokens.Num() > 0))
{
const FName SuspendToken = SuspendInputTokens.Pop();
UCommonUIExtensions::ResumeInputForPlayer(GetOwningLocalPlayer(), SuspendToken);
}
}
}
这里又引出了
UCommonUIExtensions,UCommonInputSubsystem 一会再来
还有 UCommonActivatableWidgetContainerBase 这个栈我们还没看过
其他接口
属性看好了,剩下的是
PushWidgetToLayerStack
PushWidgetToLayerStackAsync
去掉加载流程,最后都会走到这一步
if (UCommonActivatableWidgetContainerBase* Layer = GetLayerWidget(LayerName))
{
return Layer->AddWidget<ActivatableWidgetT>(ActivatableWidgetClass, InitInstanceFunc);
}
GetPrimaryGameLayoutForPrimaryPlayer
一路拿到 UGameUIManagerSubsystem 再到 policy
最后
UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CommonLocalPlayer)
===
UGameUIPolicy
前面我们只知道配置了一个类 LayoutClass
然后 ULyraUIManagerSubsystem 转发事件 NotifyPlayer Add Remove Destoryed
UCLASS(MinimalAPI, Abstract, Blueprintable, Within = GameUIManagerSubsystem)
class UGameUIPolicy : public UObject
{
GENERATED_BODY()
private:
ELocalMultiplayerInteractionMode LocalMultiplayerInteractionMode = ELocalMultiplayerInteractionMode::PrimaryOnly;
UPROPERTY(EditAnywhere)
TSoftClassPtr<UPrimaryGameLayout> LayoutClass;
UPROPERTY(Transient)
TArray<FRootViewportLayoutInfo> RootViewportLayouts;
private:
UE_API void NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer);
UE_API void NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer);
UE_API void NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer);
};
本质上是玩家创建的时候
if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
{
AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout);
LayoutInfo->bAddedToViewport = true;
}
else
{
CreateLayoutWidget(LocalPlayer);
}
如果对于这个localplayer是第一次
void UGameUIPolicy::CreateLayoutWidget(UCommonLocalPlayer* LocalPlayer)
{
if (APlayerController* PlayerController = LocalPlayer->GetPlayerController(GetWorld()))
{
// 这个就是我们配置的 layout class 加载出来
TSubclassOf<UPrimaryGameLayout> LayoutWidgetClass = GetLayoutWidgetClass(LocalPlayer);
if (ensure(LayoutWidgetClass && !LayoutWidgetClass->HasAnyClassFlags(CLASS_Abstract)))
{
// 然后 CreateWidget,加入 RootViewportLayouts 引用一下
UPrimaryGameLayout* NewLayoutObject = CreateWidget<UPrimaryGameLayout>(PlayerController, LayoutWidgetClass);
RootViewportLayouts.Emplace(LocalPlayer, NewLayoutObject, true);
// 然后加到视口
AddLayoutToViewport(LocalPlayer, NewLayoutObject);
}
}
}
AddToPlayerScreen 很眼熟了,老版本我们用的就是CreateWidget 和 AddToViewport/AddToPlayerScreen
void UGameUIPolicy::AddLayoutToViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout)
{
UE_LOG(LogCommonGame, Log, TEXT("[%s] is adding player [%s]'s root layout [%s] to the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout));
Layout->SetPlayerContext(FLocalPlayerContext(LocalPlayer));
Layout->AddToPlayerScreen(1000);
OnRootLayoutAddedToViewport(LocalPlayer, Layout);
}
总结:
我们定义了UIManagerSubSystem 这是GameInstance SubSystem。
UIManagerSubSystem 配置 UI Policy。
UI Policy中又配置了根布局,那4个栈的类
通过重写GameInstance的 Add/Remove LocalPlayer 把相关事件一路转发给policy。
创建和销毁这个根布局(这个过程本质上是CreateWidget和AddToPlayerScreen)
UCommonActivatableWidgetContainerBase
这是4个栈,其实这是容器基类。UI哪里点过来是这个类,他还有
UCommonActivatableWidgetStack 和 UCommonActivatableWidgetQueue 两个子类
Slate相关
先看属性
UCLASS(MinimalAPI, Abstract)
class UCommonActivatableWidgetContainerBase : public UWidget
{
// ...
/** The type of transition to play between widgets */
UPROPERTY(EditAnywhere, Category = Transition)
ECommonSwitcherTransition TransitionType;
/** The curve function type to apply to the transition animation */
UPROPERTY(EditAnywhere, Category = Transition)
ETransitionCurve TransitionCurveType;
/** The total duration of a single transition between widgets */
UPROPERTY(EditAnywhere, Category = Transition)
float TransitionDuration = 0.4f;
/**
* Whether to completely reset the pool of widgets when slate resources are released.
* This usually happens when changing maps. You may not want to have all frontend screens loaded taking up memory while in game and vice versa.
* Enabling this means widgets will have to be loaded again when re-entering the map next time.
*/
UPROPERTY(EditAnywhere, Category = Performance)
bool bResetPoolWhenReleasingSlateResources = false;
/**
* Controls how we will choose another widget if a transitioning widget is removed during the transition.
* Note for Queues and Stacks, ECommonSwitcherTransitionFallbackStrategy::Previous is a good option.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Transition)
ECommonSwitcherTransitionFallbackStrategy TransitionFallbackStrategy = ECommonSwitcherTransitionFallbackStrategy::None;
// ...
TSharedPtr<SOverlay> MyOverlay;
TSharedPtr<SSpacer> MyInputGuard;
TSharedPtr<SCommonAnimatedSwitcher> MySwitcher;
};
这些属性是这里用的,用于传递给底层Slate类 SCommonAnimatedSwitcher 它负责处理子界面的淡入淡出、滑动等动画。
https://www.cwlgame.cn/2024/06/25/ue-slate/
TSharedRef<SWidget> UCommonActivatableWidgetContainerBase::RebuildWidget()
{
// slate 的写法就不看了
}
在我们 UUserWidget::AddToPlayerScreen 的时候
一路会跑到这里
bool UGameViewportSubsystem::AddToScreen(UWidget* Widget, ULocalPlayer* Player, FGameViewportWidgetSlot& Slot)
{
SConstraintCanvas::FSlot* RawSlot = nullptr;
// 构造...
RawSlot->AttachWidget(Widget->TakeWidget());
}
UWidget::TakeWidget=>UWidget::TakeWidget_Private=>RebuildWidget 这样调用
Slate是UE底层的C++ UI框架,UserWidget是用户层的封装。
所有Slate都是 swidget的子类,通过 OnPaint 的参数 FGeometry& AllottedGeometry 返回几何体给渲染流程。
===
AddWidget
UPROPERTY(Transient)
TArray<TObjectPtr<UCommonActivatableWidget>> WidgetList;
UPROPERTY(Transient)
TObjectPtr<UCommonActivatableWidget> DisplayedWidget;
UPROPERTY(Transient)
FUserWidgetPool GeneratedWidgetsPool;
外部调用这个容器都是走 AddWidget
从UMG控件对象池中get or get ,然后加到 WidgetList
UCommonActivatableWidget* UCommonActivatableWidgetContainerBase::AddWidgetInternal(TSubclassOf<UCommonActivatableWidget> ActivatableWidgetClass, TFunctionRef<void(UCommonActivatableWidget&)> InitFunc)
{
if (UCommonActivatableWidget* WidgetInstance = GeneratedWidgetsPool.GetOrCreateInstance(ActivatableWidgetClass))
{
InitFunc(*WidgetInstance);
RegisterInstanceInternal(*WidgetInstance);
return WidgetInstance;
}
return nullptr;
}
void UCommonActivatableWidgetContainerBase::RegisterInstanceInternal(UCommonActivatableWidget& NewWidget)
{
UE_LOG(LogCommonUI, VeryVerbose, TEXT("UCommonActivatableWidgetContainerBase::RegisterInstanceInternal() NewWidget: %s"), *NewWidget.GetName());
// @TODO: Log if bAutoActivate is true on the provided widget, since it quite simply makes no sense.
if (ensure(!WidgetList.Contains(&NewWidget)))
{
WidgetList.Add(&NewWidget);
OnWidgetAddedToList(NewWidget);
}
}
而这个 DisplayedWidget 是显示的widget指针
在激活的UI变化的时候构建
void UCommonActivatableWidgetContainerBase::HandleActiveIndexChanged(int32 ActiveWidgetIndex)
{
// ...
DisplayedWidget = ActivatableWidgetFromSlate(MySwitcher->GetActiveWidget());
}
释放
TArray<TSharedPtr<SWidget>> ReleasedWidgets;
bool bRemoveDisplayedWidgetPostTransition = false;
mutable FOnDisplayedWidgetChanged OnDisplayedWidgetChangedEvent;
注释说这里临界情况有点复杂,他希望被销毁的控件在内存中延迟一帧,所以有了这几个变量,另外是当做变化时候的代理
他的两个子类只是修改了维护切换的方式是栈还是队列。
总结
维护了一个栈中多个 UCommonActivatableWidget 的push, pop。过度。当前显示的界面。