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 类:
- SlAiContainerBaseWidget:作为背包格子的基类。
- SlAiContainerInputWidget:作为背包九宫格合成的输入格子。
- SlAiContainerNormalWidget:作为背包的普通格子。
- SlAiContainerOutputWidget:作为背包内合成出物品的格子。
- 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);
};
记得要将另外三个界面也复刻这个操作。