当前位置: 首页 > news >正文

UE4运用C++和框架开发坦克大战教程笔记(十七)(第51~54集)

UE4运用C++和框架开发坦克大战教程笔记(十七)(第51~54集)

  • 51. UI 框架介绍
    • UE4 使用 UI 所面临的问题以及解决思路
    • 关于即将编写的 UI 框架的思维导图
  • 52. 管理类与面板类
  • 53. 预加载与直接加载
  • 54. UI 首次进入界面

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,     // 对应 CanvasPanelOverlay,	// 对应 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 的 paddingUPROPERTY(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 必须是该面板注册到框架的 ObjectNameTMap<FName, UDDPanelWidget*> AllPanelGroup;// 已经显示的 UITMap<FName, UDDPanelWidget*> ShowPanelGroup;// 弹窗栈TMap<FName, UDDPanelWidget*> PopPanelStack;// 已经加载过的 UI 面板的名字TArray<FName> LoadedPanelName;// 遮罩图片UPROPERTY()UImage* MaskPanel;// 透明度值FLinearColor NormalLucency;FLinearColor TranslucenceLucency;FLinearColor ImPenetrableLucency;
};

DDFrameWidget.cpp

// 引入头文件
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "Components/Image.h"
#include "Blueprint/WidgetTree.h"bool UDDFrameWidget::Initialize()
{if (!Super::Initialize()) return false;// 获取根节点RootCanvas = Cast<UCanvasPanel>(GetRootWidget());// 使自身忽略鼠标事件检测,但子控件可以接受检测RootCanvas->SetVisibility(ESlateVisibility::SelfHitTestInvisible);// 生成遮罩MaskPanel = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass());// 设置透明度NormalLucency = FLinearColor(1.f, 1.f, 1.f, 0.f);TranslucenceLucency = FLinearColor(0.f, 0.f, 0.f, 0.6f);ImPenetrableLucency = FLinearColor(0.f, 0.f, 0.f, 0.3f);return true;
}

剩余部分留到下一节课继续编写。

53. 预加载与直接加载

我们先来编写一下 UI 的加载功能。加载 UI 的方式有两种(截取自梁迪老师的文档):

// (1)提前加载到内存
// 该方法提前加载 UI 面板到内存,保存到字典里
void AdvanceLoadPanel(FName PanelName);// (2)显示时如果发现未加载则进行加载
// 该方法为显示 UI 面板,如果该名字对应的面板已经存在于内存中,则不进行加载;
// 如果不存在,先加载,再进行显示
void ShowUIPanel(FName PanelName);

来到 UI 管理类来添加加载 UI 的相关逻辑。

DDFrameWidget.h

public:// 提前加载UFUNCTION()void AdvanceLoadPanel(FName PanelName);// 显示面板  面板 = UI 功能面板UFUNCTION()void ShowUIPanel(FName PanelName);// 提前加载面板回调函数UFUNCTION()void AcceptAdvancePanel(FName BackName, UUserWidget* BackWidget);// 显示时加载回调函数UFUNCTION()void AcceptPanelWidget(FName BackName, UUserWidget* BackWidget);protected:// 执行第一次进入 UIvoid DoEnterUIPanel(FName PanelName);// 执行显示 UIvoid DoShowUIPanel(FName PanelName);// 进入界面,第一次(区分面板类型和布局类型来声明方法)void EnterPanelDoNothing(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget);void EnterPanelDoNothing(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget);void EnterPanelHideOther(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget);void EnterPanelHideOther(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget);void EnterPanelReverse(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget);void EnterPanelReverse(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget);

DDFrameWidget.cpp

