赞
踩
下面的文字截取自梁迪老师准备的 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 管理器进行消息的传递,一般是使用委托或者回调函数等,但是这种方式有其局限性,面对大量不同类型的方法调用时需要定义很多不同类型的委托。
解决思路:
使用框架的反射事件系统以及注册事件系统,爱怎么调就怎么调,随心所欲。
下图截取自梁迪老师准备的 DataDriven 思维导图:
下图 弹窗遮罩透明度 的 全透明 英文应该是 Penetrate。
基于 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*> AllPanelGroup; // 已经显示的 UI TMap<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; }
剩余部分留到下一节课继续编写。
我们先来编写一下 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: // 执行第一次进入 UI void DoEnterUIPanel(FName PanelName); // 执行显示 UI void 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); // 如果加载的界面不是继承自 PanelWidget if (!PanelWidget) { DDH::Debug() << "Load UI Panel : " << " Is Not DDPanelWidget" <<DDH::Endl(); return; } // 注册到框架,不注册类名,BackName 必须是面板名以及 ObjectName PanelWidget->RegisterToModule(ModuleIndex, BackName); // 添加到全部组 AllPanelGroup.Add(BackName, PanelWidget); } void UDDFrameWidget::AcceptPanelWidget(FName BackName, UUserWidget* BackWidget) { UDDPanelWidget* PanelWidget = Cast<UDDPanelWidget>(BackWidget); // 如果加载的界面不是继承自 PanelWidget if (!PanelWidget) { DDH::Debug() << "Load UI Panel : " << " Is Not DDPanelWidget" <<DDH::Endl(); return; } // 注册到框架,不注册类名,BackName 必须是面板名以及 ObjectName PanelWidget->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) { // 判断最底层的布局控件是否是 Canvas WorkLayout = Cast<UCanvasPanel>(RootCanvas->GetChildAt(RootCanvas->GetChildrenCount() - 1)); if (!WorkLayout) { // 判断是否有可用的 Canvas 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)); } else WorkLayout = UnActiveCanvas.Pop(); // 添加到激活组 ActiveCanvas.Push(WorkLayout); } } // 如果根节点下没有任何对象 else { // 判断是否有可用的 Canvas 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)); } else WorkLayout = 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) { }
剩余的代码留到下一节课。
上一节课里写的 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]); } } // 移除完成的 UI for (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; // 下面这一段作些许调整 // 判断最底层的布局控件是否是 Canvas if (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)); } else WorkLayout = 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; // 如果存在布局控件,试图把最后一个布局控件转换成 Overlay if (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)); } else WorkLayout = 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 框架目前已经可以显示出目标面板了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。