UE4开发C++沙盒游戏教程笔记(十一)(对应教程集数 34 ~ 37)

33. 动态球形检测

为了让 “纸片” 掉落物方便被玩家观察到,就需要给它添加旋转的逻辑(写在 Tick 方法里)。此外还要添加检测的逻辑,在玩家靠近的时候会自动飞向玩家。添加两个 Timer,一个用于定时检测玩家是否靠近,另一个用于自动销毁掉落物,以免玩家不捡的掉落物太多而占用资源。

SlAiFlobObject.h

private:

	// 动态检测事件
	void DetectPlayer();

	// 销毁事件
	void DestroyEvent();

private:

	// 玩家指针
	class ASlAiPlayerCharacter* SPCharacter;
	
	// 动态检测 Timer
	FTimerHandle DetectTimer;

	// 销毁 Timer
	FTimerHandle DestroyTimer;

SlAiFlobObject.cpp

// 引入头文件
#include "TimerManager.h"
#include "SlAiPlayerCharacter.h"

void ASlAiFlobObject::BeginPlay()
{
	Super::BeginPlay();
	
	// 检测世界是否存在
	if (!GetWorld()) return;

	// 注册检测事件
	FTimerDelegate DetectPlayerDele;
	DetectPlayerDele.BindUObject(this, &ASlAiFlobObject::DetectPlayer);
	// 每秒运行一次,循环运行,延迟 3 秒运行
	GetWorld()->GetTimerManager().SetTimer(DetectTimer, DetectPlayerDele, 1.f, true, 3.f);

	// 注册销毁事件
	FTimerDelegate DestroyDele;
	DestroyDele.BindUObject(this, &ASlAiFlobObject::DestroyEvent);
	// 10 秒销毁是为了看效果,实际大概是 60 秒才自动销毁
	GetWorld()->GetTimerManager().SetTimer(DestroyTimer, DestroyDele, 10.f, false);

	// 初始化玩家指针为空
	SPCharacter = NULL;
}

void ASlAiFlobObject::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// 一直旋转
	BaseMesh->AddLocalRotation(FRotator(DeltaTime * 60.f, 0.f, 0.f));

	// 如果检测到玩家
	if (SPCharacter) {
		// 靠近玩家
		SetActorLocation(FMath::VInterpTo(GetActorLocation(), SPCharacter->GetActorLocation() + FVector(0.f, 0.f, 40.f), DeltaTime, 5.f));
		// 如果距离接近 0
		if (FVector::Distance(GetActorLocation(), SPCharacter->GetActorLocation() + FVector(0.f, 0.f, 40.f)) < 10.f) {
			// 判断玩家背包是否有空间(还没写背包,所以用 true 先临时替代)
			if (true) {
				// 添加对应的物品到背包(先空着)

				// 销毁自己
				DestroyEvent();
			}
			else {
				// 如果玩家背包不为空,重置参数
				SPCharacter = NULL;
				// 唤醒检测
				GetWorld()->GetTimerManager().UnPauseTimer(DetectTimer);
				// 唤醒销毁线程
				GetWorld()->GetTimerManager().UnPauseTimer(DestroyTimer);
				// 开启物理模拟
				BoxCollision->SetSimulatePhysics(true);
			}
		}
	}
}

void ASlAiFlobObject::DetectPlayer()
{
	// 检测世界是否存在
	if (!GetWorld()) return;

	// 保存检测结果
	TArray<FOverlapResult> Overlaps;
	FCollisionObjectQueryParams ObjectParams;
	FCollisionQueryParams Params;
	Params.AddIgnoredActor(this);
	//Params.bTraceAsyncScene = true;		// 4.26 已经移除此变量

	// 进行动态检测,检测范围是 200,检测成功的话返回 true
	if (GetWorld()->OverlapMultiByObjectType(Overlaps, GetActorLocation(), FQuat::Identity, ObjectParams, FCollisionShape::MakeSphere(200.f), Params)) {
		for (TArray<FOverlapResult>::TIterator It(Overlaps); It; ++It) {
			// 如果检测到了玩家
			if (Cast<ASlAiPlayerCharacter>(It->GetActor())) {
				// 赋给玩家角色
				SPCharacter = Cast<ASlAiPlayerCharacter>(It->GetActor());
				// 后面再添加背包有否空间的判定逻辑
				if (true) {	
					// 停止检测
					GetWorld()->GetTimerManager().PauseTimer(DetectTimer);
					// 停止销毁定时器
					GetWorld()->GetTimerManager().PauseTimer(DestroyTimer);
					// 关闭物理模拟
					BoxCollision->SetSimulatePhysics(false);
				}
				return;
			}
		}
	}
}