void UDDFrameWidget::AdvanceLoadPanel(FName PanelName)
{// 如果全部组已经存在该面板,或者已加载面板名组存在该面板名if (AllPanelGroup.Contains(PanelName) || LoadedPanelName.Contains(PanelName))return;// 进行异步加载BuildSingleClassWealth(EWealthType::Widget, PanelName, "AcceptAdvancePanel");// 添加面板名到已加载面板名组LoadedPanelName.Push(PanelName);
}void UDDFrameWidget::ShowUIPanel(FName PanelName)
{// 判断面板是否已经显示在界面上if (ShowPanelGroup.Contains(PanelName) || PopPanelStack.Contains(PanelName))return;// 判断是否已经加载该面板if (!AllPanelGroup.Contains(PanelName) && !LoadedPanelName.Contains(PanelName)) {BuildSingleClassWealth(EWealthType::Widget, PanelName, "AcceptPanelWidget");LoadedPanelName.Push(PanelName);return;}// 如果存在该 UI 面板if (AllPanelGroup.Contains(PanelName)) {// 判定是否是第一次显示在界面上UDDPanelWidget* PanelWidget = *AllPanelGroup.Find(PanelName);// 如果没有父控件,说明没有进入过界面if (PanelWidget->GetParent()) DoShowUIPanel(PanelName);else DoEnterUIPanel(PanelName);}
}void UDDFrameWidget::AcceptAdvancePanel(FName BackName, UUserWidget* BackWidget)
{UDDPanelWidget* PanelWidget = Cast<UDDPanelWidget>(BackWidget);// 如果加载的界面不是继承自 PanelWidgetif (!PanelWidget) {DDH::Debug() << "Load UI Panel : " << " Is Not DDPanelWidget" <<DDH::Endl();return;}// 注册到框架,不注册类名,BackName 必须是面板名以及 ObjectNamePanelWidget->RegisterToModule(ModuleIndex, BackName);// 添加到全部组AllPanelGroup.Add(BackName, PanelWidget);
}void UDDFrameWidget::AcceptPanelWidget(FName BackName, UUserWidget* BackWidget)
{UDDPanelWidget* PanelWidget = Cast<UDDPanelWidget>(BackWidget);// 如果加载的界面不是继承自 PanelWidgetif (!PanelWidget) {DDH::Debug() << "Load UI Panel : " << " Is Not DDPanelWidget" <<DDH::Endl();return;}// 注册到框架,不注册类名,BackName 必须是面板名以及 ObjectNamePanelWidget->RegisterToModule(ModuleIndex, BackName);// 添加到全部组AllPanelGroup.Add(BackName, PanelWidget);// 进行第一次显示,执行进入界面方法DoEnterUIPanel(BackName);
}void UDDFrameWidget::DoEnterUIPanel(FName PanelName)
{// 获取面板实例UDDPanelWidget* PanelWidget = *AllPanelGroup.Find(PanelName);// 区分布局类型(是 Canvas 还是 Overlay?)以便添加到相应界面if (PanelWidget->UINature.LayoutType == ELayoutType::Canvas) {// 获取布局控件,父控件UCanvasPanel* WorkLayout = NULL;if (RootCanvas->GetChildrenCount() > 0) {// 判断最底层的布局控件是否是 CanvasWorkLayout = Cast<UCanvasPanel>(RootCanvas->GetChildAt(RootCanvas->GetChildrenCount() - 1));if (!WorkLayout) {// 判断是否有可用的 Canvasif (UnActiveCanvas.Num() == 0) {// 没有就创建一个WorkLayout = WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass());WorkLayout->SetVisibility(ESlateVisibility::SelfHitTestInvisible);UCanvasPanelSlot* FrameCanvasSlot = RootCanvas->AddChildToCanvas(WorkLayout);FrameCanvasSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));FrameCanvasSlot->SetOffsets(FMargin(0.f, 0.f, 0.f, 0.f));}elseWorkLayout = UnActiveCanvas.Pop();// 添加到激活组ActiveCanvas.Push(WorkLayout);}}// 如果根节点下没有任何对象else {// 判断是否有可用的 Canvasif (UnActiveCanvas.Num() == 0) {WorkLayout = WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass());WorkLayout->SetVisibility(ESlateVisibility::SelfHitTestInvisible);UCanvasPanelSlot* FrameCanvasSlot = RootCanvas->AddChildToCanvas(WorkLayout);FrameCanvasSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));FrameCanvasSlot->SetOffsets(FMargin(0.f, 0.f, 0.f, 0.f));}elseWorkLayout = UnActiveCanvas.Pop();// 添加到激活画布组ActiveCanvas.Push(WorkLayout);}// 根据面板类型采用不同的首次进入界面方法switch (PanelWidget->UINature.PanelShowType) {case EPanelShowType::DoNothing:EnterPanelDoNothing(WorkLayout, PanelWidget);break;case EPanelShowType::HideOther:EnterPanelHideOther(WorkLayout, PanelWidget);break;case EPanelShowType::Reverse:EnterPanelReverse(WorkLayout, PanelWidget);break;}}// 布局类型为 Overlay 的留到后面再写else {}
}// 下面这些方法留到后面再写
void UDDFrameWidget::DoShowUIPanel(FName PanelName)
{
}void UDDFrameWidget::EnterPanelDoNothing(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget)
{
}void UDDFrameWidget::EnterPanelDoNothing(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget)
{
}void UDDFrameWidget::EnterPanelHideOther(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget)
{
}void UDDFrameWidget::EnterPanelHideOther(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget)
{
}void UDDFrameWidget::EnterPanelReverse(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget)
{
}void UDDFrameWidget::EnterPanelReverse(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget)
{
}

