Lyra 中的UI

前置,需要有Game Feature 相关知识。

架构梳理

初步分析

在Shoot Core 中,体验配置可以 LAS_ShooterGame_StandardHUD 这个 ActionSet 设置

他的 GameFeatureAction_AddWidget 的配置

USTRUCT()
struct FLyraHUDLayoutRequest
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category=UI, meta=(AssetBundles="Client"))
    TSoftClassPtr<UCommonActivatableWidget> LayoutClass;

    UPROPERTY(EditAnywhere, Category=UI, meta=(Categories="UI.Layer"))
    FGameplayTag LayerID;
};

USTRUCT()
struct FLyraHUDElementEntry
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category=UI, meta=(AssetBundles="Client"))
    TSoftClassPtr<UUserWidget> WidgetClass;

    UPROPERTY(EditAnywhere, Category = UI)
    FGameplayTag SlotID;
};

/**
 * GameFeatureAction responsible for adding widgets.
 */
UCLASS(MinimalAPI, meta = (DisplayName = "Add Widgets"))
class UGameFeatureAction_AddWidgets final : public UGameFeatureAction_WorldActionBase
{
    GENERATED_BODY()
private:
    UPROPERTY(EditAnywhere, Category=UI, meta=(TitleProperty="{LayerID} -> {LayoutClass}"))
    TArray<FLyraHUDLayoutRequest> Layout;

    UPROPERTY(EditAnywhere, Category=UI, meta=(TitleProperty="{SlotID} -> {WidgetClass}"))
    TArray<FLyraHUDElementEntry> Widgets;
};

配置有Layout和Widgets。内容是 class 和 tag。

对于layout配置了这个类和tag

W_ShooterHUDLayout,UI.Layer.Game

这个类父类是 lyra HUD layout。类似挂了很多个标记点。再把对应可能不同的扩展UI推上来的感觉。

B_LyraUIPolicy 是 lyra ui sub system 里面的,在 DefaultGame.ini 配置的

[/Script/LyraGame.LyraUIManagerSubsystem]
DefaultUIPolicyClass=/Game/UI/B_LyraUIPolicy.B_LyraUIPolicy_C

/Game/UI/B_LyraUIPolicy.B_LyraUIPolicy 这个政策配置了

/Game/UI/W_OverallUILayout.W_OverallUILayout 这个父类是 Primary Game Layout,根布局。

根布局里面是空的,只有【GameLayer_Stack, GameMenu_Stack,Menu_Stack, Modal_Stack】四个每一个是

CommonActivatableWidgetStack

在游戏里面对应着层级,比如有些游戏UI层级。例如菜单所处层级就比游戏UI层级高。

这个根布局的蓝图

蓝图上面注释的内容:
这就是我们注册各类层控件的地方,你可以将内容推送至这些控件中。
这些层的具体用途如下:
● Game(游戏层):用于显示 HUD(平视显示器)这类界面元素。
● GameMenu(游戏菜单层):专门承载与游戏玩法相关的菜单,比如游戏内的物品栏界面。
● Menu(通用菜单层):用于设置界面这类功能菜单。
● Modal(模态层):用于显示确认对话框、错误提示框等。
你可以通过 推入 / 弹出 或 推入 / 停用 的方式操作不同的层。若在 GameMenu 层已有内容的情况下,再向该层推送新内容,旧的界面会停止显示,转而展示新内容;直到新内容被停用(即弹出),旧内容才会重新显示。
如果将内容推送至一个完全独立的层(例如 Menu 层),其下方所有层的内容依然会保持可见 —— 因为每个层都是一个独立的控件栈。
层控件可以是任何继承自 UCommonActivatableWidgetContainerBase(通用可激活控件容器基类)的控件。因此,你可以将模态层的控件栈设置为 UCommonActivatableWidgetQueue(通用可激活控件队列);如此一来,每当你向模态层推送新的模态内容时,该层就会以队列模式运行:只有当前处于队首的模态窗口处理完毕后,下一个模态窗口才能被激活。

他说如何推送的呢?