void ASlAiFlobObject::DestroyEvent()
{
	if (!GetWorld()) return;
	// 注销定时器
	GetWorld()->GetTimerManager().ClearTimer(DetectTimer);
	GetWorld()->GetTimerManager().ClearTimer(DestroyTimer);
	// 销毁自己
	GetWorld()->DestroyActor(this);
}

运行后,采集资源后掉落物会掉出来,并且在 3 秒后飞向玩家。(掉落物的飞行速度有些慢了,读者可酌情调整其飞行速度以及角色的接收距离判定)

34. 玩家状态 UI

按照老师的安排,背包先不做。本集内容是制作玩家状态(血量、饥饿值)的 UI。

创建一个 C++ 的 SlateWidget 类,路径为 /Public/UI/Widget,取名为 SlAiPlayerStateWidget,作为玩家的状态栏 UI。

把状态栏 Widget 加入到根 Widget。

SSlAiGameHUDWidget.h

class SLAICOURSE_API SSlAiGameHUDWidget : public SCompoundWidget
{

public:
	
	TSharedPtr<class SSlAiPointerWidget> PointerWidget;
	// 玩家状态栏指针
	TSharedPtr<class SSlAiPlayerStateWidget> PlayerStateWidget;
	
};

SSlAiGameHUDWidget.cpp