剩余的代码留到下一节课。

54. UI 首次进入界面

上一节课里写的 ShowUIPanel() 需要补充一段逻辑,避免刚调用 AdvanceLoadPanel() 提前加载面板资源就立刻显示面板,这会导致面板没加载出来就被使用。

接下来补充 EnterPanelDoNothing() 的逻辑,即 UI 首次显示需要用到的方法。需要区分 Canvas 和 Overlay 这两种布局类型执行不同的逻辑。

DDFrameWidget.h

protected:// 正在预加载但是收到显示到界面命令时,进行循环检测是否加载完毕,加载完毕则进行显示void WaitShowPanel();protected:// 正在提前加载但是已经收到显示命令的界面名,简称预显示组TArray<FName> WaitShowPanelName;// 保存循环检测加载完毕则显示方法的延时循环任务名字FName WaitShowTaskName;

DDFrameWidget.cpp

#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "DDUI/DDPanelWidget.h"bool UDDFrameWidget::Initialize()
{WaitShowTaskName = FName("WaitShowTask");	return true;
}void UDDFrameWidget::ShowUIPanel(FName PanelName);
{if (ShowPanelGroup.Contains(PanelName) || PopPanelStack.Contains(PanelName))return;if (!AllPanelGroup.Contains(PanelName) && !LoadedPanelName.Contains(PanelName)) {BuildSingleClassWealth(EWealthType::Widget, PanelName, "AcceptPanelWidget");LoadedPanelName.Push(PanelName);return;}// 如果预加载未完成,就调用显示命令,启动循环检测函数,检测到预加载完成的时候,显示 UI 面板if (!AllPanelGroup.Contains(PanelName) && LoadedPanelName.Contains(PanelName) && !WaitShowPanelName.Contains(PanelName)) {// 添加名字到预显示名字组WaitShowPanelName.Push(PanelName);// 启动循环检测加载完毕则显示函数,每 0.3 秒检测一次InvokeRepeat(WaitShowTaskName, 0.3f, 0.3f, this, &UDDFrameWidget::WaitShowPanel);return;}// ... 省略
}void UDDFrameWidget::WaitShowPanel()
{TArray<FName> CompleteName;// for 循环条件表达式缺了 i <(这个错误会在第 60 集改正)for (int i = 0; i < WaitShowPanelName.Num(); ++i) {if (AllPanelGroup.Contains(WaitShowPanelName[i])) {// 执行进入界面方法DoEnterUIPanel(WaitShowPanelName[i]);// 添加到完成组CompleteName.Push(WaitShowPanelName[i]);}}// 移除完成的 UIfor (int i = 0; i < CompleteName.Num(); i++)WaitShowPanelName.Remove(CompleteName[i]);// 如果没有等待显示的 UI 了,停止该循环函数if (WaitShowPanelName.Num() == 0)StopInvoke(WaitShowTaskName);
}void UDDFrameWidget::EnterPanelDoNothing(UCanvasPanel* WorkLayout, UDDPanelWidget* PanelWidget)
{// 添加 UI 面板到父控件UCanvasPanelSlot* PanelSlot = WorkLayout->AddChildToCanvas(PanelWidget);PanelSlot->SetAnchors(PanelWidget->UINature.Anchors);PanelSlot->SetOffsets(PanelWidget->UINature.Offsets);// 把 UI 面板添加到显示组,UI 面板的 GetObjectName(),PanelName,资源系统下的 WealthName 必须一致ShowPanelGroup.Add(PanelWidget->GetObjectName(), PanelWidget);// 调用 UI 面板的进入界面生命周期PanelWidget->PanelEnter();
}void UDDFrameWidget::EnterPanelDoNothing(UOverlay* WorkLayout, UDDPanelWidget* PanelWidget)
{// 添加 UI 面板到 Overlay 布局UOverlaySlot* PanelSlot = WorkLayout->AddChildToOverlay(PanelWidget);PanelSlot->SetPadding(PanelWidget->UINature.Offsets);PanelSlot->SetHorizontalAlignment(PanelWidget->UINature.HAlign);PanelSlot->SetVerticalAlignment(PanelWidget->UINature.VAlign);// 把 UI 面板添加到显示组,UI 面板的 GetObjectName(),PanelName,资源系统下的 WealthName 必须一致ShowPanelGroup.Add(PanelWidget->GetObjectName(), PanelWidget);// 调用 UI 面板的进入界面生命周期PanelWidget->PanelEnter();
}

