UE4运用C++和框架开发坦克大战教程笔记(十七)(第51~54集)
51. UI 框架介绍
UE4 使用 UI 所面临的问题以及解决思路
下面的文字截取自梁迪老师准备的 DataDriven 框架文档,篇幅稍长,即便看了可能也弄不清楚,读者可以在后续编写 UI 框架的过程中再回过头来看。
UE4 中运用 UI 时需要解决的问题以及解决思路:
(1)问题 :
UE4 通过蓝图方式开发 UI 界面时,需要创建非常多的蓝图 Widget,然后手动地逐层添加到主界面,设定其位置变换,容易造成结构混乱和逻辑混乱;在蓝图内创建蓝图 Widget 并且添加进界面的这一过程也会增加耦合,不利于项目维护。
通过 C++ 生成新的 UI 面板并且添加到主界面,需要先在主界面设定好放置该 UI 面板的父控件,由父控件定义 UI 面板的位置变换,获取添加到的父级控件的对象实例进行界面的添加,父级控件的对象实例又有自己的父级对象,层层获取实例需要引入大量头文件和实例,耦合性非常高。
解决思路:
分层叠加 UI 结构,所有独立功能的 UI 面板在界面上显示的位置与形式不由主界面决定,通过在 UI 面板上设置相应的 UI 类型和变换属性,生成时由 UI 管理器通过这些属性进行 UI 的添加。
(2)问题 :
UI 面板的生成、销毁、显示、隐藏、冻结、激活等生命周期功能往往在复杂的 UI 逻辑中会被自己或者其他对象大量调用,各个 UI 面板脚本之间相互引用,容易出现 “紧耦合” 的情况,导致项目的 “可复用性” 降低。
解决思路:
建立统一的 UI 管理器,所有 UI 面板的生成、销毁、显示、隐藏、冻结、激活等接口都只跟 UI 管理器对接,使用 FName 对所有 UI 面板进行标识,由 UI 管理器通过标识对相应 UI 进行生命周期操作,不存在一个 UI 面板直接调用另一个 UI 面板的生命周期功能。
(3)问题:
UI 面板包括很多不同类型,在执行生命周期功能时对其他 UI 面板的影响不同,比如弹窗类型的 UI 面板弹出时需要保持 UI 窗体的 “模态显示(不允许操作父窗体)”,普通开发模式下需要手动维护弹窗类型 UI 面板的层级关系,手动设置遮罩等,十分麻烦。
解决思路:
为 UI 面板设定显示类型,包括无影响(DoNothing),隐藏其他(HideOther),反向反转(Reverse)等几种,不同的类型在 UI 管理器下实现不同的生命周期函数,提供遮罩管理器,定义不同透明度与可否穿透遮罩的类型。
(4)问题 :
UI 面板使用 C++ 创建时需要获取蓝图 Widget 链接并且加载 UClass 再进行创建。以及 UI 开发需要加载数量众多的图片等资源,用普通的方式进行加载十分麻烦。
解决思路:
使用框架的资源加载系统进行 UI 面板的异步生成以及各种 UI 资源的异步加载,并且为 UI 面板资源提供预加载提前加载到内存,随时调用。
(5)问题:
UI 面板与其他 UI 面板或者玩家对象之间有事件交互时,普通的框架一般是通过 UI 管理器进行消息的传递,一般是使用委托或者回调函数等,但是这种方式有其局限性,面对大量不同类型的方法调用时需要定义很多不同类型的委托。
解决思路:
使用框架的反射事件系统以及注册事件系统,爱怎么调就怎么调,随心所欲。
关于即将编写的 UI 框架的思维导图
下图截取自梁迪老师准备的 DataDriven 思维导图:
下图 弹窗遮罩透明度 的 全透明 英文应该是 Penetrate。
52. 管理类与面板类
基于 DDUserWidget 创建两个 C++ 类,类目标模组选择 DataDriven,路径为 /Public/DDUI:
一个命名为 DDFrameWidget,作为主界面和 UI 管理器。
一个命名为 DDPanelWidget,作为面板类。
随后在 DDTypes.h 里添加上一节课的思维导图列出来的枚举,为开发 UI 框架作铺垫。
DDTypes.h
// 引入头文件
#include "Widgets/Layout/Anchors.h"
#pragma region UIFrame
// 布局类型
UENUM()
enum class ELayoutType : uint8 {
Canvas, // 对应 CanvasPanel
Overlay, // 对应 Overlay
};
// UI层级类型, 可以自己动态添加, 一般6层够用了
UENUM()
enum class ELayoutLevel : uint8
{
Level_0 = 0,
Level_1,
Level_2,
Level_3,
Level_All, // 这个层级会隐藏所有ShowGroup的对象
};
// 面板类型
UENUM()
enum class EPanelShowType : uint8 {
DoNothing, // 不影响其他面板
HideOther, // 隐藏其他
Reverse, // 反向切换,弹窗类型
};
// 弹窗遮罩透明度
UENUM()
enum class EPanelLucencyType : uint8 {
// 此处老师将 Lucency 拼写错了
Lucency, // 全透明, 不能穿透
Translucence, // 半透明,不能穿透
ImPenetrable, // 低透明度,不能穿透
Penetrate, // 全透明, 可以穿透(此处老师拼写错了)
};
// 面板属性,在面板类使用
USTRUCT()
struct FUINature
{
GENERATED_BODY()
public:
// 布局类型
UPROPERTY(EditAnywhere)
ELayoutType LayoutType;
// UI 层级,给 HideOther 类型的面板使用,指定影响的范围
UPROPERTY(EditAnywhere)
ELayoutLevel LayoutLevel;
// 面板类型
UPROPERTY(EditAnywhere)
EPanelShowType PanelShowType;
// 弹窗遮罩透明度
UPROPERTY(EditAnywhere)
EPanelLucencyType PanelLucencyType;
// Canvas 锚点
UPROPERTY(EditAnywhere)
FAnchors Anchors;
// Canvas 的 Offset(pos, size) Overlay 的 padding
UPROPERTY(EditAnywhere)
FMargin Offsets;
// Overlay 的水平布局
UPROPERTY(EditAnywhere)
TEnumAsByte<EHorizontalAlignment> HAlign;
// Overlay 的垂直布局
UPROPERTY(EditAnywhere)
TEnumAsByte<EVerticalAlignment> VAlign;
};
#pragma endregion
接下来在面板类加入面板属性结构体 FUINature 的实例,以及面板类的生命周期所用到的一些方法。
DDPanelWidget.h
UCLASS()
class DATADRIVEN_API UDDPanelWidget : public UDDUserWidget
{
GENERATED_BODY()
public:
// UI 面板生命周期
virtual void PanelEnter(); // 第一次进入界面,只会执行一次
virtual void PanelDisplay(); // 第二次以及以后 N 次显示在界面
virtual void PanelHidden(); // 隐藏
virtual void PanelFreeze(); // 冻结
virtual void PanelResume(); // 解冻
virtual void PanelExit(); // 销毁
public:
// 面板属性,初始化工作留到蓝图内手动配置
UPROPERTY(EditAnywhere)
FUINature UINature;
};
DDPanelWidget.cpp
void UDDPanelWidget::PanelEnter()
{
SetVisibility(ESlateVisibility::Visible);
}
void UDDPanelWidget::PanelDisplay()
{
SetVisibility(ESlateVisibility::Visible);
}
void UDDPanelWidget::PanelHidden()
{
SetVisibility(ESlateVisibility::Hidden);
}
// 下面的先不写
void UDDPanelWidget::PanelFreeze()
{
}
void UDDPanelWidget::PanelResume()
{
}
void UDDPanelWidget::PanelExit()
{
}
来到界面管理类写一下初始化相关的代码。
DDFrameWidget.h
// 提前声明
class UCanvasPanel;
class UImage;
class UOverlay;
class UDDPanelWidget;
UCLASS()
class DATADRIVEN_API UDDFrameWidget : public UDDUserWidget
{
GENERATED_BODY()
public:
virtual bool Initialize() override;
protected:
// 根节点(即新建 Widget 蓝图时自带的那个 Canvas Panel)
UCanvasPanel* RootCanvas;
// 此处如果想优化的话可以写成结构体
// 分别保存激活的和未激活的 Overlay 控件
UPROPERTY() // 通过这个宏避免被回收
TArray<UOverlay*> ActiveOverlay;
UPROPERTY()
TArray<UOverlay*> UnActiveOverlay;
// 分别保存激活的和未激活的 Canvas 控件
TArray<UCanvasPanel*> ActiveCanvas;
TArray<UCanvasPanel*> UnActiveCanvas;
// 所有 UI 面板,键 FName 必须是该面板注册到框架的 ObjectName
TMap<FName, UDDPanelWidget*>