109. UE5 GAS RPG 实现检查点的存档功能

在这一篇文章里,我们接着实现存档的功能,保存当前玩家的生成位置,游戏里有很多中方式去实现玩家的位置存储,这里我们采用检查点的方式,当玩家接触到当前检查点后,我们可以通过检查点进行保存玩家的状态,后续也能够实现检查点移动玩家等等功能。

实现定义角色生成位置

存档加载关卡后,需要一个出生位置,如果场景里有多个PlayerStart,我们如何确定让角色在哪个PlayerStart生成呢?

答案是,我们可以为每个PlayerStart设置标签,然后覆写GameMode的选择初始点的函数来实现。

首先,我们实现对数据的全局存储,这里需要使用到GameInstance,我们将其作为父类,实现一个派生类,来实现自定义的需求。

设置自定义命名

在类里,我们存储几个值,一个是切换关卡时需要获取的PlayerStart的标签命名,另外就是如果需要保存,所需的存档名称和索引。

cpp 复制代码
UCLASS()
class RPG_API URPGGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:

	//角色进入关卡后默认生成的PlayerStart的Tag
	UPROPERTY()
	FName PlayerStartTag = FName();

	//当前使用的或后续保存内容到的存档名称
	UPROPERTY()
	FString LoadSlotName = FString();

	//当前使用活后续保存的存档索引
	UPROPERTY()
	int32 LoadSlotIndex = 0;
};

然后在我们自定义的GameMode里增加一个参数用于设置玩家生成的PlayerStart的标签

cpp 复制代码
	//角色切换关卡后默认生成位置的PlayerStart的标签
	UPROPERTY(EditDefaultsOnly)
	FName DefaultPlayerStartTag;

	//覆写父类的选择PlayerStart函数,修改为可以通过Tag获取生成位置
	virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override;

函数实现这里,我们会获取到关卡里的所有的PlayerStart,然后从GameInstance获取需要生成的标签,遍历获取到对应的PlayerStart生成,所以,只需要在进入关卡前,将GameInstance的标签修改了然后进入场景时,就可以自动寻找对应的PlayerStart去生成。

cpp 复制代码
AActor* ARPGGameMode::ChoosePlayerStart_Implementation(AController* Player)
{
	const URPGGameInstance* RPGGameInstance = Cast<URPGGameInstance>(GetGameInstance());
	
	//获取关卡里的所有PlayerStart实例
	TArray<AActor*> Actors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), Actors);
	
	if(Actors.Num() > 0)
	{
		//获取到第一个实例对象
		AActor* SelectedActor = Actors[0];
		for(AActor* Actor : Actors)
		{
			if(APlayerStart* PlayerStart = Cast<APlayerStart>(Actor))
			{
				//判断PlayerStart的Tag设置是否为指定的Tag
				if(PlayerStart->PlayerStartTag == RPGGameInstance->PlayerStartTag)
				{
					SelectedActor = PlayerStart;
					break;
				}
			}
		}
		return SelectedActor;
	}
	return nullptr;
}

添加PlayerStart标签配置

我们需要在存档里实现对关卡里的开始标签的存储,在读取存档进入关卡时,可以明确知道角色需要在哪里生成。

所以,我们需要在LoadScreenSaveMode(存档类)和存档ViewModel视图模型里增加PlayerStart标签配置属性。

cpp 复制代码
	//存储玩家关卡出生位置的标签
	UPROPERTY()
	FName PlayerStartTag;

在创建新存档时,使用GameMode设置的默认PlayerStart标签

接着在GameMode存储存档时,将存档的视图模型的中的PlayerStart标签存储到存档

存储没问题了,就是读取存档时,将存档里存储的PlayerStart标签设置给存档的视图模型

最后,在我们在加载界面视图模型进入游戏的函数里,在调用加载关卡之前,将PlayerStart标签存储到GameInstance里,进入关卡后,然后再通过我们覆写的函数获取对应标签的PlayerStart

cpp 复制代码
void UMVVM_LoadScreen::EnterGameButtonPressed(const int32 Slot)
{
	ARPGGameMode* RPGGameMode = Cast<ARPGGameMode>(UGameplayStatics::GetGameMode(this));

	//设置全局数据,方便后续使用
	URPGGameInstance* RPGGameInstance = Cast<URPGGameInstance>(RPGGameMode->GetGameInstance());
	RPGGameInstance->LoadSlotName = LoadSlots[Slot]->GetSlotName();
	RPGGameInstance->LoadSlotIndex = LoadSlots[Slot]->SlotIndex;
	RPGGameInstance->PlayerStartTag = LoadSlots[Slot]->PlayerStartTag;

	//进入场景
	RPGGameMode->TravelToMap(LoadSlots[Slot]);
}