接下来为了测试 UI 首次进入界面的功能,我们需要创建 DDFrameWidget 和 DDPanelWidget 的具体类,并且创建它们的蓝图界面。

基于 DDFrameWidget 创建一个 C++ 类,目标模组为项目名,命名为 RCGameUIFrame,路径为默认路径 + /UIFrame。如果创建后编译不通过,笔者这里的解决方法是在 .cpp 的引入头文件路径前补全 RaceCarFrame/(即项目名)

再基于 DDPanelWidget 创建六个 C++ 类,目标模组为项目名,路径为默认路径 + /UIFrame。命名分别为 RCStatePanelRCShortCutPanelRCMiniMapPanelRCBigMapPanelRCMenuPanelRCOptionPanel

在 Blueprint 下创建一个名为 UIFrame 的文件夹。在里面创建一个 Widget Blueprint,命名为 GameUIFrame。作为游戏主界面 UI。

打开界面,将其父类指定为 RCGameUIFrame。

为了让主界面可以在运行时直接生成,打开 HUDData,将 Auto Widget Data 的配置修改如下:

在这里插入图片描述
继续在 /Blueprint/UIFrame 下创建两个 Widget Blueprint,分别命名为 StatePanelMiniMapPanel,作为状态栏面板和小地图面板。然后分别将它们的父类指定为 RCStatePanel 和 RCMiniMapPanel。

给 StatePanel 界面调整如下:(控件名字都是随机的,“_53” 只是为了标明对象; Layout Level 的 Level 0 一般是给背景使用的,所以选 Level_1 给普通面板比较合适)

在这里插入图片描述
如果读者对 UMG 不太熟悉的话,可以先去看看 UMG 的相关教程。Offsets 属性控制界面的偏移位置,Anchors 控制界面的锚点。

接下来修改 MiniMapPanel 如下:

在这里插入图片描述
读者可以发现,状态栏面板设定为 Overlay 的布局类型,小地图设定为 Canvas,以便测试两种不同布局类型的 UI 显示在主界面上是否正常。

来到 HUDData,指定状态栏和小地图面板的资源数据。将原本 Class Wealth Data 的内容删除掉,添加内容如下:

在这里插入图片描述
在游戏主界面类里重写初始化函数,添加界面到窗口并显示状态栏和小地图面板。

RCGameUIFrame.h

public:virtual void DDInit() override;

RCGameUIFrame.cpp

void URCGameUIFrame::DDInit()
{AddToViewport();ShowUIPanel("StatePanel");ShowUIPanel("MiniMapPanel");
}

