前提
Lyra的UI框架十分庞大,各个模块拆分的非常细致,本文将从源码+实机演示入手,层层分析UI框架,从而移植到自己的项目中
Lyra UI所依赖了一些插件,如CommonGame、CommonUser等,首先将对这几个插件进行分析
CommonGame
在Lyra中,UI使用了层级管理
如在 W_OverallUILayout 中,展示了Lyra的几个UI层级
GameLayer_Stack | 游戏HUD的UI层 |
GameMenu_Stack | 游戏菜单UI层 |
Menu_Stack | 菜单层 |
Modal_Stack | 消息层 |
层级之间有先后关系,越下的Stack层级越高
如Modal_Stack层级最高,在Modal_Stack上显示的UI会将其他层的覆盖
如需添加UI,将 Widget 添加到对应的堆栈层(Stack)中
接下来详细分析CommonGame的源码
通过文件类名可以大致推断出CommonGame这个插件的主要功能:UI推送、消息窗口、以及关于Player
ConmmonGame.uplugin
"Plugins": [
{
"Name": "CommonUI",
"Enabled": true
},
{
"Name": "CommonUser",
"Enabled": true
},
{
"Name": "ModularGameplayActors",
"Enabled": true
},
{
"Name": "OnlineFramework",
"Enabled": true
}
]
该插件依赖了CommonUser等插件
Messaging
消息模块
CommonMessagingSubsystem
顾名思义是用于控制Messaging弹窗消息显示的子系统,该子系统继承自ULocalPlayerSubsystem,需要在自己的项目中继承该子系统 如图在Lyra中,UI下有ULyraUIMessaging,重写了父类的Show方法
CommonMessagingSusbsystem.h
UCLASS(config = Game)
class COMMONGAME_API UCommonMessagingSubsystem : public ULocalPlayerSubsystem
{
GENERATED_BODY()
public:
UCommonMessagingSubsystem() { }
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
virtual void ShowConfirmation(UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback = FCommonMessagingResultDelegate());
virtual void ShowError(UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback = FCommonMessagingResultDelegate());
private:
};
CommonMessagingSubsystem 重写了ShouldCreateSubsystem方法,在该方法的实现中看出,当本地玩家的GameInstance不在DS服务器上,且没有创建时,才会创建该子系统
子系统的另外两个虚函数:ShowConfirmation、ShowError,用于显示确认窗口和错误消息窗口
该函数传入两个参数
UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback = FCommonMessagingResultDelegate()
首先是 UCommonGameDialogDescriptor,CommonGame插件专门使用了UCommonGameDialogDescriptor作为消息描述体
功能
ShowConfirmation | 显示确定UI Widget |
ShowError | 显示错误UI Widget |
CommonGameDialog
对话弹窗UI基类,其中Kill和SetupDialog函数需要在子类中重写
Lyra中一个确定对话弹窗UI的展示
UCommonGameDialogDescriptor
CommonGameDialogDescriptor做为消息体的描述符,其中Body和Header为消息的内容和标题,以及一系列的Button处理
CommonGameDialogDescriptor还包含一系列的静态函数,用于创建CommonGameDialogDescriptor并返回指针
UCommonGameInstance
游戏实例的基类,GameInstance功能很简单,主要三个方面
1. 处理 Handle
2. 会话 Session
3. 添加/移除角色
所以Lyra的处理流程大致为
UI -> 获取子系统 -> 通知GameInstance -> 发送消息、回调
消息发送流程
UPrimaryGameLayout
用于UI层级布局,在蓝图中,W_OverallUILayout继承自该类,可自定义不同的UI层级,使用CommonActivatableWidgetStack
并在初始化函数中将组件注册
除此之外,通过 UPrimaryGameLayout 来推送UI (PushWidget)
需要在 UGameUIPolicy 指定该类
功能
在蓝图中使用CommonActivatableWidgetStack来做为UI层 | |
RegisterLayer | 注册UI层 |
PushWidgetToLayer | 将UI推送到指定的Layer |
获取UILayout
如图展示获取UILayout的流程
获取到 UILayout 必须通过 UIPolicy
下文提到获取到 UIPolicy 需要通过 UIManagerSubsystem
UGameUIPolicy
Policy 翻译为政策、原则、方针
那什么是UI的政策呢:负责 UILayout 的添加移除等
功能
CreateLayoutWidget | 创建Layout |
Add/Remove Layout | 添加或移除Layout |
NotifyPlayer Added/Removed/Destroyed | 添加/移除/销毁Layout |
GetRootLayot | 获取指定玩家的Layout |
GameUIPolicy 使用一个 TArray 存放所有玩家的 Layout
Layout 被封装在 FRootViewportLayoutInfo 中
GameUIPolicy.h
USTRUCT()
struct FRootViewportLayoutInfo
{
GENERATED_BODY()
public:
UPROPERTY(Transient)
TObjectPtr<ULocalPlayer> LocalPlayer = nullptr;
UPROPERTY(Transient)
TObjectPtr<UPrimaryGameLayout> RootLayout = nullptr;
UPROPERTY(Transient)
bool bAddedToViewport = false;
FRootViewportLayoutInfo() {}
FRootViewportLayoutInfo(ULocalPlayer* InLocalPlayer, UPrimaryGameLayout* InRootLayout, bool bIsInViewport)
: LocalPlayer(InLocalPlayer)
, RootLayout(InRootLayout)
, bAddedToViewport(bIsInViewport)
{}
bool operator==(const ULocalPlayer* OtherLocalPlayer) const { return LocalPlayer == OtherLocalPlayer; }
};
该结构体重写了 == 方法,用于获取指定的玩家
创建Layout
GameUIPolicy.cpp
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);
}
}
}
在 CreatelayoutWidget 函数中创建了Layout并添加到RootViewportLayouts数组中
销毁Layout
GameUIPolicy.cpp
void UGameUIPolicy::NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer)
{
NotifyPlayerRemoved(LocalPlayer);
LocalPlayer->OnPlayerControllerSet.RemoveAll(this);
const int32 LayoutInfoIdx = RootViewportLayouts.IndexOfByKey(LocalPlayer);
if (LayoutInfoIdx != INDEX_NONE)
{
UPrimaryGameLayout* Layout = RootViewportLayouts[LayoutInfoIdx].RootLayout;
RootViewportLayouts.RemoveAt(LayoutInfoIdx);
RemoveLayoutFromViewport(LocalPlayer, Layout);
OnRootLayoutReleased(LocalPlayer, Layout);
}
}
在 NotifyPlayerDestroyed 函数中从RootViewportLayouts数组移除指定的Layout
创建/销毁流程
对于创建Layout:
1. UCommonGameInstance 调用 AddLocalPlayer()
2. AddLocalPlayer()中获取 UGameUIManagerSubsystem,并调用子系统的 NotifyPlayerAdded()
3. UIManager 子系统获取 CurrentPolicy,并调用 CurrentPolicy 的 NotifyPlayerAdded() 函数
4. NotifyPlayerAdded() 会进行判断,如果该角色的Layout已创建则直接显示,否则调用 CreateLayoutWidget() 函数
5. CreateLayoutWidget() 则会 new 一个 UPrimaryGameLayout 并添加到 RootViewportLayouts 中
6. 添加后调用 AddLayoutToViewport() 函数,将Layout显示在玩家屏幕中,最后执行 OnRootLayoutAddedToViewport()
销毁Layout流程与创建类似
实例化
GameUIPolicy又是在哪实例化的呢?
GameUIManagerSubsystem.cpp
void UGameUIManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (!CurrentPolicy && !DefaultUIPolicyClass.IsNull())
{
TSubclassOf<UGameUIPolicy> PolicyClass = DefaultUIPolicyClass.LoadSynchronous();
SwitchToPolicy(NewObject<UGameUIPolicy>(this, PolicyClass));
}
}
在 GameUIManagerSubsystem 初始化时,创建了 UIPolicy
所以要在 GameUIManagerSubsystem 中指定该 UIPolicy 类
GameUIManagerSubsystem.h
UPROPERTY(config, EditAnywhere)
TSoftClassPtr<UGameUIPolicy> DefaultUIPolicyClass;
需要在 DefaultGame.ini 文件中指定UIPolicy资产
[/Script/LyraGame.LyraUIManagerSubsystem]
DefaultUIPolicyClass=/Game/UI/B_LyraUIPolicy.B_LyraUIPolicy_C
UGameUIManagerSubsystem
用于管理UI的子系统
功能
GetCurrentUIPolicy | 获取当前的UIPolicy |
NotifyPlayerAdded/Removed/Destroyed | 通知添加/移除/销毁UI |
要获取到 UIPolicy,则必须通过 UIMananger 获取
由于该子系统继承自 GameInstance 子系统,则获取该子系统时需要通过 GameInstance 从而 Get 子系统
UCommonLocalPlayer
UCommonLocalPlayer 作为游戏本地玩家的基类,只在父类 ULocalPlayer 类上添加了有关于 UI 的部分功能
CommonLocalPlayer.cpp
UPrimaryGameLayout* UCommonLocalPlayer::GetRootUILayout() const
{
if (UGameUIManagerSubsystem* UIManager = GetGameInstance()->GetSubsystem<UGameUIManagerSubsystem>())
{
if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy())
{
return Policy->GetRootLayout(this);
}
}
return nullptr;
}
与上述过程一样,获取到 UILayout 则需通过游戏实例获取到UI管理子系统,在通过 UIPolicy 获取到 UILayout
GameUser
现在来到 GameUser 插件
UCommonUserSubsystem
关于 CommonUserSubsystem,内容可就多了,先看看关于该子系统的基本描述
Game subsystem that handles queries and changes to user identity and login status.
One subsystem is created for each game instance and can be accessed from blueprints or C++ code.
If a game-specific subclass exists, this base subsystem will not be created.游戏子系统,处理查询和更改用户身份和登录状态。
为每个游戏实例创建一个子系统,可以从蓝图或c++代码访问。
如果存在子类,则不会创建这个基本子系统。
初始化
CommonUserSubsystem.h
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
创建子系统与 CommonGame 中的子系统不同,CommonUserSubsystem 创建时不判断是否在DS服务器上,只要该子系统未创建则为玩家创建该子系统
委托
/** BP delegate called when any requested initialization request completes */
UPROPERTY(BlueprintAssignable, Category = CommonUser)
FCommonUserOnInitializeCompleteMulticast OnUserInitializeComplete;
/** BP delegate called when the system sends an error/warning message */
UPROPERTY(BlueprintAssignable, Category = CommonUser)
FCommonUserHandleSystemMessageDelegate OnHandleSystemMessage;
/** BP delegate called when privilege availability changes for a user */
UPROPERTY(BlueprintAssignable, Category = CommonUser)
FCommonUserAvailabilityChangedDelegate OnUserPrivilegeChanged;
该子系统定义了三个委托
这三个委托均在 CommonGameInstance 初始化函数中被绑定
CommonGameInstance.cpp
void UCommonGameInstance::Init()
{
Super::Init();
// After subsystems are initialized, hook them together
FGameplayTagContainer PlatformTraits = ICommonUIModule::GetSettings().GetPlatformTraits();
UCommonUserSubsystem* UserSubsystem = GetSubsystem<UCommonUserSubsystem>();
if (ensure(UserSubsystem))
{
UserSubsystem->SetTraitTags(PlatformTraits);
UserSubsystem->OnHandleSystemMessage.AddDynamic(this, &UCommonGameInstance::HandleSystemMessage);
UserSubsystem->OnUserPrivilegeChanged.AddDynamic(this, &UCommonGameInstance::HandlePrivilegeChanged);
UserSubsystem->OnUserInitializeComplete.AddDynamic(this, &UCommonGameInstance::HandlerUserInitialized);
}
UCommonSessionSubsystem* SessionSubsystem = GetSubsystem<UCommonSessionSubsystem>();
if (ensure(SessionSubsystem))
{
SessionSubsystem->OnUserRequestedSessionEvent.AddUObject(this, &UCommonGameInstance::OnUserRequestedSession);
}
}
UCommonUserInfo
CommoonUserInfo是单个玩家的逻辑表示,将会存在于所有本地玩家中
CommonUserSubsystem.h
/** Returns the user info for a given local player index in game instance, 0 is always valid in a running game */
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser)
const UCommonUserInfo* GetUserInfoForLocalPlayerIndex(int32 LocalPlayerIndex) const;
/** Deprecated, use PlatformUserId when available */
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser)
const UCommonUserInfo* GetUserInfoForPlatformUserIndex(int32 PlatformUserIndex) const;
/** Returns the primary user info for a given platform user index. Can return null */
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser)
const UCommonUserInfo* GetUserInfoForPlatformUser(FPlatformUserId PlatformUser) const;
/** Returns the user info for a unique net id. Can return null */
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser)
const UCommonUserInfo* GetUserInfoForUniqueNetId(const FUniqueNetIdRepl& NetId) const;
/** Deprecated, use InputDeviceId when available */
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser)
const UCommonUserInfo* GetUserInfoForControllerId(int32 ControllerId) const;
/** Returns the user info for a given input device. Can return null */
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser)
const UCommonUserInfo* GetUserInfoForInputDevice(FInputDeviceId InputDevice) const;
在User子系统中有一系列获取 UCommonUserInfo 的方法
ListenForLoginKeyInput
CommonUserSubsystem.h
/**
* Starts the process of listening for user input for new and existing controllers and logging them.
* This will insert a key input handler on the active GameViewportClient and is turned off by calling again with empty key arrays.
*
* @param AnyUserKeys Listen for these keys for any user, even the default user. Set this for an initial press start screen or empty to disable
* @param NewUserKeys Listen for these keys for a new user without a player controller. Set this for splitscreen/local multiplayer or empty to disable
* @param Params Params passed to TryToInitializeUser after detecting key input
*/
UFUNCTION(BlueprintCallable, Category = CommonUser)
virtual void ListenForLoginKeyInput(TArray<FKey> AnyUserKeys, TArray<FKey> NewUserKeys, FCommonUserInitializeParams Params);
启动监听新控制器和现有控制器的用户输入并记录它们的过程。这将在活动的GameViewportClient上插入一个键输入处理程序,并通过再次调用空键数组来关闭。