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。命名分别为 RCStatePanel、RCShortCutPanel、RCMiniMapPanel、RCBigMapPanel、RCMenuPanel 和 RCOptionPanel。
在 Blueprint 下创建一个名为 UIFrame 的文件夹。在里面创建一个 Widget Blueprint,命名为 GameUIFrame。作为游戏主界面 UI。
打开界面,将其父类指定为 RCGameUIFrame。
为了让主界面可以在运行时直接生成,打开 HUDData,将 Auto Widget Data 的配置修改如下:
继续在 /Blueprint/UIFrame 下创建两个 Widget Blueprint,分别命名为 StatePanel 和 MiniMapPanel,作为状态栏面板和小地图面板。然后分别将它们的父类指定为 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和框架开发坦克大战教程笔记(十七)(第51~54集) 51. UI 框架介绍UE4 使用 UI 所面临的问题以及解决思路关于即将编写的 UI 框架的思维导图 52. 管理类与面板类53. 预加载与直接加载54. UI 首次进入界面 51. UI 框架介绍 U…...

GaussDB新体验,新零售选品升级注入新思路【华为云GaussDB:与数据库同行的日子】
选品思维:低频VS高频 一个的商超,假设有50个左右的品类,每个品类下有2到10个不等的商品。然而如此庞大的商品,并非所有都是高频消费品。 结合自身日常的消费习惯,对于高频和低频的区分并不难。一般大型家电、高端礼盒…...

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:取a数组的地址,然后1,即指针跳过int [4]大小的字节…...
QT 的 blockSignals(true) 的作用范围
在 Qt 中,blockSignals 是一个用于控件的方法,它用于阻止控件发出的信号。如果你在一个 MainWindow 对象上调用 blockSignals(true),它会阻止该 MainWindow 对象发出的所有信号。 这意味着,如果 MainWindow 上有任何子控件&#…...

【C++私房菜】类和对象万字详解
目录 一、类与对象 1、类是什么 二、类和对象的基础知识 2.1 定义类:成员变量和成员函数 2.2 创建对象:实例化一个类的对象。 2.3对象的生命周期:构造函数和析构函数。 a. 构造函数 b. 析构函数 c.小结: 三、成员变量和…...
PDF下载添加水印和访问密码
下载接口 ApiOperation(value "下载文件-pdf", notes "下载文件pdf版", httpMethod "GET", response WebResult.class)RequestMapping(value "/downloadPdf", method RequestMethod.GET)public void downloadFilePdf(RequestPar…...

基于SSM+MySQL的的新闻发布系统设计与实现
目录 项目简介 项目技术栈 项目运行环境 项目截图 代码截取 源码获取 项目简介 新闻发布系统是一款基于Servletjspjdbc的网站应用程序,旨在提供一个全面且高效的新闻发布平台。该系统主要包括后台管理和前台新闻展示两个平台,涵盖了新闻稿件的撰写…...
记录首次使用yolov8-obb
1.数据格式 之前使用的数据格式是yolov5_obb的数据格式,然后需要转数据格式: 目前的数据只支持四个坐标点标注的数据,参考:If a corner of the rotate rectangle is out of the image range, How to annotate the image? Issu…...

深度学习环境配置:Anaconda 安装和 pip 源
conda是一种通用包管理系统,与pip的使用类似,环境管理则允许用户方便地安装不同版本的python并可以快速切换。 Anaconda则是一个打包的集合,里面预装好了conda、某个版本的python、众多packages、科学计算工具等等,就是把很多常用…...
100 个 NLP 面试问题
100 个 NLP 面试问题 一、 说明 对于技术磨练中,其中一项很酷的技能培训是提问。不知道答案并没有多大的错;错就错在不谷歌这些疑问。本篇就是在面试之前,您将此文档复制给自己,做一个系统的模拟实战。 二、经典NLP问题(共8题&a…...

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

MQTT在linux下服务端和客户端的应用
MQTT(Message Queuing Telemetry Transport)是一种轻量级、开放标准的消息传输协议,设计用于受限设备和低带宽、不稳定网络的通信。 MQTT的一些关键特点和概念: 发布/订阅模型: MQTT采用发布/订阅(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(Remote Dictionary Server)是一个开源的高性能键值对数据库,它支持多种数据结构,包括字符串、哈希、列表、集合、有序集合等。这些数据结构为开发者提供了丰富的数据操作方式,使得Redis在缓存、消息队列、排行榜…...
云数据库RDS云监控
1. 什么是云数据库RDS?它有哪些特点? 云数据库RDS是一种在线关系型数据库服务,它具备的特点包括: 安全可靠:提供了容灾、备份、恢复等高可用性功能,确保数据的安全与可靠。弹性伸缩:用户可以根…...

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

Leetcode—33. 搜索旋转排序数组【中等】
2024每日刷题(110) 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网关,而Apache APISIX Dashboard是一个配套的前端面板。 Apache APISIX Dashboard 2.10.1版本前存在两个API/apisix/admin/migrate/export和/apisix/admin/migrate/import,他们没有经过droplet框架的权限验证&…...
JavaSE习题 使用函数求最大值、求最大值方法的重载和求和方法的重载
目录 1 使用函数求最大值2 求最大值方法的重载3 求和方法的重载 1 使用函数求最大值 使用函数求最大值:创建方法求两个数的最大值max2,随后再写一个求3个数的最大值的函数max3。 要求: 在max3这个函数中,调用max2函数ÿ…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...

Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.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(Direct Memory Access)直接存储器存取 DMA可以提供外设…...

2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 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? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...

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