void UGameFeatureAction_AddWidgets::AddWidgets(AActor* Actor, FPerContextData& ActiveData)
{
    ALyraHUD* HUD = CastChecked<ALyraHUD>(Actor);

    if (!HUD->GetOwningPlayerController())
    {
        return;
    }

    if (ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(HUD->GetOwningPlayerController()->Player))
    {
        FPerActorData& ActorData = ActiveData.ActorData.FindOrAdd(HUD);

        for (const FLyraHUDLayoutRequest& Entry : Layout)
        {
            if (TSubclassOf<UCommonActivatableWidget> ConcreteWidgetClass = Entry.LayoutClass.Get())
            {
                ActorData.LayoutsAdded.Add(UCommonUIExtensions::PushContentToLayer_ForPlayer(LocalPlayer, Entry.LayerID, ConcreteWidgetClass));
            }
        }

        UUIExtensionSubsystem* ExtensionSubsystem = HUD->GetWorld()->GetSubsystem<UUIExtensionSubsystem>();
        for (const FLyraHUDElementEntry& Entry : Widgets)
        {
            ActorData.ExtensionHandles.Add(ExtensionSubsystem->RegisterExtensionAsWidgetForContext(Entry.SlotID, LocalPlayer, Entry.WidgetClass.Get(), -1));
        }
    }
}

UCommonUIExtensions::PushContentToLayer_ForPlayer

ExtensionSubsystem->RegisterExtensionAsWidgetForContext

这两个,但是直接这么看要蒙蔽了。(其实这里只是注册了一下,推送有另一个 UAsyncAction_PushContentToLayerForPlayer 异步蓝图节点)

梳理这么多类

我们先看一个继承树

[1] UUserWidget -> UCommonUserWidget -> UCommonActivatableWidget -> ULyraActivatableWidget -> ULyraHUDLayout

[2] UUserWidget -> UCommonUserWidget -> UPrimaryGameLayout

--

UI 设计师 只关心 UUserWidget (在编辑器里摆按键)。

UI 程序员 关心 UCommonActivatableWidget (管理页面的进出栈逻辑)。

Gameplay 程序员 关心 ULyraActivatableWidget (确保打开背包时,角色不会因为乱按键盘而开枪)。

--

UCommonUserWidget 这个比原来的多了一点输入的逻辑

UPrimaryGameLayout 根布局,里面有四个层级,每个层级是一个栈

  • Game (对应类是UCommonActivatableWidgetContainerBase,是UWidget的子类)
  • GameMenu
  • Menu
  • Modal

每个栈里面的元素是 Lyra HUD Layout 继承关系看上面.

可以把各种控件推送到这个layout上面。能被推送的控件是啥呢?配置的所有widget都看一遍

UUserWidget

UUserWidget - UCommonUserWidget - ULyraTaggedWidget

UUserWidget - UCommonUserWidget - ULyraAccoladeHostWidget

UUserWidget - UCommonUserWidget - ULyraWeaponUserInterface

UUserWidget - UCommonUserWidget - ULyraPerfStatContainerBase

UUserWidget - UCommonUserWidget - ULyraSimulatedInputWidget - ULyraJoystickWidget

UUserWidget - UCommonUserWidget - ULyraSimulatedInputWidget - ULyraTouchRegion

得出结论能被推送的配置里面全都是 UUserWidget 的子类

总结一下:为了不蒙蔽我们需要理解这几个类为什么划分。否则就傻了。

[1] UUserWidget -> UCommonUserWidget -> UCommonActivatableWidget -> ULyraActivatableWidget -> ULyraHUDLayout

[2] UUserWidget -> UCommonUserWidget -> UPrimaryGameLayout

UUserWidget 这个很熟悉了。

UCommonUserWidget:
来源: 引擎 CommonUI 插件 (CommonUserWidget.h)
定位: UUserWidget 的增强版,修复/增强了基础输入功能。
它做了什么:

  • 输入路由地基: 它是 CommonUI 输入系统的基本单元。
  • 动作绑定 (Action Binding): 提供了 RegisterUIActionBinding,允许你用代码更方便地绑定手柄/键盘按键(比如“按下 A 键确认”)。
  • 指针输入控制: 提供了 SetConsumePointerInput 来更好地控制鼠标/触摸点击是否穿透。
  • 滚动管理: 管理 Gamepad 模拟摇杆滚动列表的接收者。

