UE5 Lyra中的UI层级与资产管理

  • Post author:
  • Post category:其他




一、外部接入接口: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;
  1. 容器

    WidgetList



    GeneratedWidgetsPool



    DisplayedWidget



    WidgetList

    :存储激活的Widget


    GeneratedWidgetsPool

    :存储整个Widget池并负责Widget的动态创建与销毁,

    DisplayedWidget



    MySwitcher

    确定当前顶层的Widget
  2. UI布局


    MySwitcher

    :储存并显示位于顶部的SWidget,其在变化时会更新

    DisplayedWidget

    为对应的Widget


    MyOverlay

    :UI的顶层布局


    MyInputGuard

    :布局的一部分,没有细看
  3. 过渡动画参数

    不表



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:

  1. 将当前激活Index之上的Widget都Deactive
  2. 如果需要的话我们也release刚刚显示的Widget?(需要查看别的地方)
  3. 激活现在在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,不表



版权声明:本文为qq_40026009原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。