UE4开发C++沙盒游戏教程笔记(二十一)(对应教程集数 64)完结
63. 保存存档与项目完结
继续补充保存数据的逻辑。
在 PlayerState 里面添加一个保存角色生命值和饥饿度的方法。
SlAiPlayerState.h
public:
// 保存血量和饥饿值到指定数据
void SaveState(float& HPVal, float& HungerVal);
SlAiPlayerState.cpp
void ASlAiPlayerState::SaveState(float& HPVal, float& HungerVal)
{
HPVal = HP;
HungerVal = Hunger;
}
敌人类里面添加一个获取敌人目前生命值的方法,方便后面存进敌人血量数组。
SlAiEnemyCharacter.h
public:
// 获取血量,保存游戏时调用
float GetHP();
SlAiEnemyCharacter.cpp
float ASlAiEnemyCharacter::GetHP()
{
return HP;
}
然后就是背包管理器添加一个保存背包内容器的物品种类与数量的方法。
SlAiPackageManager.h
public:
// 保存存档数据
void SaveData(TArray<int32>& InputIndex, TArray<int32>& InputNum, TArray<int32>& NormalIndex, TArray<int32>& NormalNum, TArray<int32>& ShortcutIndex, TArray<int32>& ShortcutNum);
SlAiPackageManager.cpp
void SlAiPackageManager::SaveData(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) {
InputIndex.Add(InputContainerList[i]->GetIndex());
InputNum.Add(InputContainerList[i]->GetNum());
}
for (int i = 0; i < NormalContainerList.Num(); ++i) {
NormalIndex.Add(NormalContainerList[i]->GetIndex());
NormalNum.Add(NormalContainerList[i]->GetNum());
}
for (int i = 0; i < ShortcutContainerList.Num(); ++i) {
ShortcutIndex.Add(ShortcutContainerList[i]->GetIndex());
ShortcutNum.Add(ShortcutContainerList[i]->GetNum());
}
}
给数据处理类添加一个新增存档名的方法。
SlAiDataHandle.h
public:
// 添加新存档名
void AddNewRecord();
SlAiDataHandle.cpp
void SlAiDataHandle::AddNewRecord()
{
// 将现在的存档名添加到数组
RecordDataList.Add(RecordName);
// 更新 json 数据
SlAiSingleton<SlAiJsonHandle>::Get()->UpdateRecordData(GetEnumValueAsString<ECultureTeam>(FString("ECultureTeam"), CurrentCulture), MusicVolume, SoundVolume, &RecordDataList);
}
在 Content/Res/ConfigData/RecordData.json 里面把之前测试添加的(如果有的话)RecordData 的对象去掉,只留下默认的 “0”: “Default”
在 GameMode 里添加一个保存游戏方法,就是实例化一个存档类,然后把游戏的各种数据保存到里面。
SlAiGameMode.h
public:
// 保存游戏
void SaveGame();
SlAiGameMode.cpp
void ASlAiGameMode::SaveGame()
{
// 如果存档名是 Default,不进行保存
if (SlAiDataHandle::Get()->RecordName.Equals(FString("Default"))) return;
// 创建一个新的存档
USlAiSaveGame* NewRecord = Cast<USlAiSaveGame>(UGameplayStatics::CreateSaveGameObject(USlAiSaveGame::StaticClass()));
// 对存档进行赋值
// 设置玩家位置和生命值
NewRecord->PlayerLocation = SPCharacter->GetActorLocation();
SPState->SaveState(NewRecord->PlayerHP, NewRecord->PlayerHunger);
// 循环设置敌人
for (TActorIterator<ASlAiEnemyCharacter> EnemyIt(GetWorld()); EnemyIt; ++EnemyIt) {
NewRecord->EnemyLocation.Add((*EnemyIt)->GetActorLocation());
NewRecord->EnemyHP.Add((*EnemyIt)->GetHP());
}
// 循环设置岩石
for (TActorIterator<ASlAiResourceRock> RockIt(GetWorld()); RockIt; ++RockIt) {
NewRecord->ResourceRock.Add((*RockIt)->GetActorLocation());
}
// 循环设置树木
for (TActorIterator<ASlAiResourceTree> TreeIt(GetWorld()); TreeIt; ++TreeIt) {
NewRecord->ResourceTree.Add((*TreeIt)->GetActorLocation());
}
// 循环设置拾取物品石头
for (TActorIterator<ASlAiPickupStone> StoneIt(GetWorld()); StoneIt; ++StoneIt) {
NewRecord->PickupStone.Add((*StoneIt)->GetActorLocation());
}
// 循环设置拾取物品木头
for (TActorIterator<ASlAiPickupWood> WoodIt(GetWorld()); WoodIt; ++WoodIt) {
NewRecord->PickupWood.Add((*WoodIt)->GetActorLocation());
}
// 获取背包数据
SlAiPackageManager::Get()->SaveData(NewRecord->InputIndex, NewRecord->InputNum, NewRecord->NormalIndex, NewRecord->NormalNum, NewRecord->ShortcutIndex, NewRecord->ShortcutNum);
// 查看是否已经有存档存在
if (UGameplayStatics::DoesSaveGameExist(SlAiDataHandle::Get()->RecordName, 0)) {
// 有的话先删除
UGameplayStatics::DeleteGameInSlot(SlAiDataHandle::Get()->RecordName, 0);
}
// 保存存档
UGameplayStatics::SaveGameToSlot(NewRecord, SlAiDataHandle::Get()->RecordName, 0);
// 查看 json 是否已经有这个存档
bool IsRecordExist = false;
for (TArray<FString>::TIterator It(SlAiDataHandle::Get()->RecordDataList); It; ++It) {
// 只要有一个相同,就跳出
if ((*It).Equals(SlAiDataHandle::Get()->RecordName)) {
IsRecordExist = true;
break;
}
}
// 如果存档不存在,让数据管理类添加存档到 json
if (!IsRecordExist) SlAiDataHandle::Get()->AddNewRecord();
}
最后到 HUD 类,绑定暂停菜单界面中的 “保存游戏” 按钮的委托到这个 GameMode 的方法。
SlAiGameHUD.cpp
#include "SSlAiMiniMapWidget.h"
// 引入头文件
#include "SSlAiGameMenuWidget.h"
void ASlAiGameHUD::BeginPlay()
{
// 保存游戏事件绑定
GameHUDWidget->GameMenuWidget->SaveGameDele.BindUObject(GM, &ASlAiGameMode::SaveGame);
}
编译后打开项目,在 Project Settings 里把默认地图和游玩地图设置回 MenuMap。用 Standalone 模式运行游戏,新建一个存档,然后在场景中作出一些游玩过程的改变,存档后退出。再次运行游戏并且读取该存档,可以发现场景大体上还是上一次保存时的状态。
如果大家发现明明对敌人造成了伤害后存档了,再次读档时敌人的血量又满了。这个跟老师代码的逻辑不太一样 = =💧。笔者发现是因为 SlAiGameMode 的载入方法在执行顺序上先于敌人的 BeginPlay() 方法,导致载入的数据被敌人的 BeginPlay() 方法覆盖掉了。
并且如果载入时报错导致引擎崩溃,且崩溃来源是 GameMode 的载入方法中的一处调用 LoadHP() 的代码,这个问题的原因同上。读者可以作如下修改:
ASlAiEnemyCharacter::ASlAiEnemyCharacter()
{
// 移到此处
SAssignNew(HPBarWidget, SSlAiEnemyHPWidget);
// 移到此处
HP = 200.f;
}
void ASlAiEnemyCharacter::BeginPlay()
{
// 设置血条 Widget(此处代码移到构造函数)
//SAssignNew(HPBarWidget, SSlAiEnemyHPWidget);
// 设置初始生命值(此处代码移到构造函数)
//HP = 200.f;
}
至此,课程已经结束啦!希望这个系列的笔记能够帮到你,在此也感谢梁迪老师的高质量课程。: )