一、外部接入接口:UGameUIManagerSubsystem
主要用来从
GameInstance
获取玩家的加入退出和销毁通知,并传入
UGameUIPolicy
DefaultUIPolicyClass
在
DefaultGame.ini
中进行配置
UPROPERTY(config, EditAnywhere)
TSoftClassPtr<UGameUIPolicy> DefaultUIPolicyClass;
void UGameUIManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
if (!CurrentPolicy && !DefaultUIPolicyClass.IsNull()){
TSubclassOf<UGameUIPolicy> PolicyClass = DefaultUIPolicyClass.LoadSynchronous();
SwitchToPolicy(NewObject<UGameUIPolicy>(this, PolicyClass));}
}
void UGameUIManagerSubsystem::NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer){
if (ensure(LocalPlayer) && CurrentPolicy){
CurrentPolicy->NotifyPlayerAdded(LocalPlayer);
}}
void UGameUIManagerSubsystem::NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer){
if (LocalPlayer && CurrentPolicy){
CurrentPolicy->NotifyPlayerRemoved(LocalPlayer);
}}
二、对内总管:UGameUIPolicy
用来自动控制·UPrimaryGameLayout·不同情况下(主要是Player的改动)的创建与销毁
LayoutClass
可以由
GameUIPolicy
的蓝图实例来指定
UPROPERTY(EditAnywhere)
TSoftClassPtr<UPrimaryGameLayout> LayoutClass;
void UGameUIPolicy::NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer){
LocalPlayer->OnPlayerControllerSet.AddWeakLambda(this, [this](UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController){
NotifyPlayerRemoved(LocalPlayer);
if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)){
AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout);
LayoutInfo->bAddedToViewport = true;}
else{
CreateLayoutWidget(LocalPlayer);}
});
if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)){
AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout);
LayoutInfo->bAddedToViewport = true;}
else{
CreateLayoutWidget(LocalPlayer);
}}
void UGameUIPolicy::CreateLayoutWidget(UCommonLocalPlayer* LocalPlayer)
{
if (APlayerController* PlayerController = LocalPlayer->GetPlayerController(GetWorld())){
TSubclassOf<UPrimaryGameLayout> LayoutWidgetClass = GetLayoutWidgetClass(LocalPlayer);
if (ensure(LayoutWidgetClass && !LayoutWidgetClass->HasAnyClassFlags(CLASS_Abstract))){
UPrimaryGameLayout* NewLayoutObject = CreateWidget<UPrimaryGameLayout>(PlayerController, LayoutWidgetClass);
RootViewportLayouts.Emplace(LocalPlayer, NewLayoutObject, true);
AddLayoutToViewport(LocalPlayer, NewLayoutObject);
}}
}
三、Layout分层管理:UPrimaryGameLayout
作为游戏的主UI,负责对整个UI框架逻辑的管理,包括栈状态机、Layout、动态加载等。不参与布局与响应逻辑。继承自
UCommonUserWidget
UPROPERTY(Transient, meta = (Categories = "UI.Layer"))
TMap<FGameplayTag, UCommonActivatableWidgetContainerBase*> Layers;
template <typename ActivatableWidgetT = UCommonActivatableWidget>
TSharedPtr<FStreamableHandle> PushWidgetToLayerStackAsync(FGameplayTag LayerName, bool bSuspendInputUntilComplete, TSoftClassPtr<UCommonActivatableWidget> ActivatableWidgetClass, TFunction<void(EAsyncWidgetLayerState, ActivatableWidgetT*)> StateFunc)
{
static_assert(TIsDerivedFrom<ActivatableWidgetT, UCommonActivatableWidget>::IsDerived, "Only CommonActivatableWidgets can be used here");
static FName NAME_PushingWidgetToLayer("PushingWidgetToLayer");
const FName SuspendInputToken = bSuspendInputUntilComplete ? UCommonUIExtensions::SuspendInputForPlayer(GetOwningPlayer(), NAME_PushingWidgetToLayer) : NAME_None;
FStreamableManager& StreamableManager = UAssetManager::Get().GetStreamableManager();
TSharedPtr<FStreamableHandle> StreamingHandle = StreamableManager.RequestAsyncLoad(ActivatableWidgetClass.ToSoftObjectPath(), FStreamableDelegate::CreateWeakLambda(this,
[this, LayerName, ActivatableWidgetClass, StateFunc, SuspendInputToken]()
{
UCommonUIExtensions::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken);
ActivatableWidgetT* Widget = PushWidgetToLayerStack<ActivatableWidgetT>(LayerName, ActivatableWidgetClass.Get(), [StateFunc](ActivatableWidgetT& WidgetToInit) {
StateFunc(EAsyncWidgetLayerState::Initialize, &WidgetToInit);
});
StateFunc(EAsyncWidgetLayerState::AfterPush, Widget);
})
);
// Setup a cancel delegate so that we can resume input if this handler is canceled.
StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this,
[this, StateFunc, SuspendInputToken]()
{
UCommonUIExtensions::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken);
StateFunc(EAsyncWidgetLayerState::Canceled, nullptr);
})
);
return StreamingHandle;
}
template <typename ActivatableWidgetT = UCommonActivatableWidget>
ActivatableWidgetT* PushWidgetToLayerStack(FGameplayTag LayerName, UClass* ActivatableWidgetClass, TFunctionRef<void(ActivatableWidgetT&)> InitInstanceFunc)
{
static_assert(TIsDerivedFrom<ActivatableWidgetT, UCommonActivatableWidget>::IsDerived, "Only CommonActivatableWidgets can be used here");
if (UCommonActivatableWidgetContainerBase* Layer = GetLayerWidget(LayerName))
{
return Layer->AddWidget<ActivatableWidgetT>(ActivatableWidgetClass, InitInstanceFunc);
}
return nullptr;
}
void UPrimaryGameLayout::RegisterLayer(FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget)
{
if (!IsDesignTime()){
//过渡相关
LayerWidget->OnTransitioningChanged.AddUObject(this, &UPrimaryGameLayout::OnWidgetStackTransitioning);
LayerWidget->SetTransitionDuration(0.0);
Layers.Add(LayerTag, LayerWidget);}
}
bSuspendInputUntilComplete
:在ui加载完成前暂停布局,防止玩家按下delete等误操作
RegisterLayer
注册新的
LayerWidget
容器到
PrimaryWidget
中,并将Tag作为Key
PushWidgetToLayerStackAsync
将
UCommonActivatableWidget
异步载入后推入对应Tag的
UCommonActivatableWidgetContainerBase
容器(目前是Stack)中
蓝图节点中与之对应的Task:
PushContentTolayerForPlayer
由此,实现了UI的动态加载以及层级显示问题
蓝图实例
W_OverallUILayout
仅用于确定层级和Layer注册
四、Widget管理容器:UCommonActivatableWidgetContainerBase
Widget基础容器、Layout层级管理来使用一个Container就是一层Layout,而不再使用优先级
继承自UWidget,不参与布局与响应逻辑。
所有参数:
UPROPERTY(Transient)
TArray<UCommonActivatableWidget*> WidgetList;
UPROPERTY(Transient)
FUserWidgetPool GeneratedWidgetsPool;
UPROPERTY(Transient)
UCommonActivatableWidget* DisplayedWidget;
TSharedPtr<SCommonAnimatedSwitcher> MySwitcher;
TSharedPtr<SOverlay> MyOverlay;
TSharedPtr<SSpacer> MyInputGuard;
UPROPERTY(EditAnywhere, Category = Transition)
ECommonSwitcherTransition TransitionType;
UPROPERTY(EditAnywhere, Category = Transition)
ETransitionCurve TransitionCurveType;
UPROPERTY(EditAnywhere, Category = Transition)
float TransitionDuration = 0.4f;
-
容器
WidgetList
、
GeneratedWidgetsPool
与
DisplayedWidget
WidgetList
:存储激活的Widget
GeneratedWidgetsPool
:存储整个Widget池并负责Widget的动态创建与销毁,
DisplayedWidget
受
MySwitcher
确定当前顶层的Widget -
UI布局
MySwitcher
:储存并显示位于顶部的SWidget,其在变化时会更新
DisplayedWidget
为对应的Widget
MyOverlay
:UI的顶层布局
MyInputGuard
:布局的一部分,没有细看 -
过渡动画参数
不表
AddWidget
template <typename ActivatableWidgetT = UCommonActivatableWidget>
ActivatableWidgetT* AddWidget(TSubclassOf<UCommonActivatableWidget> ActivatableWidgetClass)
{
// Don't actually add the widget if the cast will fail
if (ActivatableWidgetClass && ActivatableWidgetClass->IsChildOf<ActivatableWidgetT>()){
return Cast<ActivatableWidgetT>(AddWidgetInternal(ActivatableWidgetClass, [](UCommonActivatableWidget&) {}));
}
return nullptr;
}
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)
{
if (ensure(!WidgetList.Contains(&NewWidget))){
WidgetList.Add(&NewWidget);
OnWidgetAddedToList(NewWidget);
}}
void UCommonActivatableWidgetStack::OnWidgetAddedToList(UCommonActivatableWidget& AddedWidget)
{
unimplamentation();
}}
首先创建(已有则略过)并添加Widget的实例到
GeneratedWidgetsPool
中,然后注册进
WidgetList
中。
可以看到,
GeneratedWidgetsPool
和
WidgetList
都仅仅是用于存储Widget本体与其对应SWidget的容器,都无法控制容器中Widget的显示与隐藏。所以在这里Widget仅仅是进入了容器,但对Widget的进一步处理被设置为unimplamentation.
GeneratedWidgetsPool
仅仅传入的参数仅仅是Class,如果有多个实例的话不确定会返回哪一个,详情请看
GeneratedWidgetsPool
RemoveWidget
void UCommonActivatableWidgetContainerBase::RemoveWidget(UCommonActivatableWidget& WidgetToRemove)
{
if (&WidgetToRemove == GetActiveWidget()){
// To remove the active widget, just deactivate it (if it's already deactivated, then we're already in the process of ditching it)
if (WidgetToRemove.IsActivated()){
WidgetToRemove.DeactivateWidget();}
else{
bRemoveDisplayedWidgetPostTransition = true;
}}
else{
// Otherwise if the widget isn't actually being shown right now, yank it right on out
TSharedPtr<SWidget> CachedWidget = WidgetToRemove.GetCachedWidget();
if (CachedWidget && MySwitcher){
ReleaseWidget(CachedWidget.ToSharedRef());
}}
}
void UCommonActivatableWidgetContainerBase::ReleaseWidget(const TSharedRef<SWidget>& WidgetToRelease)
{
if (UCommonActivatableWidget* ActivatableWidget = ActivatableWidgetFromSlate(WidgetToRelease)){
GeneratedWidgetsPool.Release(ActivatableWidget, true);
WidgetList.Remove(ActivatableWidget);}
if (MySwitcher->RemoveSlot(WidgetToRelease) != INDEX_NONE){
ReleasedWidgets.Add(WidgetToRelease);
if (ReleasedWidgets.Num() == 1){
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateWeakLambda(this,
[this](float){
QUICK_SCOPE_CYCLE_COUNTER(STAT_UCommonActivatableWidgetContainerBase_ReleaseWidget);
ReleasedWidgets.Reset();
return false;
}));}}
}
void UCommonActivatableWidgetContainerBase::HandleActiveWidgetDeactivated(UCommonActivatableWidget* DeactivatedWidget)
{
if (ensure(DeactivatedWidget == DisplayedWidget) && MySwitcher && MySwitcher->GetActiveWidgetIndex() > 0){
DisplayedWidget->OnDeactivated().RemoveAll(this);
MySwitcher->TransitionToIndex(MySwitcher->GetActiveWidgetIndex() - 1);
}
}
void UCommonActivatableWidgetContainerBase::HandleActiveIndexChanged(int32 ActiveWidgetIndex)
{
// 1
while (MySwitcher->GetNumWidgets() - 1 > ActiveWidgetIndex){
TSharedPtr<SWidget> WidgetToRelease = MySwitcher->GetWidget(MySwitcher->GetNumWidgets() - 1);
if (ensure(WidgetToRelease)){
ReleaseWidget(WidgetToRelease.ToSharedRef());
}}
//2. Also remove the widget that we just transitioned away from if desired
if (DisplayedWidget && bRemoveDisplayedWidgetPostTransition)
{
if (TSharedPtr<SWidget> DisplayedSlateWidget = DisplayedWidget->GetCachedWidget())
{
ReleaseWidget(DisplayedSlateWidget.ToSharedRef());
}
}
bRemoveDisplayedWidgetPostTransition = false;
// Activate the widget that's now being displayed
DisplayedWidget = ActivatableWidgetFromSlate(MySwitcher->GetActiveWidget());
if (DisplayedWidget)
{
SetVisibility(ESlateVisibility::SelfHitTestInvisible);
DisplayedWidget->OnDeactivated().AddUObject(this, &UCommonActivatableWidgetContainerBase::HandleActiveWidgetDeactivated, DisplayedWidget);
DisplayedWidget->ActivateWidget();
if (UWorld* MyWorld = GetWorld())
{
FTimerManager& TimerManager = MyWorld->GetTimerManager();
TimerManager.SetTimerForNextTick(FSimpleDelegate::CreateWeakLambda(this, [this]() { InvalidateLayoutAndVolatility(); }));
}
}
else
{
SetVisibility(ESlateVisibility::Collapsed);
}
OnDisplayedWidgetChanged().Broadcast(DisplayedWidget);
}
-
如果是正在激活的,则根据是否是可激活执行取消激活或者标记关闭。否则进行Release,将Widget从
GeneratedWidgetsPool和WidgetList
移除,并从
MySwitcher
中进行移除,最后将该Widget对应的的SWidget扔进
ReleasedWidgets
中。 -
MySwitcher
决定了Widget的显示层级。添加时并没有规定进入
MySwitcher
的方式,而不管什么方式进入
MySwitcher
的,移除的时候都一样。
Release时将MySwitcher的ActiveIndex设置为其所在的下一层,而**TransitionToIndex()**会执行绑定的
HandleActiveIndexChanged
。
HandleActiveIndexChanged:
- 将当前激活Index之上的Widget都Deactive
- 如果需要的话我们也release刚刚显示的Widget?(需要查看别的地方)
-
激活现在在MySwitcher中激活的Widget。这里对
DisplayedWidget
进行了赋值
ClearWidget
void UCommonActivatableWidgetContainerBase::ClearWidgets()
{
SetSwitcherIndex(0);
}
void UCommonActivatableWidgetContainerBase::SetSwitcherIndex(int32 TargetIndex, bool bInstantTransition /*= false*/)
{
if (MySwitcher && MySwitcher->GetActiveWidgetIndex() != TargetIndex){
if (DisplayedWidget){
DisplayedWidget->OnDeactivated().RemoveAll(this);
if (DisplayedWidget->IsActivated()){
DisplayedWidget->DeactivateWidget();
}
else if (MySwitcher->GetActiveWidgetIndex() != 0)
{
// The displayed widget has already been deactivated by something other than us, so it should be removed from the container
// We still need it to remain briefly though until we transition to the new index - then we can remove this entry's slot
bRemoveDisplayedWidgetPostTransition = true;
}
}
MySwitcher->TransitionToIndex(TargetIndex, bInstantTransition);
}
}
首先将正在激活的Widget关闭,然后设置
MySwitcher
的Index为0。
GetActiveWidget
UCommonActivatableWidget* UCommonActivatableWidgetContainerBase::GetActiveWidget() const
{
return MySwitcher ? ActivatableWidgetFromSlate(MySwitcher->GetActiveWidget()) : nullptr;
}
返回
MySwitcher
的
ActiveWidget
,而不是自己存储的
DisplayedWidget
可以看出显示的主导在于
MySwitcher
而不是另外两个容器
UCommonActivatableWidgetStack
将进入
MySwitcher
的方式设置为栈
给栈提供了一个
RootWidget
UPROPERTY(EditAnywhere, Category = Content)
TSubclassOf<UCommonActivatableWidget> RootContentWidgetClass;
UPROPERTY(Transient)
UCommonActivatableWidget* RootContentWidget;
void UCommonActivatableWidgetStack::SynchronizeProperties()
{
Super::SynchronizeProperties();
#if WITH_EDITOR
if (IsDesignTime() && RootContentWidget && RootContentWidget->GetClass() != RootContentWidgetClass){
// At design time, account for the possibility of the preview class changing
if (RootContentWidget->GetCachedWidget()){
MySwitcher->GetChildSlot(0)->DetachWidget();}
RootContentWidget = nullptr;}
#endif
if (!RootContentWidget && RootContentWidgetClass){
// Establish the root content as the blank 0th slot content
RootContentWidget = CreateWidget<UCommonActivatableWidget>(this, RootContentWidgetClass);
MySwitcher->GetChildSlot(0)->AttachWidget(RootContentWidget->TakeWidget());
SetVisibility(ESlateVisibility::SelfHitTestInvisible);
}
}
void UCommonActivatableWidgetStack::OnWidgetAddedToList(UCommonActivatableWidget& AddedWidget)
{
if (MySwitcher){
MySwitcher->AddSlot() [AddedWidget.TakeWidget()];
SetSwitcherIndex(MySwitcher->GetNumWidgets() - 1);
}
}
SynchronizeProperties
:
编辑器下如果RootContentWidget与RootContentWidgetClass的类型不同,直接Detach,见英文注释
如果当前还没有RootContentWidget则创建并放置到MySwitcher的首位
OnWidgetAddedToList
:
实现基类没有处理的添加到MySwitcher问题,直接将Widget放入,并设置SwitcherIndex为新放入的Widget
UCommonActivatableWidgetQueue
将Widget进入
MySwitcher
的方式设置为队列
void UCommonActivatableWidgetQueue::OnWidgetAddedToList(UCommonActivatableWidget& AddedWidget)
{
if (MySwitcher){
// Insert after the empty slot 0 and before the already queued widgets
MySwitcher->AddSlot(1) [AddedWidget.TakeWidget()];
if (MySwitcher->GetNumWidgets() == 2)
{
// The queue was empty, so we'll show this one immediately
SetSwitcherIndex(1);
}
else if (DisplayedWidget && !DisplayedWidget->IsActivated() && MySwitcher->GetNumWidgets() == 3)
{
//The displayed widget is on its way out and we should not be going to 0 anymore, we should go to the new one
SetSwitcherIndex(1, true); //and get there fast, we need to finish and evict the old widget before anything else happens
}
}
}
将Widget插入到index1(index0一直为空),如果之前队列为空则直接显示,之前队列还存在一个但已经Deactive也可以直接显示
总结:
该容器一方面负责了Widget的创建与缓存,另一方面也使用Switch来对容器中UI的显示进行了控制。位于Switch顶层的UI则显示,保证了容器中UI是互斥的,而Pool保证了UI在被Switch移除且Deactive后不被销毁。
而Widget加入时并没有确定进入Switch的方式,而将其放入了子类中实现。
Stack与Queue分别定义了两种进入Switch的方式
五、Widget池:FUserWidgetPool
一个管理Widget创建与销毁的池,主要用Active和Deactive池来存储不同状态下的Widget
细节以后再看
六、用于管理Widget显示的UI:SCommonAnimatedSwitcher
一个实现了过渡动画的SwitcherUI,不表