UIUCommonActivatableWidget:
来源: CommonUI 插件 (CommonActivatableWidget.h)
定位: 核心概念类。代表一个“屏幕”或“菜单页面”及“生命周期”。
它做了什么:

  • 激活/未激活状态 (Activation): 引入了 Activated 和 Deactivated 概念。
  • 当你 Push 一个界面入栈,它变为 Active(获取焦点,接收输入)。
  • 当你 Push 另一个界面盖在它上面,它变为 Inactive(失去焦点,暂停输入响应,但可视)。
  • 当你 Pop 顶层界面,下面的界面重新变为 Active。
  • 返回键处理: 自动集成了“Back”逻辑(ESC 或 手柄B键),可以配置为自动关闭自己。
  • 焦点恢复: 自动记录上一次选中的按钮,当界面重新激活时,自动把焦点还给那个按钮(非常适合手柄)。

来源: LyraActivatableWidget.h
定位: 项目特定的基类,连接 UI 系统和 Gameplay 系统。
它做了什么:

  • 输入模式配置 (GetDesiredInputConfig):
  • 這是最关键的一点。它覆写了这个函数来告诉 PlayerController:“当我在显示时,输入模式应该是怎样的?”
  • 它定义了 ELyraWidgetInputMode (例如: Game, Menu, GameAndMenu)。
  • 例子: 当你打开“设置菜单”时,这个类会自动告诉游戏层:“现在显示鼠标光标,并且忽略角色的移动输入”。当你关闭它时,自动恢复回去。
  • 鼠标捕获模式: 定义了当此界面激活时,鼠标是否应该锁定在视口内 (CapturePermanently, CaptureDuringMouseDown, etc.)。

ULyraHUDLayout:

它是 Game 层 的主要内容。当游戏开始时,GameplayAbility 或 Experience 会请求 "Push HUD"。

这个类通常是空的或者只是个容器,里面通过 UCommonActivatableWidgetContainer 再去加载具体的血条、弹药栏等子控件。

UI 设计师 只关心 UUserWidget (在编辑器里摆按键)。

UI 程序员 关心 UCommonActivatableWidget (管理页面的进出栈逻辑)。

Gameplay 程序员 关心 ULyraActivatableWidget (确保打开背包时,角色不会因为乱按键盘而开枪)。

回到layout推送

有了初步理解后又回来了

void UGameFeatureAction_AddWidgets::AddWidgets(AActor* Actor, FPerContextData& ActiveData)
{
    // 。。。
    ActorData.LayoutsAdded.Add(UCommonUIExtensions::PushContentToLayer_ForPlayer(LocalPlayer, Entry.LayerID, ConcreteWidgetClass));

}

看这个怎么推layout的,

推上去后保留一个 TWeakObjectPtr 弱引用。

UCommonActivatableWidget* UCommonUIExtensions::PushContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, FGameplayTag LayerName, TSubclassOf<UCommonActivatableWidget> WidgetClass)
{
    if (!ensure(LocalPlayer) || !ensure(WidgetClass != nullptr))
    {
        return nullptr;
    }

    if (UGameUIManagerSubsystem* UIManager = LocalPlayer->GetGameInstance()->GetSubsystem<UGameUIManagerSubsystem>())
    {
        if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy())
        {
            if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked<UCommonLocalPlayer>(LocalPlayer)))
            {
                return RootLayout->PushWidgetToLayerStack(LayerName, WidgetClass);
            }
        }
    }

    return nullptr;
}

拿到根布局 UPrimaryGameLayout* RootLayout 然后 PushWidgetToLayerStack 推上去

UGameUIManagerSubsystem 配置了 UGameUIPolicy 这个 DA,

里面根据local player 找了 root layout