之前在 DDFrameWidget 的 DoEnterUIPanel() 只写了 Canvas 首次显示在界面的逻辑,还没写 Overlay 的,现在给它补上。

DDFrameWidget.cpp

void UDDFrameWidget::DoEnterUIPanel(FName PanelName)
{UDDPanelWidget* PanelWidget = *AllPanelGroup.Find(PanelName);if (PanelWidget->UINature.LayoutType == ELayoutType::Canvas) {UCanvasPanel* WorkLayout = NULL;// 下面这一段作些许调整// 判断最底层的布局控件是否是 Canvasif (RootCanvas->GetChildrenCount() > 0)WorkLayout = Cast<UCanvasPanel>(RootCanvas->GetChildAt(RootCanvas->GetChildrenCount() - 1));// 如果没有任何对象if (!WorkLayout) {if (UnActiveCanvas.Num() == 0) {WorkLayout = WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass());WorkLayout->SetVisibility(ESlateVisibility::SelfHitTestInvisible);UCanvasPanelSlot* FrameCanvasSlot = RootCanvas->AddChildToCanvas(WorkLayout);FrameCanvasSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));FrameCanvasSlot->SetOffsets(FMargin(0.f, 0.f, 0.f, 0.f));}elseWorkLayout = UnActiveCanvas.Pop();ActiveCanvas.Push(WorkLayout);}switch (PanelWidget->UINature.PanelShowType) {case EPanelShowType::DoNothing:EnterPanelDoNothing(WorkLayout, PanelWidget);break;case EPanelShowType::HideOther:EnterPanelHideOther(WorkLayout, PanelWidget);break;case EPanelShowType::Reverse:EnterPanelReverse(WorkLayout, PanelWidget);break;}}// 对于 Overlay 的布局类型else {UOverlay* WorkLayout = NULL;// 如果存在布局控件,试图把最后一个布局控件转换成 Overlayif (RootCanvas->GetChildrenCount() > 0)	WorkLayout = Cast<UOverlay>(RootCanvas->GetChildAt(RootCanvas->GetChildrenCount() - 1));if (!WorkLayout) {if (UnActiveOverlay.Num() == 0) {WorkLayout = WidgetTree->ConstructWidget<UOverlay>(UOverlay::StaticClass());WorkLayout->SetVisibility(ESlateVisibility::SelfHitTestInvisible);UCanvasPanelSlot* FrameCanvasSlot = RootCanvas->AddChildToCanvas(WorkLayout);FrameCanvasSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));FrameCanvasSlot->SetOffsets(FMargin(0.f, 0.f, 0.f, 0.f));}elseWorkLayout = UnActiveOverlay.Pop();ActiveOverlay.Push(WorkLayout);}switch (PanelWidget->UINature.PanelShowType) {case EPanelShowType::DoNothing:EnterPanelDoNothing(WorkLayout, PanelWidget);break;case EPanelShowType::HideOther:EnterPanelHideOther(WorkLayout, PanelWidget);break;case EPanelShowType::Reverse:EnterPanelReverse(WorkLayout, PanelWidget);break;}}	
}

编译后运行游戏,可以看到状态栏和小地图面板都出现在界面上。说明我们的 UI 框架目前已经可以显示出目标面板了。

在这里插入图片描述

相关文章:

UE4运用C++和框架开发坦克大战教程笔记(十七)(第51~54集)

UE4运用C和框架开发坦克大战教程笔记&#xff08;十七&#xff09;&#xff08;第51~54集&#xff09; 51. UI 框架介绍UE4 使用 UI 所面临的问题以及解决思路关于即将编写的 UI 框架的思维导图 52. 管理类与面板类53. 预加载与直接加载54. UI 首次进入界面 51. UI 框架介绍 U…...

GaussDB新体验,新零售选品升级注入新思路【华为云GaussDB:与数据库同行的日子】

选品思维&#xff1a;低频VS高频 一个的商超&#xff0c;假设有50个左右的品类&#xff0c;每个品类下有2到10个不等的商品。然而如此庞大的商品&#xff0c;并非所有都是高频消费品。 结合自身日常的消费习惯&#xff0c;对于高频和低频的区分并不难。一般大型家电、高端礼盒…...

C语言问题汇总

指针 #include <stdio.h>int main(void){int a[4] {1,2,3,4};int *p &a1;int *p1 a1;printf("%#x,%#x",p[-1],*p1);} 以上代码中存在错误。 int *p &a1; 错误1&#xff1a;取a数组的地址&#xff0c;然后1&#xff0c;即指针跳过int [4]大小的字节…...

QT 的 blockSignals(true) 的作用范围

在 Qt 中&#xff0c;blockSignals 是一个用于控件的方法&#xff0c;它用于阻止控件发出的信号。如果你在一个 MainWindow 对象上调用 blockSignals(true)&#xff0c;它会阻止该 MainWindow 对象发出的所有信号。 这意味着&#xff0c;如果 MainWindow 上有任何子控件&#…...

【C++私房菜】类和对象万字详解

目录 一、类与对象 1、类是什么 二、类和对象的基础知识 2.1 定义类&#xff1a;成员变量和成员函数 2.2 创建对象&#xff1a;实例化一个类的对象。 2.3对象的生命周期&#xff1a;构造函数和析构函数。 a. 构造函数 b. 析构函数 c.小结&#xff1a; 三、成员变量和…...

PDF下载添加水印和访问密码

下载接口 ApiOperation(value "下载文件-pdf", notes "下载文件pdf版", httpMethod "GET", response WebResult.class)RequestMapping(value "/downloadPdf", method RequestMethod.GET)public void downloadFilePdf(RequestPar…...

基于SSM+MySQL的的新闻发布系统设计与实现

目录 项目简介 项目技术栈 项目运行环境 项目截图 代码截取 源码获取 项目简介 新闻发布系统是一款基于Servletjspjdbc的网站应用程序&#xff0c;旨在提供一个全面且高效的新闻发布平台。该系统主要包括后台管理和前台新闻展示两个平台&#xff0c;涵盖了新闻稿件的撰写…...

记录首次使用yolov8-obb

1.数据格式 之前使用的数据格式是yolov5_obb的数据格式&#xff0c;然后需要转数据格式&#xff1a; 目前的数据只支持四个坐标点标注的数据&#xff0c;参考&#xff1a;If a corner of the rotate rectangle is out of the image range, How to annotate the image? Issu…...

深度学习环境配置:Anaconda 安装和 pip 源

conda是一种通用包管理系统&#xff0c;与pip的使用类似&#xff0c;环境管理则允许用户方便地安装不同版本的python并可以快速切换。 Anaconda则是一个打包的集合&#xff0c;里面预装好了conda、某个版本的python、众多packages、科学计算工具等等&#xff0c;就是把很多常用…...

100 个 NLP 面试问题

100 个 NLP 面试问题 一、 说明 对于技术磨练中&#xff0c;其中一项很酷的技能培训是提问。不知道答案并没有多大的错;错就错在不谷歌这些疑问。本篇就是在面试之前&#xff0c;您将此文档复制给自己&#xff0c;做一个系统的模拟实战。 二、经典NLP问题&#xff08;共8题&a…...

C# OMRON PLC FINS TCP协议简单测试

FINS(factory interface network service)通信协议是欧姆龙公司开发的用于工业自动化控制网络的指令&#xff0f;响应系统。运用 FINS指令可实现各种网络间的无缝通信&#xff0c;包括用于信息网络的 Etherne(以太网)&#xff0c;用于控制网络的Controller Link和SYSMAC LINK。…...

MQTT在linux下服务端和客户端的应用

MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级、开放标准的消息传输协议&#xff0c;设计用于受限设备和低带宽、不稳定网络的通信。 MQTT的一些关键特点和概念&#xff1a; 发布/订阅模型&#xff1a; MQTT采用发布/订阅&#xff08;Publ…...

韦达定理用处多

文章目录 前言一、一元二次方程中根和系数之间的关系二、韦达定理的数学推导和作用1. 韦达定理的数学推导2. 韦达定理的作用 三、韦达定理的应用举例1. 解题示例12. 解题示例23. 解题示例34. 解题示例45. 解题示例56. 解题示例67. 解题示例7 总结 前言 韦达定理说明了一元n次方…...

Kotlin-类

构造函数 Java final File file new File("file.txt");Kotlin val file File("file.txt")类 Java public final class User { }Kotlin class User公开类 Java public class User { }Kotlin open class User属性类 Java final class User {pri…...

redis基本数据结构介绍

Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的高性能键值对数据库&#xff0c;它支持多种数据结构&#xff0c;包括字符串、哈希、列表、集合、有序集合等。这些数据结构为开发者提供了丰富的数据操作方式&#xff0c;使得Redis在缓存、消息队列、排行榜…...

云数据库RDS云监控

1. 什么是云数据库RDS&#xff1f;它有哪些特点&#xff1f; 云数据库RDS是一种在线关系型数据库服务&#xff0c;它具备的特点包括&#xff1a; 安全可靠&#xff1a;提供了容灾、备份、恢复等高可用性功能&#xff0c;确保数据的安全与可靠。弹性伸缩&#xff1a;用户可以根…...

全自动网页生成系统重构版源码

全自动网页生成系统重构版源码分享&#xff0c;所有模板经过精心审核与修改&#xff0c;完美兼容小屏手机大屏手机&#xff0c;以及各种平板端、电脑端和360浏览器、谷歌浏览器、火狐浏览器等等各大浏览器显示。 为用户使用方便考虑&#xff0c;全自动网页制作系统无需繁琐的注…...

Leetcode—33. 搜索旋转排序数组【中等】

2024每日刷题&#xff08;110&#xff09; Leetcode—33. 搜索旋转排序数组 实现代码 class Solution { public:int search(vector<int>& nums, int target) {int n nums.size();int l 0, r n - 1;while(l < r) {int m l (r - l) / 2;if(nums[m] target) …...

vulhub中Apache APISIX Dashboard API权限绕过导致RCE(CVE-2021-45232)

Apache APISIX是一个动态、实时、高性能API网关&#xff0c;而Apache APISIX Dashboard是一个配套的前端面板。 Apache APISIX Dashboard 2.10.1版本前存在两个API/apisix/admin/migrate/export和/apisix/admin/migrate/import&#xff0c;他们没有经过droplet框架的权限验证&…...

JavaSE习题 使用函数求最大值、求最大值方法的重载和求和方法的重载

目录 1 使用函数求最大值2 求最大值方法的重载3 求和方法的重载 1 使用函数求最大值 使用函数求最大值&#xff1a;创建方法求两个数的最大值max2&#xff0c;随后再写一个求3个数的最大值的函数max3。 ​要求&#xff1a; 在max3这个函数中&#xff0c;调用max2函数&#xff…...

浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)

✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义&#xff08;Task Definition&…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

ubuntu搭建nfs服务centos挂载访问

在Ubuntu上设置NFS服务器 在Ubuntu上&#xff0c;你可以使用apt包管理器来安装NFS服务器。打开终端并运行&#xff1a; sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享&#xff0c;例如/shared&#xff1a; sudo mkdir /shared sud…...

Xshell远程连接Kali(默认 | 私钥)Note版

前言:xshell远程连接&#xff0c;私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...

Cesium1.95中高性能加载1500个点

一、基本方式&#xff1a; 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

2025盘古石杯决赛【手机取证】

前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来&#xff0c;实在找不到&#xff0c;希望有大佬教一下我。 还有就会议时间&#xff0c;我感觉不是图片时间&#xff0c;因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...

全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比

目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec&#xff1f; IPsec VPN 5.1 IPsec传输模式&#xff08;Transport Mode&#xff09; 5.2 IPsec隧道模式&#xff08;Tunne…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...

C++--string的模拟实现

一,引言 string的模拟实现是只对string对象中给的主要功能经行模拟实现&#xff0c;其目的是加强对string的底层了解&#xff0c;以便于在以后的学习或者工作中更加熟练的使用string。本文中的代码仅供参考并不唯一。 二,默认成员函数 string主要有三个成员变量&#xff0c;…...