// 引入头文件
#include "SSlAiPlayerStateWidget.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::Construct(const FArguments& InArgs)
{


	ChildSlot
	[
		SNew(SDPIScaler)
		.DPIScaler(UIScaler)
		[
			SNew(SOverlay)

			// ... 省略
			
			// 玩家状态
			+SOverlay::Slot()
			.HAlign(HAlign_Left)
			.VAlign(VAlign_Top)
			[
				SAssignNew(PlayerStateWidget, SSlAiPlayerStateWidget)
			]
		]
	];	
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

给状态栏 Widget 准备笔刷。

SlAiGameStyle.h

USTRUCT()
struct SLAICOURSE_API FSlAiGameStyle : public FSlateWidgetStyle
{
	
	UPROPERTY(EditAnywhere, Category = "Info")
	FSlateBrush PointerBrush;

	// 玩家属性背景图
	UPROPERTY(EditAnywhere, Category = "PlayerState")
	FSlateBrush PlayerStateBGBrush;

	// 玩家头像背景图片
	UPROPERTY(EditAnywhere, Category = "PlayerState")
	FSlateBrush PlayerHeadBGBrush;

	// 血条 Brush
	UPROPERTY(EditAnywhere, Category = "PlayerState")
	FSlateBrush HPBrush;

	// 饥饿 Brush
	UPROPERTY(EditAnywhere, Category = "PlayerState")
	FSlateBrush HungerBrush;

	// 玩家头像
	UPROPERTY(EditAnywhere, Category = "PlayerState")
	FSlateBrush PlayerHeadBrush;
}

依旧是先在状态栏 Widget 里获取游玩样式类。玩家状态栏 Widget 有角色头像、生命值条和饥饿度条。除了添加对应的表现控件外,还要声明一个方法用来更新两个条的显示,比如玩家掉血了生命值条就要缩减。

SSlAiPlayerStateWidget.h

class SLAICOURSE_API SSlAiPlayerStateWidget : public SCompoundWidget
{

public:

	// 更新状态事件,绑定的委托是 PlayerState 的 UpdateStateWidget
	void UpdateStateWidget(float HPValue, float HungerValue);

private:
	
	// 获取 GameStyle
	const struct FSlAiGameStyle* GameStyle;

	// 血条
	TSharedPtr<class SProgressBar> HPBar;

	// 饥饿度
	TSharedPtr<SProgressBar> HungerBar;
	
};

SSlAiPlayerStateWidget.cpp

// 引入头文件
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "SBox.h"
#include "SOverlay.h"
#include "SImage.h"
#include "SConstraintCanvas.h"	// 相当于 UMG 的 CanvasPanel
#include "SProgressBar.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiPlayerStateWidget::Construct(const FArguments& InArgs)
{
	// 获取 GameStyle
	GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");

	ChildSlot
	[
		SNew(SBox)
		.WidthOverride(744.f)
		.HeightOverride(244.f)
		[
			SNew(SOverlay)

			// 状态背景图片
			+SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SNew(SImage)
				.Image(&GameStyle->PlayerStateBGBrush)
			]
			
			// 添加进度条的 CanvasPanel
			+SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SNew(SConstraintCanvas)
				
				// 血条
				+SConstraintCanvas::Slot()
				.Anchors(FAnchors(0.f))		// 设置锚点为左上角
				.Offset(FMargin(442.3f, 90.f, 418.f, 42.f))	// 锚点为左上角的时候就相当于设置位置和大小
				[
					SAssignNew(HPBar, SProgressBar)
					.BackgroundImage(&GameStyle->EmptyBrush)
					.FillImage(&GameStyle->HPBrush)
					.FillColorAndOpacity(FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f)))
					.Percent(1.f)
				]
				
				// 饥饿度
				+SConstraintCanvas::Slot()
				.Anchors(FAnchors(0.f))
				.Offset(FMargin(397.5f, 145.f, 317.f, 26.f))
				[
					SAssignNew(HungerBar, SProgressBar)
					.BackgroundImage(&GameStyle->EmptyBrush)
					.FillImage(&GameStyle->HungerBrush)
					.FillColorAndOpacity(FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f)))
					.Percent(1.f)
				]
			]
			
			// 添加人物头像背景和头像的 Overlay
			+SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			.Padding(FMargin(0.f, 0.f, 500.f, 0.f))
			[
				SNew(SOverlay)
				
				+SOverlay::Slot()
				.HAlign(HAlign_Fill)
				.VAlign(VAlign_Fill)
				[
					SNew(SImage)
					.Image(&GameStyle->PlayerHeadBGBrush)
				]
				
				+SOverlay::Slot()
				.HAlign(HAlign_Center)
				.VAlign(VAlign_Center)
				[
					SNew(SImage)
					.Image(&GameStyle->PlayerHeadBrush)
				]
			]
		]
	];	
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

void SSlAiPlayerStateWidget::UpdateStateWidget(float HPValue, float HungerValue)
{
	if (HPValue > 0) HPBar->SetPercent(FMath::Clamp<float>(HPValue, 0.f, 1.f));
	// 这里应该判断是大于等于
	if (HungerValue >= 0) HungerBar->SetPercent(FMath::Clamp<float>(HungerValue, 0.f, 1.f));
}

PlayerState 声明一个用于更新玩家状态栏的委托。除了声明生命值和饥饿度的变量外,还要重写一下 Tick 函数来让饥饿度和生命值在一定条件下持续加减。比如饥饿度会一直随着时间下降。(额,这么一描述感觉应该叫饱食度,不过读者明白就好)

SlAiPlayerState.h

class STextBlock;

// 更新玩家状态 UI 委托
DECLARE_DELEGATE_TwoParams(FUpdateStateWidget, float, float)


public:

	virtual void Tick(float DeltaSeconds) override;

public:

	// 更新玩家状态 UI,绑定的方法是 PlayerStateWidget 的 UpdateStateWidgt
	FUpdateStateWidget UpdateStateWidget;

private:

	// 生命值、饥饿值
	float HP;
	float Hunger;

