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

news2024/11/25 6:38:08

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,     // 对应 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;
}

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

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:

	// 执行第一次进入 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)
{
}

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

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]);
		}
	}
	// 移除完成的 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。命名分别为 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;
		// 下面这一段作些许调整
		// 判断最底层的布局控件是否是 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 框架目前已经可以显示出目标面板了。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1433836.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c#string方法对比

字符串的截取匹配操作在开发中非常常见&#xff0c;比如下面这个示例&#xff1a;我要匹配查找出来字符串数组中以“abc”开头的字符串并打印&#xff0c;我下面分别用了两种方式实现&#xff0c;代码如下&#xff1a; using System; namespace ConsoleApp23{ class Progra…

【计算机网络】物理层概述|通信基础|奈氏准则|香农定理|信道复用技术

目录 一、思维导图 二、 物理层概述 1.物理层概述 2.四大特性&#xff08;巧记"械气功程") 三、通信基础 1.数据通信基础 2.趁热打铁☞习题训练 3.信号の变身&#xff1a;编码与调制 4.极限数据传输率 5.趁热打铁☞习题训练 6.信道复用技术 推荐 前些天发…

flutter开发实战-可扩展popup弹窗template模版样式

flutter开发实战-可扩展popup弹窗template模版样式 最近在看到一个flutter_beautiful_popup&#xff0c;可以美化弹窗窗口样式。该插件通过一个template模版的类BeautifulPopupTemplate作为抽象的base类。 一、基类BeautifulPopupTemplate 在BeautifulPopupTemplate中&…

前端面试题——Vue的双向绑定

前言 双向绑定机制是Vue中最重要的机制之一&#xff0c;甚至可以说是Vue框架的根基&#xff0c;它将数据与视图模板相分离&#xff0c;使得数据处理和页面渲染更为高效&#xff0c;同时它也是前端面试题中的常客&#xff0c;接下来让我们来了解什么是双向绑定以及其实现原理。…

可解释性对人工智能发展的影响

文章目录 每日一句正能量前言可解释AI已成热点可解释性人工智能的重要性可解释性人工智能的研究现状推动可解释模型构建未来展望后记 每日一句正能量 不好等待运气降临&#xff0c;就应去发奋掌握知识。 前言 随着人工智能技术的快速发展&#xff0c;越来越多的应用场景需要人…

神经网络激活函数到底是什么?

激活函数 其实不是很难啦&#xff0c;归结一下就是大概这样几个分类&#xff0c;详情请参考【神经网络】大白话直观理解&#xff01;_哔哩哔哩_bilibili神经网络就是干这个事的~ 如果队伍不长&#xff0c;一个ykxb就可以了&#xff0c;如果 如果 队伍太长 就加一个激活函数也…

HBase相关面试准备问题

为什么选择HBase 1、海量存储 Hbase适合存储PB级别的海量数据&#xff0c;在PB级别的数&#xff0c;能在几十到几百毫秒内返回数据。这与Hbase的极易扩展性息息相关。正是因为Hbase良好的扩展性&#xff0c;才为海量数据的存储提供了便利。 2、列式存储 这里的列式存储其实说的…

Verilog实现2进制码与BCD码的互相转换

1、什么是BCD码&#xff1f; BCD码是一种2进制的数字编码形式&#xff0c;用4位2进制数来表示1位10进制中的0~9这10个数。这种编码技术&#xff0c;最常用于会计系统的设计里&#xff0c;因为会计制度经常需要对很长的数字做准确的计算。相对于一般的浮点式记数法&#xff0c;…

微信小程序 --- 腾讯地图线路规划

目录 微信小程序JavaScript 简介 Hello world&#xff01; geocoder(options:Object) 微信小程序插件 简介 路线规划插件 入驻腾讯位置服务平台 申请开发者密钥&#xff08;Key&#xff09;&#xff1a;申请秘钥 Key的作用与注意事项 微信公众平台绑定插件 方式一&a…

新型IT运维管理,基础设施和数据两手都要硬

编前语&#xff1a;数据是AI的基石&#xff0c;缺数据无AI。 AI大模型时代&#xff0c;数据赋予IT人“新使命” 当下IT人在企业中扮演着运营支撑的角色。说到运维管理&#xff0c;相信每人都是一把辛酸泪&#xff0c;每天承担着繁琐、高负荷且又高风险的运维工作&#xff0c;但…