创建蓝图

接下来,我们编译打开UE,在BP_GameMode里设置默认选择的PlayerStart的标签

接着,我们基于GameInstance类创建一个蓝图

设置命名

在项目设置里,将默认的GameInstance修改为我们所需的GameInstance

最后,我们在场景里设置PlayerStart的标签,注意,如果多个PlayerStart设置了相同的对应的标签,角色将在第一个获取的PlayerStart的位置生成。

创建检查点

在正常游戏流程里,为了保存游戏进度,开发者会使用某种方式让进度保存下来,比如到达某个进度后自动保存,又或者像生化危机里的打字机。

这里,我们将实现一种检查点的类,在角色接触后,自动保存当前进度。

命名为检查点类,我们将在里面增加一些额外的内容。

在类里,我们将增加两个属性,用于显示检查点的模型和触发保存游戏的碰撞盒子

cpp 复制代码
private:

	//检查点显示的模型
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UStaticMeshComponent> CheckpointMesh;

	//检查点模型使用的碰撞体
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<USphereComponent> Sphere;

然后增加一个函数,用于玩家角色和碰撞球碰撞后的逻辑处理

cpp 复制代码
	/**
	 * 球碰撞体和物体发生碰撞后的回调
	 * @param OverlappedComponent 发生重叠事件的自身的碰撞体对象
	 * @param OtherActor 目标的actor对象
	 * @param OtherComp 目标的碰撞体组件
	 * @param OtherBodyIndex 目标身体的索引
	 * @param bFromSweep 是否为瞬移检测到的碰撞
	 * @param SweepResult 如果位置发生过瞬移(直接设置到某处),两个位置中间的内容会记录到此对象内
	 */
	UFUNCTION()
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

接着增加一个函数,玩家角色碰撞后的函数处理,主要里面是创建了一个新的材质实例,修改自发光,表示检查点已经激活。

cpp 复制代码
	//当玩家角色和检测点产生碰撞后,检查点被激活触发此函数
	void HandleGlowEffects();

由于自发光亮起需要时间轴,这个比较方便在蓝图里实现,我们再增加一个需蓝图实现的函数。

cpp 复制代码
	/**
	 * 检查点激活后的处理,需要在蓝图中对其实现
	 * @param DynamicMaterialInstance 传入检查点模型的材质实例
	 */
	UFUNCTION(BlueprintImplementableEvent)
	void CheckpointReached(UMaterialInstanceDynamic* DynamicMaterialInstance);

以下是整个.h文件

cpp 复制代码
// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerStart.h"
#include "CheckPoint.generated.h"

class USphereComponent;
/**
 * 
 */
UCLASS()
class RPG_API ACheckPoint : public APlayerStart
{
	GENERATED_BODY()

public:

	//构造函数
	ACheckPoint(const FObjectInitializer& ObjectInitializer);

protected:

	virtual void BeginPlay() override;
	
	/**
	 * 球碰撞体和物体发生碰撞后的回调
	 * @param OverlappedComponent 发生重叠事件的自身的碰撞体对象
	 * @param OtherActor 目标的actor对象
	 * @param OtherComp 目标的碰撞体组件
	 * @param OtherBodyIndex 目标身体的索引
	 * @param bFromSweep 是否为瞬移检测到的碰撞
	 * @param SweepResult 如果位置发生过瞬移(直接设置到某处),两个位置中间的内容会记录到此对象内
	 */
	UFUNCTION()
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	/**
	 * 检查点激活后的处理,需要在蓝图中对其实现
	 * @param DynamicMaterialInstance 传入检查点模型的材质实例
	 */
	UFUNCTION(BlueprintImplementableEvent)
	void CheckpointReached(UMaterialInstanceDynamic* DynamicMaterialInstance);

	//当玩家角色和检测点产生碰撞后,检查点被激活触发此函数
	void HandleGlowEffects();
private:

	//检查点显示的模型
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UStaticMeshComponent> CheckpointMesh;

	//检查点模型使用的碰撞体
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<USphereComponent> Sphere;
};

在cpp里,我们对函数进行实现,首先在构造函数里,我们实例化模型和碰撞体。

cpp 复制代码
ACheckPoint::ACheckPoint(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
{
	//关闭帧更新
	PrimaryActorTick.bCanEverTick = false;

	//创建检测点显示模型
	CheckpointMesh = CreateDefaultSubobject<UStaticMeshComponent>("CheckpointMesh");
	CheckpointMesh->SetupAttachment(GetRootComponent());
	CheckpointMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); //设置查询并产生物理
	CheckpointMesh->SetCollisionResponseToChannels(ECR_Block); //设置阻挡所有物体与其重叠

	//设置球碰撞体
	Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
	Sphere->SetupAttachment(CheckpointMesh);
	Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly); //设置其只用作查询使用
	Sphere->SetCollisionResponseToChannels(ECR_Ignore); //设置其忽略所有碰撞检测
	Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); //设置其与Pawn类型物体产生重叠事件
}