UPrimaryGameLayout* UGameUIPolicy::GetRootLayout(const UCommonLocalPlayer* LocalPlayer) const
{
    const FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer);
    return LayoutInfo ? LayoutInfo->RootLayout : nullptr;
}

有查询就有加入 NotifyPlayerAdded

从 UCommonGameInstance::AddLocalPlayer 调用

GetSubsystem()->NotifyPlayerAdded(Cast(NewPlayer));

从这里一路加到

UGameUIPolicy::NotifyPlayerAdded

UCLASS(MinimalAPI, Abstract, meta = (DisableNativeTick))
class UPrimaryGameLayout : public UCommonUserWidget
{
    GENERATED_BODY()

    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;
    }
};

最后回到这里,继续看 Layer->AddWidget 代码就在引擎目录里面了

这里 UCommonActivatableWidgetContainerBase 就是上面我们看的那四个栈

这里 GetLayerWidget 是从这里找,那就有注册

TMap<FGameplayTag, TObjectPtr> Layers;

在这里 UPrimaryGameLayout::RegisterLayer 就和上面根布局蓝图那一堆register的对上了。

re

上面还用到了 UCommonUIExtensions这个

UCommonUIExtensions 这个是lyra插件 CommonGame里面的,func lib

有一些例如

SuspendInputForPlayer, ResumeInputForPlayer 基于 UCommonInputSubsystem 做的输入控制。

Engine\Plugins\Runtime\CommonUI\Source\CommonUI\Public\Widgets\CommonActivatableWidgetContainer.h

控件推送

void UGameFeatureAction_AddWidgets::AddWidgets(AActor* Actor, FPerContextData& ActiveData)
{
    ALyraHUD* HUD = CastChecked<ALyraHUD>(Actor);

    if (!HUD->GetOwningPlayerController())
    {
        return;
    }

    if (ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(HUD->GetOwningPlayerController()->Player))
    {
        FPerActorData& ActorData = ActiveData.ActorData.FindOrAdd(HUD);

        // layout 推送 ...

        UUIExtensionSubsystem* ExtensionSubsystem = HUD->GetWorld()->GetSubsystem<UUIExtensionSubsystem>();
        for (const FLyraHUDElementEntry& Entry : Widgets)
        {
            ActorData.ExtensionHandles.Add(ExtensionSubsystem->RegisterExtensionAsWidgetForContext(Entry.SlotID, LocalPlayer, Entry.WidgetClass.Get(), -1));
        }
    }
}

RegisterExtensionAsWidgetForContext 然后存一个句柄

ExtensionSubsystem->RegisterExtensionAsWidgetForContext(Entry.SlotID, LocalPlayer, Entry.WidgetClass.Get(), -1)

这里 slotID 是 tag,然后class。

这是lyra插件UIExtension包里面的内容,里面就理工东西

UUIExtensionSubsystem (sub system)

UUIExtensionPointWidget

在 W_ShooterHUDLayout 那个放布局的里面,父类是 Lyra HUD Layout

里面的控件都是 UUIExtensionPointWidget,这是一个槽位,具体的UI内容能直接推过来

UUIExtensionPointWidget

整体看看这个类,看成员变量

/**
 * A slot that defines a location in a layout, where content can be added later
 */
UCLASS(MinimalAPI)
class UUIExtensionPointWidget : public UDynamicEntryBoxBase
{
    GENERATED_BODY()

protected:
    /** 扩展点的tag */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Extension")
    FGameplayTag ExtensionPointTag;