SlAiPlayerState.cpp

ASlAiPlayerState::ASlAiPlayerState()
{
	// 允许每帧运行
	PrimaryActorTick.bCanEverTick = true;

	CurrentShortcutIndex = 0;
	
	// 设置初始血量为 500
	HP = 500.f;
	// 设置初始饥饿值为 600
	Hunger = 600.f;
}

void ASlAiPlayerState::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	// 如果饥饿值为 0,持续扣血
	if (Hunger <= 0) {
		HP -= DeltaSeconds * 2;
	}
	else {
		// 如果饥饿值不为0,持续减饥饿值,每秒减2
		Hunger -= DeltaSeconds * 2;
		// 持续加血,每秒加 1
		HP += DeltaSeconds;
	}
	// 设定范围
	HP = FMath::Clamp<float>(HP, 0.f, 500.f);
	Hunger = FMath::Clamp<float>(Hunger, 0.f, 600.f);
	// 执行修改玩家状态 UI 的委托(此处第二个参数饥饿度除以 500 是为了在角色饱的时候不让饥饿度立刻下降,会有一段时间保持饥饿度)
	UpdateStateWidget.ExecuteIfBound(HP / 500.f, Hunger / 500.f);
}

依旧是在 HUD 绑定 PlayerState 的委托到 Widget。

SlAiGameHUD.cpp

#include "SSlAiPointerWidget.h"
// 引入头文件
#include "SSlAiPlayerStateWidget.h"

void ASlAiGameHUD::BeginPlay()
{

	GM->SPController->UpdatePointer.BindRaw(GameHUDWidget->PointerWidget.Get(), &SSlAiPointerWidget::UpdatePointer);
	// 绑定更新玩家状态的委托
	GM->SPState->UpdateStateWidget.BindRaw(GameHUDWidget->PlayerStateWidget.Get(), &SSlAiPlayerStateWidget::UpdateStateWidget);
}

Material 目录下新建一个材质 PlayerStateMat,作如下修改:(TargetTex 转化为一个变量)

状态栏主材质

再以这个材质创建材质实例 PlayerHeadMatInst,点开后勾选 TargetTex,图片选 PlayerHead

再以原来的材质创建材质实例 PlayerHeadBGMatInst,点开后勾选 TargetTex,图片选 PlayerHeadBG

设置一下样式类的内容:(Player Head BGBrush 选 PlayerHeadBGMatInst,图片尺寸填 244×244;最底下的头像是 188×188)

样式设置

此时运行后可看到左上角已经显示出人物的状态栏。并且等待足够长的时间后,饥饿条会减少,减少到 0 后则生命值条减少。

状态栏

35. 游戏暂停与输入模式切换

添加几个动作映射的按键绑定:
EscEvent -> Escape(Esc)
PackageEvent -> E
ChatRoomEvent -> T

在 /Public/UI/Widget 目录下创建两个 C++ 的 SlateWidget 类:

一个取名 SlAiGameMenuWidget,作为游玩时的弹出主菜单。
另一个取名 SlAiChatRoomWidget,作为聊天室界面。

在 /Public/UI/Widget/Package 目录下创建 C++ 的 SlateWidget 类,取名 SlAiPackageWidget,作为背包界面。

在数据结构类添加不同类型 UI 的枚举。

SlAiTypes.h

// Game 界面分类
namespace EGameUIType
{
	enum Type
	{
		Game,	// 游戏模式 UI
		Pause,	// 暂停
		Lose,	// 输了,死亡
		Package,	// 背包
		ChatRoom	// 聊天室
	};
}

把这三个 Widget 加入到根 Widget。再添加一个透明黑色遮罩,用于当作打开这三种界面时与游戏界面之间的背景板。

随后添加一个 TMap,用于存放 UI 类型枚举对相应 UI 界面指针的映射。再配套一个注册映射关系的方法 InitUIMap()。

最后添加一个方法 ShowGameUI() 来控制 UI 的显示。

SSlAiGameHUDWidget.h

