UE4开发C++沙盒游戏教程笔记(二十)(对应教程集数 61 ~ 63)

该博客围绕UE4游戏开发展开,介绍了游戏暂停菜单的布局编写、按钮绑定及界面切换方法;添加了音效和菜单更改音量功能,完善玩家角色死亡逻辑;还创建了存档类,为游戏各元素添加加载存档方法,目前完成了存档加载,后续将补充存储逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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,分别命名为 SlAiMusicClassSlAiSoundClass,分别作为音乐组和音效组。
一个 SoundMix,命名为 SlAiSoundMix,作为管理两个声音组的混合器。

到 Content/Res/Sound/GameSound 目录下将 GameBG 的 SoundClass 选为 SlAiMusicClass,同目录下的其他音效文件的 SoundClass 都选为 SlAiSoundClass。

在 SlAiSoundMix 中添加新建的两个 SoundClass 进去,第一个是 SlAiMusicClass,第二个是 SlAiSoundClass。

来到第三人称动画蓝图,在右下方动画资源列表选到 Player_Eat 动画,在时间轴刚开始的位置添加 PlaySound 的 Notify,然后选中它,在面板里选择 Eat 作为播放的声音,此时播放动画能听见音效。然后对 Player_Fight 和 Player_Death 也做同样的操作,给它们添加声音。

安放声音 Notify
去到第一人称动画蓝图也给吃和挥剑动画做以上操作,声音播放位置自己看着穿插就好。

然后去敌人的动画蓝图,给敌人的攻击动画添加 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;
}

目前只写好了存档的加载,下一节课会补充存档的存储逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值