    /** tag怎么匹配,全匹配还是部分匹配就行 */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Extension")
    EUIExtensionPointMatch ExtensionPointTagMatch = EUIExtensionPointMatch::ExactMatch;
    // 允许的控件
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Extension")
    TArray<TObjectPtr<UClass>> DataClasses;

例如这里AllowedDataClasses扩展合规的子类,原来有一个UUserWidget::StaticClass()

调用subsystem RegisterExtensionPoint 注册,给了三个参数,说你注册好了回调这个代理。

void UUIExtensionPointWidget::RegisterExtensionPoint()
{
    if (UUIExtensionSubsystem* ExtensionSubsystem = GetWorld()->GetSubsystem<UUIExtensionSubsystem>())
    {
        TArray<UClass*> AllowedDataClasses;
        AllowedDataClasses.Add(UUserWidget::StaticClass());
        AllowedDataClasses.Append(DataClasses);

        ExtensionPointHandles.Add(ExtensionSubsystem->RegisterExtensionPoint(
            ExtensionPointTag, ExtensionPointTagMatch, AllowedDataClasses,
            FExtendExtensionPointDelegate::CreateUObject(this, &ThisClass::OnAddOrRemoveExtension)
        ));

        ExtensionPointHandles.Add(ExtensionSubsystem->RegisterExtensionPointForContext(
            ExtensionPointTag, GetOwningLocalPlayer(), ExtensionPointTagMatch, AllowedDataClasses,
            FExtendExtensionPointDelegate::CreateUObject(this, &ThisClass::OnAddOrRemoveExtension)
        ));
    }
}

RegisterExtensionPoint 那边只是存一下,这个还不知道什么用。

两个代理,在 UUIExtensionPointWidget::OnAddOrRemoveExtension 回调中调用

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="UI Extension", meta=( IsBindableEvent="True" ))
    FOnGetWidgetClassForData GetWidgetClassForData;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="UI Extension", meta=( IsBindableEvent="True" ))
    FOnConfigureWidgetForData ConfigureWidgetForData;
    TArray<FUIExtensionPointHandle> ExtensionPointHandles;

对 ExtensionSubsystem 注册后的回调句柄存储,用于清理

    UPROPERTY(Transient)
    TMap<FUIExtensionHandle, TObjectPtr<UUserWidget>> ExtensionMapping;

其中

struct FUIExtension : TSharedFromThis<FUIExtension>
{
public:
    /** The extension point this extension is intended for. */
    FGameplayTag ExtensionPointTag;
    int32 Priority = INDEX_NONE;
    TWeakObjectPtr<UObject> ContextObject;
    //Kept alive by UUIExtensionSubsystem::AddReferencedObjects
    TObjectPtr<UObject> Data = nullptr;
};

USTRUCT(BlueprintType)
struct FUIExtensionHandle
{
    GENERATED_BODY()

public:
    // 重载了等于和hash值
private:
    TWeakObjectPtr<UUIExtensionSubsystem> ExtensionSource;
    TSharedPtr<FUIExtension> DataPtr;
    friend UUIExtensionSubsystem;
};

FUIExtensionHandle 去掉构造,重载等于号和哈希,其实是一个FUIExtension的句柄

FUIExtension 是一个带有tag, 优先级,呃呃呃的结构

其实就是action配置里面的tag和 widget class,如果创建成功了,把request里面的一个句柄存一下

具体是什么FUIExtensionRequest一会说

void UUIExtensionPointWidget::OnAddOrRemoveExtension(EUIExtensionAction Action, const FUIExtensionRequest& Request)
{
    if (Action == EUIExtensionAction::Added)
    {
        UObject* Data = Request.Data;

        TSubclassOf<UUserWidget> WidgetClass(Cast<UClass>(Data));
        if (WidgetClass)
        {
            UUserWidget* Widget = CreateEntryInternal(WidgetClass);
            ExtensionMapping.Add(Request.ExtensionHandle, Widget);
        }

    // ...
    }
}

重构组件的时候会到这