开源软件全景解析:驱动技术创新与行业革新的力量

目录 什么是开源 开源的核心 开源软件的特点 为什么程序员应该拥抱开源 1.学习机会&#xff1a; 2.社区支持&#xff1a; 3.提高职业竞争力&#xff1a; 4.加速开发过程&#xff1a; 5.贡献和回馈&#xff1a; 开源软件的影响力 开源软件多元分析&#xff1a; 开源…

机器学习中常用的性能度量—— ROC 和 AUC

什么是泛化能力&#xff1f; 通常我们用泛化能力来评判一个模型的好坏&#xff0c;通俗的说&#xff0c;泛化能力是指一个机器学期算法对新样本&#xff08;即模型没有见过的样本&#xff09;的举一反三的能力&#xff0c;也就是学以致用的能力。 举个例子&#xff0c;高三的…

为什么说TiDB在线扩容对业务几乎没有影响

作者&#xff1a; 数据源的TiDB学习之路 原文来源&#xff1a; https://tidb.net/blog/e82b2c5f 当前的数据库种类繁多&#xff0c;墨天轮当前统计的所有国产数据库已经有 290个 &#xff0c;其中属于关系型数据库的有 166个 。关系型数据库从部署架构上又可以分为集中式…

AI克隆自己的声音只需5秒,MockingBird实现AI克隆声音!

一、环境搭建 搭建Anaconda运行环境 搭建Anaconda运行环境请跳转链接查看https://blog.csdn.net/m0_50269929/article/details/136036402安装pytorch pip3 install torch torchvision torchaudio安装ffmpeg 打开官网 https://ffmpeg.org/download.html#get-packages 下载地址…

Python实现PDF到HTML的转换

PDF文件是共享和分发文档的常用选择&#xff0c;但提取和再利用PDF文件中的内容可能会非常麻烦。而利用Python将PDF文件转换为HTML是解决此问题的理想方案之一&#xff0c;这样做可以增强文档可访问性&#xff0c;使文档可搜索&#xff0c;同时增强文档在不同场景中的实用性。此…

InnoDB 锁系统(小白入门)

1995年 &#xff0c;MySQL 1.0发布&#xff0c;仅供内部使用&#xff01; 开发多用户、数据库驱动的应用时&#xff0c;最大的一个难点是&#xff1a;一方面要最大程度地利用数据库的并发访问&#xff0c;另一方面还要确保每个用户能以一致性的方式读取和修改数据。 MVCC 并发…

list基本使用

list基本使用 构造迭代器容量访问修改 list容器底层是带头双向链表结构&#xff0c;可以在常数范围内在任意位置进行输入和删除&#xff0c;但不支持任意位置的随机访问&#xff08;如不支持[ ]下标访问&#xff09;&#xff0c;下面介绍list容器的基本使用接口。 template <…

CSS-IN-JS

CSS-IN-JS 为什么会有CSS-IN-JS CSS-IN-JS是web项目中将CSS代码捆绑在JavaScript代码中的解决方案。 这种方案旨在解决CSS的局限性&#xff0c;例如缺乏动态功能&#xff0c;作用域和可移植性。 CSS-IN-JS介绍 1&#xff1a;CSS-IN-JS方案的优点&#xff1a; 让css代码拥…

探索数据可视化:Matplotlib在Python中的高效应用

探索数据可视化&#xff1a;Matplotlib在Python中的高效应用 引言Matplotlib基础安装和配置Matplotlib基础概念绘制简单图表线形图散点图柱状图 图表定制和美化修改颜色、线型和标记添加标题、图例和标签使用样式表和自定义样式 高级图表类型绘制高级图表多图布局和复杂布局交互…

ES6中新增Array.of()函数的用法详解

new Array()方法 ES6为Array增加了of函数用一种明确的含义将一个或多个值转换成数组。因为用new Array()构造数组的时候&#xff0c;是有二意性的。 构造时&#xff0c;传一个参数&#xff0c;实际上是指定数组的长度&#xff0c;表示生成多大的数组。 构造时&#xff0c;传…