前置,需要有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
从这里一路加到
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
在这里 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;
}