TSharedRef<SWidget> UUIExtensionPointWidget::RebuildWidget()
{
    // 运行时
    if (!IsDesignTime() && ExtensionPointTag.IsValid())
    {
        // reset + 注册
        ResetExtensionPoint();
        RegisterExtensionPoint();

        // player state 代理
        FDelegateHandle Handle = GetOwningLocalPlayer<UCommonLocalPlayer>()->CallAndRegister_OnPlayerStateSet(
            UCommonLocalPlayer::FPlayerStateSetDelegate::FDelegate::CreateUObject(this, &UUIExtensionPointWidget::RegisterExtensionPointForPlayerState)
        );
    }

    // 如果是设计时间,应该说的是编辑器查看的状态吧
    if (IsDesignTime())
    {
        auto GetExtensionPointText = [this]()
        {
            // 你编辑器看这个控件,这里就写着这句话
            return FText::Format(LOCTEXT("DesignTime_ExtensionPointLabel", "Extension Point\n{0}"), FText::FromName(ExtensionPointTag.GetTagName()));
        };

        TSharedRef<SOverlay> MessageBox = SNew(SOverlay);

        MessageBox->AddSlot()
            .Padding(5.0f)
            .HAlign(HAlign_Center)
            .VAlign(VAlign_Center)
            [
                SNew(STextBlock)
                .Justification(ETextJustify::Center)
                .Text_Lambda(GetExtensionPointText)
            ];

        // 这里返回了一个 slate 的msgbox
        return MessageBox;
    }
    else
    {
        return Super::RebuildWidget();
    }
}

注册好的回调

从这个地方绑定的回调

        ExtensionPointHandles.Add(ExtensionSubsystem->RegisterExtensionPoint(
            ExtensionPointTag, ExtensionPointTagMatch, AllowedDataClasses,
            FExtendExtensionPointDelegate::CreateUObject(this, &ThisClass::OnAddOrRemoveExtension)
        ));

回来

void UUIExtensionPointWidget::OnAddOrRemoveExtension(EUIExtensionAction Action, const FUIExtensionRequest& Request)
{
    if (Action == EUIExtensionAction::Added)
    {
        // 这个就是game feature action add widget 那里一个tag一个class的那个class
        UObject* Data = Request.Data;

        TSubclassOf<UUserWidget> WidgetClass(Cast<UClass>(Data));
        if (WidgetClass)
        {
            UUserWidget* Widget = CreateEntryInternal(WidgetClass);
            ExtensionMapping.Add(Request.ExtensionHandle, Widget);
        }
        // 如果你配置的东西不是 UUserWidget
        // 你通过预留的代理处理一下转化规则
        else if (DataClasses.Num() > 0)
        {
            if (GetWidgetClassForData.IsBound())
            {
                WidgetClass = GetWidgetClassForData.Execute(Data);

                // If the data is irrelevant they can just return no widget class.
                if (WidgetClass)
                {
                    if (UUserWidget* Widget = CreateEntryInternal(WidgetClass))
                    {
                        ExtensionMapping.Add(Request.ExtensionHandle, Widget);
                        ConfigureWidgetForData.ExecuteIfBound(Widget, Data);
                    }
                }
            }
        }
    }
    else
    {
        // 清空
        if (UUserWidget* Extension = ExtensionMapping.FindRef(Request.ExtensionHandle))
        {
            RemoveEntryInternal(Extension);
            ExtensionMapping.Remove(Request.ExtensionHandle);
        }
    }
}

UUIExtensionSubsystem

void UGameFeatureAction_AddWidgets::AddWidgets(AActor* Actor, FPerContextData& ActiveData)
{
    ALyraHUD* HUD = CastChecked<ALyraHUD>(Actor);

    if (!HUD->GetOwningPlayerController())
    {
        return;
    }

    if (ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(HUD->GetOwningPlayerController()->Player))
    {
        FPerActorData& ActorData = ActiveData.ActorData.FindOrAdd(HUD);

        // ...

        UUIExtensionSubsystem* ExtensionSubsystem = HUD->GetWorld()->GetSubsystem<UUIExtensionSubsystem>();
        for (const FLyraHUDElementEntry& Entry : Widgets)
        {
            ActorData.ExtensionHandles.Add(ExtensionSubsystem->RegisterExtensionAsWidgetForContext(Entry.SlotID, LocalPlayer, Entry.WidgetClass.Get(), -1));
        }
    }
}

前面就看过了,addwidget到ExtensionSubsystem->RegisterExtensionAsWidgetForContext

这里面