在游戏开始时,绑定球碰撞体的重叠函数

cpp 复制代码
void ACheckPoint::BeginPlay()
{
	Super::BeginPlay();

	//绑定重叠事件
	Sphere->OnComponentBeginOverlap.AddDynamic(this, &ACheckPoint::OnSphereOverlap);
}

接着实现重叠函数,在触发重叠时,我们需要实现保存当前的检查点标签,然后在调用碰撞后处理函数

cpp 复制代码
void ACheckPoint::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	//if(OtherActor->ActorHasTag("Player")) //如果只需要判断是不是玩家角色通过标签判断即可
	if(OtherActor->Implements<UPlayerInterface>())
	{
		//修改存档当的检测点
		IPlayerInterface::Execute_SaveProgress(OtherActor, PlayerStartTag);
		
		//如果与碰撞体重叠的是
		HandleGlowEffects();
	}
}

然后我们取消碰撞检测,提升性能,并创建一个新的材质实例,调用蓝图函数实现渐变发光效果。

cpp 复制代码
void ACheckPoint::HandleGlowEffects()
{
	//取消碰撞检查
	Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	//创建一个新材质实例,修改效果
	UMaterialInstanceDynamic* DynamicMaterialInstance = UMaterialInstanceDynamic::Create(CheckpointMesh->GetMaterial(0), this);
	CheckpointMesh->SetMaterial(0, DynamicMaterialInstance);
	CheckpointReached(DynamicMaterialInstance); //触发检查点修改材质后的回调
}

编译代码,打开UE,创建一个基于类的蓝图

在检测点里,我们需要修改碰撞体大小和检查点的模型

大致效果如下,可以按需设置

在平视角,我们需要将PlayerStart和模型水平,这样保证放置的时候,防止PlayerStart的位置靠下,生成角色生成到地面以下。

拖入场景中,点击End建,检测点将会自动附着到地面,青蓝色箭头是玩家在检查点生成位置和朝向,黄色碰撞球是激活检测点范围。

接着,我们修改材质,增加自发光相关节点,设置GlowEnd最大亮度,以及GlowControl来控制进度,GlowControl值为1时,将达到亮度的最大值。

接着,我们创建一个实例,去调节对应的参数。

接着,在蓝图里,实现碰撞函数回调,使用时间轴修改GlowControl

在时间轴里去修改更新的值

我们将放置到场景里的检查点的设置其标签,可以用来实现保存通过标签去寻找位置。

实现PlayerStart标签的保存

我们要实现玩家角色在场景中接触到检查点后,更新存档,将当前的检查点的值保存到存档里。

首先在GameMode类里增加两个函数,一个用于获取当前使用的存档,另一个是将修改后的存档保存下来。

cpp 复制代码
	//获取到当前游戏进行中所使用的存档数据
	ULoadScreenSaveGame* RetrieveInGameSaveData() const;

	/**
	 * 保存游戏中的进度
	 * @param SaveObject 需要保存的数据
	 */
	void SaveInGameProgressData(ULoadScreenSaveGame* SaveObject) const;

实现这里,我们可以在GameInstance身上获取到存档使用的Name和Index,通过这两项获取到存档数据。

保存函数这里,我们还需要使用存档的标签去修改GameInstance身上的PlayerStart的标签。然后保存。

cpp 复制代码
ULoadScreenSaveGame* ARPGGameMode::RetrieveInGameSaveData() const
{
	const URPGGameInstance* RPGGameInstance = Cast<URPGGameInstance>(GetGameInstance());

	//从游戏实例获取到存档名称和索引
	const FString InGameLoadSlotName = RPGGameInstance->LoadSlotName;
	const int32 InGameLoadSlotIndex = RPGGameInstance->LoadSlotIndex;

	//获取已保存的存档数据
	return GetSaveSlotData(InGameLoadSlotName, InGameLoadSlotIndex);
}

void ARPGGameMode::SaveInGameProgressData(ULoadScreenSaveGame* SaveObject) const
{
	URPGGameInstance* RPGGameInstance = Cast<URPGGameInstance>(GetGameInstance());

	//修改下一次复活的检测点
	RPGGameInstance->PlayerStartTag = SaveObject->PlayerStartTag;

	//从游戏实例获取到存档名称和索引
	const FString InGameLoadSlotName = RPGGameInstance->LoadSlotName;
	const int32 InGameLoadSlotIndex = RPGGameInstance->LoadSlotIndex;

	//保存存档
	UGameplayStatics::SaveGameToSlot(SaveObject, InGameLoadSlotName, InGameLoadSlotIndex);
}