class SLAICOURSE_API SSlAiGameHUDWidget : public SCompoundWidget
{

public:

	// 显示游戏 UI,被 PlayerController 的 ShowGameUI 委托绑定(实现部分目前先不写)
	void ShowGameUI(EGameUIType::Type PreUI, EGameUIType::Type NextUI);

public:
	
	TSharedPtr<class SSlAiPlayerStateWidget> PlayerStateWidget;

	// 游戏菜单
	TSharedPtr<class SSlAiGameMenuWidget> GameMenuWidget;
	// 聊天室
	TSharedPtr<class SSlAiChatRoomWidget> ChatRoomWidget;
	// 背包
	TSharedPtr<class SSlAiPackageWidget> PackageWidget;
	
private:

	// 将 UI 绑定到 UIMap
	void InitUIMap();

private:

	// 黑色遮罩
	TSharedPtr<class SBorder> BlackShade;
	
	// UIMap
	TMap<EGameUIType::Type, TSharedPtr<SCompoundWidget>> UIMap;
};

SSlAiGameHUDWidget.cpp

// 引入头文件
#include "SSlAiPackageWidget.h"
#include "SSlAiGameMenuWidget.h"
#include "SSlAiChatRoomWidget.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::Construct(const FArguments& InArgs)
{


	ChildSlot
	[
		SNew(SDPIScaler)
		.DPIScaler(UIScaler)
		[
			SNew(SOverlay)

			// ... 省略
			
			// 暗黑色遮罩,放在事件界面和游戏 UI 中间
			+SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SAssignNew(BlackShade, SBorder)
				// 设置为黑色透明
				.ColorAndOpacity(TAttribute<FLinearColor>(FLinearColor(0.2f, 0.2f, 0.2f, 0.5f)))
				// 开始时设置不显示
				.Visibility(EVisibility::Hidden)
				[
					SNew(SImage)
				]
			]
			
			// GameMenu
			+SOverlay::Slot()
			.HAlign(HAlign_Center)
			.VAlign(VAlign_Center)
			[
				SAssignNew(GameMenuWidget, SSlAiGameMenuWidget)
				.Visibility(EVisibility::Hidden)
			]
			
			// ChatRoom
			+SOverlay::Slot()
			.HAlign(HAlign_Left)
			.VAlign(VAlign_Bottom)
			[
				SAssignNew(ChatRoomWidget, SSlAiChatRoomWidget)
				.Visibility(EVisibility::Hidden)
			]
			
			// Package
			+SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SAssignNew(PackageWidget, SSlAiPackageWidget)
				.Visibility(EVisibility::Hidden)
			]
		]
	];	

	// 初始化 UIMap
	InitUIMap();
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION


void SSlAiGameHUDWidget::ShowGameUI(EGameUIType::Type PreUI, EGameUIType::Type NextUI)
{
}

void SSlAiGameHUDWidget::InitUIMap()
{
	UIMap.Add(EGameUIType::Pause, GameMenuWidget);
	UIMap.Add(EGameUIType::Package, PackageWidget);
	UIMap.Add(EGameUIType::ChatRoom, ChatRoomWidget);
	UIMap.Add(EGameUIType::Lose, GameMenuWidget);
}

在游玩控制类添加委托和声明委托变量,跟 SSlAiGameHUDWidget 的 ShowGameUI() 配套使用。

添加一个方法用于切换输入模式和显隐鼠标等(输入模式分为 仅对 UI 生效仅对游戏生效对 UI 和游戏皆生效)。

添加一个保存当前 UI 状态的枚举,然后再声明三个方法,用于根据当前 UI 状态来切换输入模式、更改当前 UI 状态等。目前先补充暂停界面的逻辑

SlAiPlayerController.h


// 显示 UI 委托
DECLARE_DELEGATE_TwoParams(FShowGameUI, EGameUIType::Type, EGameUIType::Type)

