虚幻引擎Lyra工程UI框架与设计分析 01

前提

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上插入一个键输入处理程序,并通过再次调用空键数组来关闭。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值