接着在玩家角色接口这里增加一个函数,用于碰撞触发后保存存档使用

cpp 复制代码
	//保存游戏进度
	UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
	void SaveProgress(const FName& CheckpointTag);

在玩家基类里覆写

cpp 复制代码
virtual void SaveProgress_Implementation(const FName& CheckpointTag) override;

实现这里,我们获取到GameMode,然后获取存档,修改存档数据,并保存回去。

cpp 复制代码
void ARPGHero::SaveProgress_Implementation(const FName& CheckpointTag)
{
	if(const ARPGGameMode* GameMode = Cast<ARPGGameMode>(UGameplayStatics::GetGameMode(this)))
	{
		//获取存档
		ULoadScreenSaveGame* SaveGameData = GameMode->RetrieveInGameSaveData();
		if(SaveGameData == nullptr) return;

		//修改存档数据
		SaveGameData->PlayerStartTag = CheckpointTag;

		//保存存档
		GameMode->SaveInGameProgressData(SaveGameData);
	}
}

我们在与检查点碰撞时,已经调用此函数,实现了存档的修改保存。

最后,我们在场景里多加几个检查点,来测试效果。创建文档,角色会生成在一个检查点上,然后我们走到另一个检查点上,重新进入游戏,查看角色下一次会不会生成在最后退出的检查点旁边,如果能够证明代码无误。

实现角色已经激活的检测点一直高亮

我们现在制作的当前检查点效果,还没有实现的一项是,让玩家已经激活的检查点一直保持高亮状态,这样,如果玩家迷路了,可以清除的得知这一段路之前探索过。

为了实现这个效果,我们需要将之前已经探索到的检查点都记录下来,然后在进入场景后,每次激活,将信息记录到存档里。在进入一个新关卡时,在GameMode的BeginPlayer会触发,我们会在此函数里处理检查点是否需要高亮。

首先,我们在SaveGame类增加一个参数,用于存储检查点数组,用于存储角色已经激活的检查点

cpp 复制代码
	//当前已经激活的检测点
	UPROPERTY()
	TArray<FName> ActivatedPlayerStatTags = TArray<FName>();

然后将检查点类的激活函数修改为public,这样,可以在类以外调用

接着,我们增加一个私有函数用于高亮已经激活的检查点

cpp 复制代码
private:
	
	//高亮已经激活的检查点
	void HighlightEnabledCheckpoints(TArray<AActor*> CheckPoints) const;

函数实现,我们将获取存档,并遍历所有的检查点,如果检查点的Tag存在于已激活的数组内,我们将检查点进行高亮显示。

cpp 复制代码
void ARPGGameMode::HighlightEnabledCheckpoints(TArray<AActor*> CheckPoints) const
{
	//获取存档
	ULoadScreenSaveGame* SaveGameData = RetrieveInGameSaveData();
	if(SaveGameData == nullptr) return;

	//遍历关卡内的所有的检查点,如果数组里存在,将高亮显示
	for(AActor* Actor : CheckPoints)
	{
		if(ACheckPoint* CheckPoint = Cast<ACheckPoint>(Actor))
		{
			if(SaveGameData->ActivatedPlayerStatTags.Contains(CheckPoint->PlayerStartTag))
			{
				CheckPoint->HandleGlowEffects();
			}
		}
	}
}

在关卡打开后,生成角色时,我们调用此函数,进行场景关卡的检查点进行初始化高亮

最后,还有保存已经激活的检查点,我们可以选择在保存角色的存档时候,将当前检查点的标签保存进去。

重点,你每个添加到关卡的检查点,要做到全局不同,也就是每个关卡的检查点都不要重名,我们可以考虑关卡+检查点的索引方式去设置检查点的tag。

相关推荐
mmsx9 分钟前
android 登录界面编写
android·登录界面
姜毛毛-JYM9 分钟前
【JetPack】Navigation知识点总结
android
小灰灰要减肥23 分钟前
装饰者模式
java
张铁铁是个小胖子34 分钟前
MyBatis学习
java·学习·mybatis
花生糖@1 小时前
Android XR 应用程序开发 | 从 Unity 6 开发准备到应用程序构建的步骤
android·unity·xr·android xr
是程序喵呀1 小时前
MySQL备份
android·mysql·adb
Yan.love1 小时前
开发场景中Java 集合的最佳选择
java·数据结构·链表
casual_clover1 小时前
Android 之 List 简述
android·list
椰椰椰耶1 小时前
【文档搜索引擎】搜索模块的完整实现
java·搜索引擎
大G哥1 小时前
java提高正则处理效率
java·开发语言