{
public:

	// 显示游戏 UI 界面委托,绑定的方法是 GameHUDWidget 的 ShowGameUI
	FShowGameUI ShowGameUI;
	
private:

	// ESC 按下事件
	void EscEvent();
	// E 键背包
	void PackageEvent();
	// T 键聊天室
	void ChatRoomEvent();

	//  转换输入模式,true 为游戏模式,false 为混合模式
	void SwitchInputMode(bool IsGameOnly);
	
private:

	// 保存当前 UI 状态
	EGameUIType::Type CurrentUIType;
}

此外还要初始化当前 UI 状态、绑定呼出 UI 的按键。

SlAiPlayerController.cpp

void ASlAiPlayerController::BeginPlay()
{
	
	IsRightButtonDown = false;

	// 给当前 UI 类型枚举赋值
	CurrentUIType = EGameUIType::Game;
}

void ASlAiPlayerController::SetupInputComponent()
{
	// ... 省略
	InputComponent->BindAction("ScrollDown", IE_Pressed, this, &ASlAiPlayerController::ScrollDownEvent);

	// 绑定 ESC 键事件并且设置当暂停游戏的时候依然可以运行
	InputComponent->BindAction("EscEvent", IE_Pressed, this, &ASlAiPlayerController::EscEvent).bExecuteWhenPaused = true;
	// 绑定背包
	InputComponent->BindAction("PackageEvent", IE_Pressed, this, &ASlAiPlayerController::PackageEvent);
	// 聊天室
	InputComponent->BindAction("ChatRoomEvent", IE_Pressed, this, &ASlAiPlayerController::ChatRoomEvent);
	
}

void ASlAiPlayerController::EscEvent()
{
	switch (CurrentUIType)
	{
	case EGameUIType::Game:
		// 设置游戏暂停
		SetPause(true);
		// 设置输入模式为 GameAndUI
		SwitchInputMode(false);
		// 更新界面
		ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::Pause);
		// 更新当前 UI
		CurrentUIType = EGameUIType::Pause;
		break;
	case EGameUIType::Pause:
	case EGameUIType::Package:
	case EGameUIType::ChatRoom:
		// 接触暂停
		SetPause(false);
		// 设置游戏模式为游戏
		SwitchInputMode(true);
		// 更新界面
		ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::Game);
		// 更新当前 UI
		CurrentUIType = EGameUIType::Game;
		break;	
	}
}

void ASlAiPlayerController::PackageEvent()
{
}

void ASlAiPlayerController::ChatRoomEvent()
{
}

void ASlAiPlayerController::SwitchInputMode(bool IsGameOnly)
{
	if (IsGameOnly)
	{
		// 隐藏鼠标
		bShowMouseCursor = false;
		// 设置输入模式为 OnlyGame
		FInputModeGameOnly InputMode;
		InputMode.SetConsumeCaptureMouseDown(true);
		SetInputMode(InputMode);
	}
	else {
		// 显示鼠标
		bShowMouseCursor = true;
		// 设置输入模式为 GameAndUI
		FInputModeGameAndUI InputMode;
		InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways);
		InputMode.SetHideCursorDuringCapture(false);
		SetInputMode(InputMode);
	}
}

依旧是在 HUD 绑定委托。

SlAiGameHUD.cpp

#include "SSlAiRayInfoWidget.h"
// 引入头文件
#include "SSlAiPointerWidget.h"

void ASlAiGameHUD::BeginPlay()
{

	GM->SPState->UpdateStateWidget.BindRaw(GameHUDWidget->PlayerStateWidget.Get(), &SSlAiPlayerStateWidget::UpdateStateWidget);
	// 绑定显示 UI 委托
	GM->SPController->ShowGameUI.BindRaw(GameHUDWidget.Get(), &SSlAiGameHUDWidget::ShowGameUI);
}

运行游戏后(注意,最好用 Standalone 模式,因为默认情况下其他启动模式按 Esc 的话游戏会直接关闭)按 Esc 可以暂停,但没有暂停菜单界面,下一节课会继续完善。

36. 游戏 UI 切换

实现 UI 切换

