UE4开发C++沙盒游戏教程笔记(二十)(对应教程 61 ~ 63)
60. 游戏暂停菜单
来到游戏暂停菜单界面,获取样式与编写结构布局。需要说的是最初布局只有一个被 SBox 包裹的垂直框,详细布局会写在一个初始化方法里面动态添加进去,跟菜单栏界面的编写方式差不多。
暂停菜单界面初始只有 3 个按钮:设置界面跳转、保存游戏和退出游戏,只有第一个是会改变界面布局的,所以我们声明两个 Widget 指针数组来存放初始界面和设置界面的 Widget。设置界面直接套用菜单栏的那个设置界面,不过更改音量和更改语言的方法还是要写在这里。
并且要给所有的按钮都准备相应的、要绑定的方法。
最后我们让这个界面承担角色挂了以后显示的失败界面功能,而切换这些界面的时候需要一个方法来重置整个暂停菜单界面。
SSlAiGameMenuWidget.h
// 存档委托
DECLARE_DELEGATE(FSaveGameDelegate)
class SLAICOURSE_API SSlAiGameMenuWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiGameMenuWidget)
{}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
// 失败方法
void GameLose();
// 重置菜单
void ResetMenu();
public:
// 存档委托,绑定 GameMode 的 SaveGame 函数
FSaveGameDelegate SaveGameDele;
private:
void InitializeWidget();
FReply OptionEvent();
FReply SaveGameEvent();
FReply QuitGameEvent();
FReply GoBackEvent();
// 改变语言(老师的形参拼写错了)
void ChangeCulture(ECultureTeam Culture);
// 改变音量
void ChangeVolume(const float MusicVolume, const float SoundVolume);
private:
TSharedPtr<class SBox> RootBox;
TSharedPtr<class SVerticalBox> VertBox;
TSharedPtr<SButton> SaveGameButton;
TSharedPtr<class STextBlock> SaveGameText;
TSharedPtr<class SButton> QuitGameButton;
TArray<TSharedPtr<SCompoundWidget>> MenuItemList;
TArray<TSharedPtr<SCompoundWidget>> OptionItemList;
// 获取 GameStyle
const struct FSlAiGameStyle* GameStyle;
}
SSlAiGameMenuWidget.cpp
// 添加新头文件
#include "SBox.h"
#include "SSlAiGameOptionWidget.h"
#include "SBoxPanel.h"
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "STextBlock.h"
#include "SlAiDataHandle.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/World.h"
#include "SlAiPlayerController.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameMenuWidget::Construct(const FArguments& InArgs)
{
// 获取 GameStyle
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
ChildSlot
[
SAssignNew(RootBox, SBox)
.WidthOverride(600.f)
.HeightOverride(400.f)
.Padding(FMargin(50.f))
[
SAssignNew(VertBox, SVerticalBox)
]
];
InitializeWidget();
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameMenuWidget::InitializeWidget()
{
// 初始化正常界面
MenuItemList.Add(
SNew(SButton)
.OnClicked(this, &SSlAiGameMenuWidget::OptionEvent)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SlAiGame", "GameOption", "GameOption"))
.Font(GameStyle->Font_30)
]
);
MenuItemList.Add(
SAssignNew(SaveGameButton, SButton)
.OnClicked(this, &SSlAiGameMenuWidget::SaveGameEvent)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
// 这里做一个引用
SAssignNew(SaveGameText, STextBlock)
.Text(NSLOCTEXT("SlAiGame", "SaveGame", "SaveGame"))
.Font(GameStyle->Font_30)
]
);
MenuItemList.Add(
SNew(SButton)
.OnClicked(this, &SSlAiGameMenuWidget::QuitGameEvent)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SlAiGame", "QuitGame", "QuitGame"))
.Font(GameStyle->Font_30)
]
);
// 初始化游戏设置数组
OptionItemList.Add(
SNew(SSlAiGameOptionWidget)
.ChangeCulture(this, &SSlAiGameMenuWidget::ChangeCulture)
.ChangeVolume(this, &SSlAiGameMenuWidget::ChangeVolume)
);
OptionItemList.Add(
SNew(SButton)
.OnClicked(this, &SSlAiGameMenuWidget::GoBackEvent)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SlAiGame", "GoBack", "GoBack"))
.Font(GameStyle->Font_30)
]
);
// 初始化一个退出游戏按钮
SAssignNew(QuitGameButton, SButton)
.OnClicked(this, &SSlAiGameMenuWidget::QuitGameEvent)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SlAiGame", "QuitGame", "QuitGame"))
.Font(GameStyle->Font_30)
];
// 将菜单按钮填充
for (TArray<TSharedPtr<SCompoundWidget>>::TIterator It(MenuItemList); It; ++It) {
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(1.f)
[
(*It)->AsShared()
];
}
}
FReply SSlAiGameMenuWidget::OptionEvent()
{
// 清空
VertBox->ClearChildren();
// 将设置控件填充
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(3.2f)
[
OptionItemList[0]->AsShared()
];
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(1.f)
[
OptionItemList[1]->AsShared()
];
// 设置高度
RootBox->SetHeightOverride(520.f);
return FReply::Handled();
}
FReply SSlAiGameMenuWidget::SaveGameEvent()
{
// 保存存档
SaveGameDele.ExecuteIfBound();
// 设置 SaveGame 按钮不可用
SaveGameButton->SetVisibility(EVisibility::HitTestInvisible);
// 修改存档文字
SaveGameText->SetText(NSLOCTEXT("SlAiGame", "SaveCompleted", "SaveCompleted"));
// 返回
return FReply::Handled();
}
FReply SSlAiGameMenuWidget::QuitGameEvent()
{
Cast<ASlAiPlayerController>(UGameplayStatics::GetPlayerController(GWorld, 0))->ConsoleCommand("quit");
return FReply::Handled();
}
FReply SSlAiGameMenuWidget::GoBackEvent()
{
// 清空
VertBox->ClearChildren();
// 将菜单按钮填充
for (TArray<TSharedPtr<SCompoundWidget>>::TIterator It(MenuItemList); It; ++It) {
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(1.f)
[
(*It)->AsShared()
];
}
// 设置高度
RootBox->SetHeightOverride(400.f);
return FReply::Handled();
}
void SSlAiGameMenuWidget::ChangeCulture(ECultureTeam Culture)
{
SlAiDataHandle::Get()->ChangeLocalizationCulture(Culture);
}
void SSlAiGameMenuWidget::ChangeVolume(const float MusicVolume, const float SoundVolume)
{
}
void SSlAiGameMenuWidget::GameLose()
{
// 清空
VertBox->ClearChildren();
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(1.f)
[
QuitGameButton->AsShared()
];
// 设置高度
RootBox->SetHeightOverride(200.f);
}
void SSlAiGameMenuWidget::ResetMenu()
{
// 清空
VertBox->ClearChildren();
// 将菜单按钮填充
for (TArray<TSharedPtr<SCompoundWidget>>::TIterator It(MenuItemList); It; ++It) {
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(1.f)
[
(*It)->AsShared()
];
}
// 设置高度
RootBox->SetHeightOverride(400.f);
// 重置存档按钮
SaveGameButton->SetVisibility(EVisibility::Visible);
// 修改存档文字
SaveGameText->SetText(NSLOCTEXT("SlAiGame", "SaveGame", "SaveGame"));
}
来到游戏主界面添加暂停时和游戏失败时分别要执行的方法,显示相应的界面布局。
SSlAiGameHUDWidget.cpp
void SSlAiGameHUDWidget::ShowGameUI(EGameUIType::Type PreUI, EGameUIType::Type NextUI)
{
else {
UIMap.Find(NextUI)->Get()->SetVisibility(EVisibility::Visible);
if (NextUI == EGameUIType::ChatRoom) ChatRoomWidget->ScrollToEnd();
// 如果是失败,只显示一个按钮
if (NextUI == EGameUIType::Lose) GameMenuWidget->GameLose();
// 如果是菜单,设置菜单初始化
if (NextUI == EGameUIType::Pause) GameMenuWidget->ResetMenu();
}
}
编译后以 Standalone Game 模式运行游戏,按 Esc 键打开菜单栏,可以看到菜单栏除音量调节以外其他按钮都能正常响应,存档按钮只能在本次打开菜单栏时响应一次,但是目前还没有写存档功能。
61. SoundMix 音量与玩家死亡
添加音效以及菜单更改音量功能
在 Blueprint 目录下创建名为 Sound 的文件夹,在该文件夹内创建以下文件:
两个 SoundClass,分别命名为 SlAiMusicClass 和 SlAiSoundClass,分别作为音乐组和音效组。
一个 SoundMix,命名为 SlAiSoundMix,作为管理两个声音组的混合器。
到 Content/Res/Sound/GameSound 目录下将 GameBG 的 SoundClass 选为 SlAiMusicClass,同目录下的其他音效文件的 SoundClass 都选为 SlAiSoundClass。
在 SlAiSoundMix 中添加新建的两个 SoundClass 进去,第一个是 SlAiMusicClass,第二个是 SlAiSoundClass。
来到第三人称动画蓝图,在右下方动画资源列表选到 Player_Eat 动画,在时间轴刚开始的位置添加 PlaySound 的 Notify,然后选中它,在面板里选择 Eat 作为播放的声音,此时播放动画能听见音效。然后对 Player_Fight 和 Player_Death 也做同样的操作,给它们添加声音。
去到第一人称动画蓝图也给吃和挥剑动画做以上操作,声音播放位置自己看着穿插就好。
然后去敌人的动画蓝图,给敌人的攻击动画添加 Fight 的 Notify。第四个攻击动画,最后敌人砸地是用 Crush 的音效(其实个人感觉 Hammer 也合适)。最后给敌人的两种死亡动画添加 Dead 音效,给受伤动画添加 Hurt 音效。
来到手持物品基类,添加默认击中声音和播放逻辑。
SlAiHandObject.h
protected:
// 打到物品时的音效
class USoundWave* OverlaySound;
SlAiHandObject.cpp
// 引入头文件
#include "Kismet/GameplayStatics.h"
ASlAiHandObject::ASlAiHandObject()
{
// 默认拳头音效
static ConstructorHelpers::FObjectFinder<USoundWave> StaticSound(TEXT("SoundWave'/Game/Res/Sound/GameSound/Punch.Punch'"));
OverlaySound = StaticSound.Object;
}
void ASlAiHandObject::OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// 如果音效存在,播放音效,默认音效为拳打
if (OverlaySound) UGameplayStatics::PlaySoundAtLocation(GetWorld(), OverlaySound, OtherActor->GetActorLocation());
}
在斧子类和锤子类,重新给击中声音赋值砍树和锤子凿的声音。
SlAiHandAxe.cpp
void ASlAiHandAxe::BeginPlay()
{
Super::BeginPlay();
ObjectIndex = 5;
// 重新加载砍树的音效
OverlaySound = LoadObject<USoundWave>(NULL, TEXT("SoundWave'/Game/Res/Sound/GameSound/Axe.Axe'"));
}
SlAiHandHammer.cpp
void ASlAiHandAxe::BeginPlay()
{
Super::BeginPlay();
ObjectIndex = 6;
// 重新加载砍树的音效
OverlaySound = LoadObject<USoundWave>(NULL, TEXT("SoundWave'/Game/Res/Sound/GameSound/Hammer.Hammer'"));
}
准备工作都做好了,来到数据处理类声明 4 个要用到的声音相关的指针变量,再编写一个声音的初始化方法。
添加一个修改游戏音量的方法,跟菜单的修改音量方法类似,不过这里用到了 AudioDevice。
SlAiDataHandle.h
public:
void ResetMenuVolume(float MusicVol, float SoundVol);
// 修改游戏音量
void ResetGameVolume(float MusicVol, float SoundVol);
private:
// 初始化 Game 声音数据
void InitializeGameAudio();
private:
// 音乐组件
class USoundMix* SlAiSoundMix;
class USoundClass* SlAiMusicClass;
USoundClass* SlAiSoundClass;
class FAudioDevice* AudioDevice;
SlAiDataHandle.cpp
// 引入头文件
#include "AudioDevice.h"
#include "Sound/SoundMix.h"
#include "Sound/SoundClass.h"
void SlAiDataHandle::ResetGameVolume(float MusicVol, float SoundVol)
{
if (MusicVol > 0) {
MusicVolume = MusicVol;
// 使用混音器来设置
AudioDevice->SetSoundMixClassOverride(SlAiSoundMix, SlAiMusicClass, MusicVolume, 1.f, 0.2f, false);
}
if (SoundVol > 0) {
SoundVolume = SoundVol;
// 使用混音器来设置
AudioDevice->SetSoundMixClassOverride(SlAiSoundMix, SlAiSoundClass, SoundVolume, 1.f, 0.2f, false);
}
// 更新存档数据
SlAiSingleton<SlAiJsonHandle>::Get()->UpdateRecordData(GetEnumValueAsString<ECultureTeam>(FString("ECultureTeam"), CurrentCulture), MusicVolume, SoundVolume, &RecordDataList);
}
void SlAiDataHandle::InitializeGameData()
{
InitObjectAttr();
InitResourceAttrMap();
InitCompoundTableMap();
// 初始化游戏声音数据
InitializeGameAudio();
}
void SlAiDataHandle::InitializeGameAudio()
{
// 获取混音器和声音类
SlAiSoundMix = LoadObject<USoundMix>(NULL, TEXT("SoundMix'/Game/Blueprint/Sound/SlAiSoundMix.SlAiSoundMix'"));
SlAiMusicClass = LoadObject<USoundClass>(NULL, TEXT("SoundClass'/Game/Blueprint/Sound/SlAiMusicClass.SlAiMusicClass'"));
SlAiSoundClass = LoadObject<USoundClass>(NULL, TEXT("SoundClass'/Game/Blueprint/Sound/SlAiSoundClass.SlAiSoundClass'"));
// 获取音乐设备(4.26 版本需要在后面再调用个 Get 方法,这里就先补充上去)
AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice();
// 推送混音器到设备
AudioDevice->PushSoundMixModifier(SlAiSoundMix);
// 根据音量设置一次声音
ResetGameVolume(MusicVolume, SoundVolume);
}
来到暂停菜单界面补充音量调整方法的逻辑。
SSlAiGameMenuWidget.cpp
void SSlAiGameMenuWidget::ChangeVolume(const float MusicVolume, const float SoundVolume)
{
SlAiDataHandle::Get()->ResetGameVolume(MusicVolume, SoundVolume);
}
在 GameMode 添加背景音乐在初始播放的逻辑。
SlAiGameMode.cpp
// 引入头文件
#include "Sound/SoundWave.h"
void ASlAiGameMode::BeginPlay()
{
// 播放背景音乐
USoundWave* BGMusic = LoadObject<USoundWave>(NULL, TEXT("SoundWave'/Game/Res/Sound/GameSound/GameBG.GameBG'"));
BGMusic->bLooping = true;
UGameplayStatics::PlaySound2D(GetWorld(), BGMusic, 0.1f);
}
编译后运行游戏,用工具采矿伐木,用拳头或剑攻击敌人都会有正确的音效了。并且游戏暂停菜单的音量调节功能也能生效,游戏开始时会自动播放背景音乐。
添加玩家角色死亡逻辑
在玩家角色类添加一个播放死亡动画的方法,并返回动画时长。
SlAiPlayerCharacter.h
public:
// 播放死亡动画,返回播放时长
float PlayDeadAnim();
private:
// 死亡动画资源
class UAnimationAsset* AnimDead;
SlAiPlayerCharacter.cpp
ASlAiPlayerCharacter::ASlAiPlayerCharacter()
{
HandObject = CreateDefaultSubobject<UChildActorComponent>(TEXT("HandObject"));
// 加载死亡动画资源
AnimDead = Cast<UAnimationAsset>(StaticLoadObject(UAnimationAsset::StaticClass(), NULL, *FString("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Player/Animation/Player_Death.Player_Death'")));
}
float ASlAiPlayerCharacter::PlayDeadAnim()
{
GetMesh()->PlayAnimation(AnimDead, false);
return AnimDead->GetMaxCurrentTime();
}
来到玩家控制器,也添加一个玩家死亡方法,执行角色死亡时要执行的逻辑;并且利用一个计时器委托来在死亡动画播放完的时候弹出失败界面。
SlAiPlayerController.h
public:
// 死亡
void PlayerDead();
private:
// 死亡时间函数
void DeadTimeOut();
private:
// 死亡时间委托
FTimerHandle DeadHandle;
SlAiPlayerController.cpp
void ASlAiPlayerController::PlayerDead()
{
// 转换到第三视角
SPCharacter->ChangeView(EGameViewMode::Third);
// 告诉角色播放死亡动画,获得死亡时间
float DeadDuration = SPCharacter->PlayDeadAnim();
// 锁住输入
LockedInput(true);
// 添加事件委托
FTimerDelegate TimerDelegate = FTimerDelegate::CreateUObject(this, &ASlAiPlayerController::DeadTimeOut);
// 延时跳转 UI
GetWorld()->GetTimerManager().SetTimer(DeadHandle, TimerDelegate, DeadDuration, false);
}
void ASlAiPlayerController::DeadTimeOut()
{
// 设置游戏暂停
SetPause(true);
// 设置游戏模式为混合
SwitchInputMode(false);
// 更新界面
ShowGameUI.ExecuteIfBound(CurrentUIType, EGameUIType::Lose);
// 更新当前 UI
CurrentUIType = EGameUIType::Lose;
// 锁住输入
LockedInput(true);
}
来到 PlayerState,在玩家生命值为 0 的时候通知玩家控制器玩家已经挂了,并且不再根据饥饿度自动恢复生命值和降低饥饿度。
SlAiPlayerState.cpp
void ASlAiPlayerState::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (Hunger <= 0) {
HP -= DeltaSeconds * 2;
}
else {
if (!IsDead) { // 添加一个判断
Hunger -= DeltaSeconds * 2;
HP += DeltaSeconds;
}
}
HP = FMath::Clamp<float>(HP, 0.f, 500.f);
Hunger = FMath::Clamp<float>(Hunger, 0.f, 600.f);
UpdateStateWidget.ExecuteIfBound(HP / 500.f, Hunger / 500.f);
if (HP == 0.f && !IsDead) {
// 告诉控制器自己死了
if (SPController) SPController->PlayerDead();
IsDead = true;
}
}
void ASlAiPlayerState::AcceptDamage(int DamageVal)
{
if (HP == 0 && !IsDead) {
// 告诉控制器自己死了
if (SPController) SPController->PlayerDead();
IsDead = true;
}
}
运行游戏,此时角色生命值到 0 时会播放死亡动画,角色生命值和饥饿值不再变动,并且播放完毕后会弹出结束的按钮。
62. SaveGame 读取存档
在场景里添加一下资源树木和石矿,并且可以分别为敌人、拾取物和资源创建场景文件夹,便于管理。
创建一个 C++ 的 Save Game 类,路径为 Public/GamePlay,名字为 SlAiSaveGame,Save Game 是虚幻引擎提供的专门用于保存游戏数据用的类。
在存档类的头文件声明所有需要保存的数据,其实最好的方法是定义一个结构体,然后把结构体放在头文件声明中。
SlAiSaveGame.h
public:
UPROPERTY(VisibleAnywhere, Category = "SlAi")
FVector PlayerLocation;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
float PlayerHP;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
float PlayerHunger;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<int32> InputIndex;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<int32> InputNum;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<int32> NormalIndex;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<int32> NormalNum;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<int32> ShortcutIndex;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<int32> ShortcutNum;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<FVector> EnemyLocation;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<float> EnemyHP;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<FVector> ResourceRock;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<FVector> ResourceTree;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<FVector> PickupStone;
UPROPERTY(VisibleAnywhere, Category = "SlAi")
TArray<FVector> PickupWood;
给 PlayerState 添加一个加载生命值和饥饿度的方法,用于读档时给角色设置。
SlAiPlayerState.h
public:
// 加载血量和饥饿度
void LoadState(float HPVal, float HungerVal);
SlAiPlayerState.cpp
void ASlAiPlayerState::LoadState(float HPVal, float HungerVal)
{
HP = HPVal;
Hunger = HungerVal;
// 执行修改玩家状态 UI 的委托
UpdateStateWidget.ExecuteIfBound(HP / 500.f, Hunger / 500.f);
}
给敌人类添加一个加载生命值的方法,用于读档时给敌人设置。并且场景里的树木、矿石、敌人等资源都要一个 bool 变量来判断自己是否已经在存档前被销毁了,毕竟不可能已经采集过的资源存档读档后又出现一次。
SlAiEnemyCharacter.h
public:
// 加载血量
void LoadHP(float HPVal);
public:
// 是否下一帧销毁自己,由 GameMode 加载游戏存档时进行设置
bool IsDestroyNextTick;
SlAiEnemyCharacter.cpp
ASlAiEnemyCharacter::ASlAiEnemyCharacter()
{
// 设置下一帧不销毁自己,得放在构造函数进行初始化,避免与 GameMode 的加载函数冲突
IsDestroyNextTick = false;
}
void ASlAiEnemyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 自动销毁
if (IsDestroyNextTick) DestroyEvent();
}
void ASlAiEnemyCharacter::LoadHP(float HPVal)
{
HP = HPVal;
// 修改血量显示
HPBarWidget->ChangeHP(HP / 200.f);
}
给资源基类添加一个 bool 变量,决定它是否销毁。
SlAiResourceObject.h
public:
// 是否在下一帧销毁
bool IsDestroyNextTick;
SlAiResourceObject.cpp
ASlAiResourceObject::ASlAiResourceObject()
{
// 设置在下一帧不销毁
IsDestroyNextTick = false;
}
void ASlAiResourceObject::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 如果检测到下一帧要销毁
if (IsDestroyNextTick) GetWorld()->DestroyActor(this);
}
给掉落物基类添加一个 bool 变量,决定它是否销毁。
SlAiPickupObject.h
public:
// 是否下一帧销毁
bool IsDestroyNextTick;
SlAiPickupObject.cpp
ASlAiPickupObject::ASlAiPickupObject()
{
// 设置下一帧不销毁
IsDestroyNextTick = false;
}
void ASlAiPickupObject::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 如果检测到下一帧要销毁
if (IsDestroyNextTick) GetWorld()->DestroyActor(this);
}
给背包管理器也添加一个加载存档时运行的方法。
SlAiPackageManager.h
public:
// 加载存档
void LoadRecord(TArray<int32>* InputIndex, TArray<int32>* InputNum, TArray<int32>* NormalIndex, TArray<int32>* NormalNum, TArray<int32>* ShortcutIndex, TArray<int32>* ShortcutNum);
SlAiPackageManager.cpp
void SlAiPackageManager::LoadRecord(TArray<int32>* InputIndex, TArray<int32>* InputNum, TArray<int32>* NormalIndex, TArray<int32>* NormalNum, TArray<int32>* ShortcutIndex, TArray<int32>* ShortcutNum)
{
for (int i = 0; i < InputContainerList.Num(); ++i) {
if ((*InputIndex)[i] != 0) InputContainerList[i]->ResetContainerPara((*InputIndex)[i], (*InputNum)[i]);
}
for (int i = 0; i < NormalContainerList.Num(); ++i) {
if ((*NormalIndex)[i] != 0) NormalContainerList[i]->ResetContainerPara((*NormalIndex)[i], (*NormalNum)[i]);
}
for (int i = 0; i < ShortcutContainerList.Num(); ++i) {
if ((*ShortcutIndex)[i] != 0) ShortcutContainerList[i]->ResetContainerPara((*ShortcutIndex)[i], (*ShortcutNum)[i]);
}
}
前面的准备工作的做好了,来到 GameMode,添加一个 bool 变量决定是否需要加载存档,毕竟如果新开游戏也强行加载存档那就不好了。
声明一个存档指针,用于存放存档实例。然后声明两个方法,一个是存档总加载方法,另一个是加载背包物品的方法。不放在一块的原因是,背包界面本身需要先初始化才能加载背包物品,把后者分出来有利于前者不被阻塞。
SlAiGameMode.h
protected:
// 存档加载
void LoadRecord();
// 给背包进行加载存档,这个函数一定要在第二帧再执行,否则快捷栏没初始化完成会崩溃
void LoadRecordPackage();
private:
// 是否需要加载存档
bool IsNeedLoadRecord;
// 游戏存档指针
class USlAiSaveGame* GameRecord;
这里再 Tick() 里顺序的安排与代码本身的逻辑,确保了载入背包物品时背包肯定已经初始化完毕了。
SlAiGameMode.cpp
// 引入头文件
#include "SlAiSaveGame.h"
#include "SlAiResourceRock.h"
#include "SlAiResourceTree.h"
#include "SlAiPickupStone.h"
#include "SlAiPickupWood.h"
ASlAiGameMode::ASlAiGameMode()
{
// 开始设置不需要加载存档
IsNeedLoadRecord = false;
}
void ASlAiGameMode::Tick(float DeltaSeconds)
{
// 给背包加载存档,放在初始化背包上面是为了在第二帧再执行
LoadRecordPackage();
InitializePackage();
}
void ASlAiGameMode::BeginPlay()
{
// 载入存档
LoadRecord();
}
void ASlAiGameMode::LoadRecord()
{
// 如果 RecordName 为空或者是 Default,直接 return
if (SlAiDataHandle::Get()->RecordName.IsEmpty() || SlAiDataHandle::Get()->RecordName.Equals(FString("Default"))) return;
// 循环检测存档是否已经存在
for (TArray<FString>::TIterator It(SlAiDataHandle::Get()->RecordDataList); It; ++It) {
// 如果有一个一样就直接设置为 true,并且直接跳出循环
if ((*It).Equals(SlAiDataHandle::Get()->RecordName)) {
IsNeedLoadRecord = true;
break;
}
}
// 如果需要加载,并且存档存在,则进行加载
if (IsNeedLoadRecord && UGameplayStatics::DoesSaveGameExist(SlAiDataHandle::Get()->RecordName, 0)) {
GameRecord = Cast<USlAiSaveGame>(UGameplayStatics::LoadGameFromSlot(SlAiDataHandle::Get()->RecordName, 0));
}
else {
IsNeedLoadRecord = false;
}
// 如果需要加载并且存档存在
if (IsNeedLoadRecord && GameRecord) {
// 设置玩家位置和血量
SPCharacter->SetActorLocation(GameRecord->PlayerLocation);
SPState->LoadState(GameRecord->PlayerHP, GameRecord->PlayerHunger);
// 循环设置敌人
int EnemyCount = 0;
for (TActorIterator<ASlAiEnemyCharacter> EnemyIt(GetWorld()); EnemyIt; ++EnemyIt) {
if (EnemyCount < GameRecord->EnemyLocation.Num()) {
(*EnemyIt)->SetActorLocation(GameRecord->EnemyLocation[EnemyCount]);
(*EnemyIt)->LoadHP(GameRecord->EnemyHP[EnemyCount]);
}
else {
// 告诉这个敌人下一帧销毁
(*EnemyIt)->IsDestroyNextTick = true;
}
++EnemyCount;
}
// 循环设置岩石
int RockCount = 0;
for (TActorIterator<ASlAiResourceRock> RockIt(GetWorld()); RockIt; ++RockIt) {
if (RockCount < GameRecord->ResourceRock.Num()) {
(*RockIt)->SetActorLocation(GameRecord->ResourceRock[RockCount]);
}
else {
// 告诉这个资源下一帧销毁
(*RockIt)->IsDestroyNextTick = true;
}
++RockCount;
}
// 循环设置树木
int TreeCount = 0;
for (TActorIterator<ASlAiResourceTree> TreeIt(GetWorld()); TreeIt; ++TreeIt) {
if (TreeCount < GameRecord->ResourceTree.Num()) {
(*TreeIt)->SetActorLocation(GameRecord->ResourceTree[TreeCount]);
}
else {
// 告诉这个资源下一帧销毁
(*TreeIt)->IsDestroyNextTick = true;
}
++TreeCount;
}
// 循环设置拾取物品石头
int StoneCount = 0;
for (TActorIterator<ASlAiPickupStone> StoneIt(GetWorld()); StoneIt; ++StoneIt) {
if (StoneCount < GameRecord->PickupStone.Num()) {
(*StoneIt)->SetActorLocation(GameRecord->PickupStone[StoneCount]);
}
else {
// 告诉这个资源下一帧销毁
(*StoneIt)->IsDestroyNextTick = true;
}
++StoneCount;
}
// 循环设置拾取物品木头
int WoodCount = 0;
for (TActorIterator<ASlAiPickupWood> WoodIt(GetWorld()); WoodIt; ++WoodIt) {
if (WoodCount < GameRecord->PickupWood.Num()) {
(*WoodIt)->SetActorLocation(GameRecord->PickupWood[WoodCount]);
}
else {
// 告诉这个资源下一帧销毁
(*WoodIt)->IsDestroyNextTick = true;
}
++WoodCount;
}
}
}
void ASlAiGameMode::LoadRecordPackage()
{
// 如果背包没有初始化或者不用加载存档,直接返回
if (!IsInitPackage || !IsNeedLoadRecord) return;
if (IsNeedLoadRecord && GameRecord) {
SlAiPackageManager::Get()->LoadRecord(&GameRecord->InputIndex, &GameRecord->InputNum, &GameRecord->NormalIndex, &GameRecord->NormalNum, &GameRecord->ShortcutIndex, &GameRecord->ShortcutNum);
}
// 最后设置不用加载存档了
IsNeedLoadRecord = false;
}
目前只写好了存档的加载,下一节课会补充存档的存储逻辑。