FUIExtensionHandle UUIExtensionSubsystem::RegisterExtensionAsData(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, UObject* Data, int32 Priority)
{
    // ...
    FExtensionList& List = ExtensionMap.FindOrAdd(ExtensionPointTag);

    TSharedPtr<FUIExtension>& Entry = List.Add_GetRef(MakeShared<FUIExtension>());
    Entry->ExtensionPointTag = ExtensionPointTag;
    Entry->ContextObject = ContextObject;
    Entry->Data = Data;
    Entry->Priority = Priority;

    // ...
    NotifyExtensionPointsOfExtension(EUIExtensionAction::Added, Entry);
    return FUIExtensionHandle(this, Entry);
}

主要是对 ExtensionMap 添加了一个 tag => FExtensionList 的结构

UCLASS(MinimalAPI)
class UUIExtensionSubsystem : public UWorldSubsystem
{
    GENERATED_BODY()
private:
    typedef TArray<TSharedPtr<FUIExtensionPoint>> FExtensionPointList;
    TMap<FGameplayTag, FExtensionPointList> ExtensionPointMap;

    typedef TArray<TSharedPtr<FUIExtension>> FExtensionList;
    TMap<FGameplayTag, FExtensionList> ExtensionMap;
};

看这个类的熟悉,除此之外是 ExtensionPointMap

他在 ExtensionSubsystem->RegisterExtensionPointForContext 添加,调用的地方就是前面看过的

UUIExtensionPointWidget::RegisterExtensionPoint

FUIExtensionPointHandle UUIExtensionSubsystem::RegisterExtensionPointForContext(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, EUIExtensionPointMatch ExtensionPointTagMatchType, const TArray<UClass*>& AllowedDataClasses, FExtendExtensionPointDelegate ExtensionCallback)
{
    // 合法性判断略...

    FExtensionPointList& List = ExtensionPointMap.FindOrAdd(ExtensionPointTag);

    TSharedPtr<FUIExtensionPoint>& Entry = List.Add_GetRef(MakeShared<FUIExtensionPoint>());
    Entry->ExtensionPointTag = ExtensionPointTag;
    Entry->ContextObject = ContextObject;
    Entry->ExtensionPointTagMatchType = ExtensionPointTagMatchType;
    Entry->AllowedDataClasses = AllowedDataClasses;
    Entry->Callback = MoveTemp(ExtensionCallback);

    NotifyExtensionPointOfExtensions(Entry);
    return FUIExtensionPointHandle(this, Entry);
}

大差不差,存一下map,触发一下事件。

UAsyncAction_PushContentToLayerForPlayer

按钮点击的时候,蓝图里面会调用这个节点,激活的时候

PushWidgetToLayerStackAsync

void UAsyncAction_PushContentToLayerForPlayer::Activate()
{
    if (UPrimaryGameLayout* RootLayout = UPrimaryGameLayout::GetPrimaryGameLayout(OwningPlayerPtr.Get()))
    {
        TWeakObjectPtr<UAsyncAction_PushContentToLayerForPlayer> WeakThis = this;
        StreamingHandle = RootLayout->PushWidgetToLayerStackAsync<UCommonActivatableWidget>(LayerName, bSuspendInputUntilComplete, WidgetClass, [this, WeakThis](EAsyncWidgetLayerState State, UCommonActivatableWidget* Widget) {
            if (WeakThis.IsValid())
            {
                switch (State)
                {
                    case EAsyncWidgetLayerState::Initialize:
                        BeforePush.Broadcast(Widget);
                        break;
                    case EAsyncWidgetLayerState::AfterPush:
                        AfterPush.Broadcast(Widget);
                        SetReadyToDestroy();
                        break;
                    case EAsyncWidgetLayerState::Canceled:
                        SetReadyToDestroy();
                        break;
                }
            }
            SetReadyToDestroy();
        });
    }
    else
    {
        SetReadyToDestroy();
    }
}

这里基于 FStreamableManager 加载了 ActivatableWidgetClass.ToSoftObjectPath()

然后到 PushWidgetToLayerStack 里面

    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;
    }

本质是 Layer->AddWidget

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;
}
上一篇
下一篇