继续补充 UI 切换的逻辑:完善上节课声明了的 ShowGameUI 逻辑,其实就是控制黑色遮罩和目标的 UI 的显隐。

SSlAiGameHUDWidget.cpp

void SSlAiGameHUDWidget::ShowGameUI(EGameUIType::Type PreUI, EGameUIType::Type NextUI)
{
	// 如果前一模式是 Game,说明要显示黑板
	if (PreUI == EGameUIType::Game) {
		BlackShade->SetVisibility(EVisibility::Visible);
	}
	else {
		// 隐藏当前正在显示的 UI
		UIMap.Find(PreUI)->Get()->SetVisibility(EVisiblity::Hidden);
	}
	// 如果下一模式是 Game,隐藏黑板
	if (NextUI == EGameUIType::Game) {
		BlackShade->SetVisibility(EVisibility::Hidden);
	}
	else {
		// 显示现在状态对应的 UI
		UIMap.Find(NextUI)->Get()->SetVisibility(EVisibility::Visible);
	}
}

先简单补充下三个 UI 的布局,方便测试和查看表现。

SSlAiChatRoomWidget.cpp

// 引入头文件
#include "SBox.h"
#include "STextBlock.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiChatRoomWidget::Construct(const FArguments& InArgs)
{

	ChildSlot
	[
		SNew(SBox)
		.WidthOverride(300.f)
		.HeightOverride(100.f)
		[
			SNew(STextBlock)
			.Text(FText::FromString("ChatRoom"))
		]
	];	
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

SSlAiPackageWidget.cpp

// 引入头文件
#include "SBox.h"
#include "STextBlock.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiPackageWidget::Construct(const FArguments& InArgs)
{

	ChildSlot
	[
		SNew(SBox)
		.WidthOverride(300.f)
		.HeightOverride(100.f)
		[
			SNew(STextBlock)
			.Text(FText::FromString("Package"))
		]
	];	
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

SSlAiGameMenuWidget.cpp

// 引入头文件
#include "SBox.h"
#include "STextBlock.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameMenuWidget::Construct(const FArguments& InArgs)
{

	ChildSlot
	[
		SNew(SBox)
		.WidthOverride(300.f)
		.HeightOverride(100.f)
		[
			SNew(STextBlock)
			.Text(FText::FromString("GameMenu"))
		]
	];	
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

再到游玩控制类补充下打开背包和聊天室时的输入模式切换等工作。

SlAiPlayerController.cpp

void ASlAiPlayerController::PackageEvent()
{
	switch (CurrentUIType)
	{
	case EGameUIType::Game:
		SwitchInputMode(false);
		ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::Package);
		CurrentUIType = EGameUIType::Package;
		break;
	case EGameUIType::Package:
		SwitchInputMode(true);
		ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::Game);
		CurrentUIType = EGameUIType::Game;
		break;
	}
}

void ASlAiPlayerController::ChatRoomEvent()
{
	switch (CurrentUIType)
	{
	case EGameUIType::Game:
		SwitchInputMode(false);
		ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::ChatRoom);
		CurrentUIType = EGameUIType::ChatRoom;
		break;
	case EGameUIType::ChatRoom:
		SwitchInputMode(true);
		ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::Game);
		CurrentUIType = EGameUIType::Game;
		break;
	}
}

此时暂停界面、背包界面和聊天室界面都可以正常切换了,不过我们在打开除暂停以外的 UI 的时候游戏角色依旧可以进行操作,选中的快捷栏也可以切换,这显然是不太合理的。

限定 UI 打开后无法操控人物

给角色类添加一个 bool 变量,用于在打开界面时限制玩家对角色的操作。

SlAiPlayerCharacter.h

public:

	// 是否锁住输入
	bool IsInputLocked;

SlAiPlayerCharacter.cpp

ASlAiPlayerCharacter::ASlAiPlayerCharacter()
{
	
	// 一开始输入不锁住
	IsInputLocked = false;
}

void ASlAiPlayerCharacter::MoveForward(float Value)
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::MoveRight(float Value)
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::LookUpAtRate(float Value)
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::Turn(float Value)
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::TurnAtRate(float Value)
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::OnStartJump()
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::OnStopJump()
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::OnStartRun()
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

void ASlAiPlayerCharacter::OnStopRun()
{
	// 如果操作被锁住,直接返回
	if(IsInputLocked) return;
	
}

然后在游玩控制类添加一个方法用于更改角色类里面这个 bool 变量,给打开界面的事件使用。同时也可以通过这个角色的 bool 变量限制一些写在控制类的操作。

SlAiPlayerController.h

{
private:

	// 设置锁住输入
	void LockedInput(bool IsLocked);
}

SlAiPlayerController.cpp

void ASlAiPlayerController::EscEvent()
{
	switch (CurrentUIType)
	{
	case EGameUIType::Game:
		
		// 锁定输入
		LockedInput(true);
		break;
	case EGameUIType::Pause:
	case EGameUIType::Package:
	case EGameUIType::ChatRoom:
		
		// 解开输入
		LockedInput(false);
		break;	
	}
}

void ASlAiPlayerController::PackageEvent()
{
	switch (CurrentUIType)
	{
	case EGameUIType::Game:
	
		// 锁定输入
		LockedInput(true);
		break;
	case EGameUIType::Package:
	
		// 解开输入
		LockedInput(false);
		break;
	}
}

void ASlAiPlayerController::ChatRoomEvent()
{
	switch (CurrentUIType)
	{
	case EGameUIType::Game:
	
		// 锁定输入
		LockedInput(true);
		break;
	case EGameUIType::ChatRoom:
		
		// 解开输入
		LockedInput(false);
		break;
	}
}

void ASlAiPlayerController::LockedInput(bool IsLocked)
{
	SPCharacter->IsInputLocked = IsLocked;
}

void ASlAiPlayerController::ChangeView()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;

}

void ASlAiPlayerController::LeftEventStart()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;

}

void ASlAiPlayerController::LeftEventStop()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;
	
}

void ASlAiPlayerController::RightEventStart()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;
	
}

void ASlAiPlayerController::RightEventStop()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;
	
}

void ASlAiPlayerController::ScrollUpEvent()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;
	
}

void ASlAiPlayerController::ScrollDownEvent()
{
	// 如果操作被锁住,直接返回
	if (SPCharacter->IsInputLocked) return;
	
}

此时运行游戏,打开背包或者聊天室界面时角色无法操作,符合预期。

简单创建背包系统需要的类

在 /Public/UI/Widget/Package 的路径下创建以下文件:

创建五个 C++ 的 Slate Widget 类:

  1. SlAiContainerBaseWidget:作为背包格子的基类。
  2. SlAiContainerInputWidget:作为背包九宫格合成的输入格子。
  3. SlAiContainerNormalWidget:作为背包的普通格子。
  4. SlAiContainerOutputWidget:作为背包内合成出物品的格子。
  5. SlAiContainerShortcutWidget:作为在背包界面显示的快捷栏格子。

创建一个 C++ 的普通类于 /Public/Player 目录下,取名为 SlAiPackageManager,作用是管理背包的大大小小的逻辑上的操作。

将刚刚创建的 5 个 Slate Widget 类,除了 SlAiContainerBaseWidget 本身,将其他的 4 个类改成以 SlAiContainerBaseWidget 为父类。因为没有办法直接在引擎内选择根据 Widget 基类创建 Widget 子类,所以要自行手动改代码,下面以 SlAiContainerInputWidget 为例。

SSlAiContainerInputWidget.h

#pragma once

#include "CoreMinimal.h"
#include "SSlAiContainerBaseWidget.h"	// 更改头文件

class SLAICOURSE_API SSlAiContainerInputWidget : public SSlAiContainerBaseWidget // 更改父类
{
public:
	SLATE_BEGIN_ARGS(SSlAiContainerInputWidget)
	{}
	SLATE_END_ARGS()

	void Construct(const FArguments& InArgs);
};

记得要将另外三个界面也复刻这个操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值