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。

相关推荐
熊大如如2 小时前
Java 反射
java·开发语言
猿来入此小猿2 小时前
基于SSM实现的健身房系统功能实现十六
java·毕业设计·ssm·毕业源码·免费学习·猿来入此·健身平台
teacher伟大光荣且正确3 小时前
Qt Creator 配置 Android 编译环境
android·开发语言·qt
goTsHgo3 小时前
Spring Boot 自动装配原理详解
java·spring boot
卑微的Coder3 小时前
JMeter同步定时器 模拟多用户并发访问场景
java·jmeter·压力测试
pjx9873 小时前
微服务的“导航系统”:使用Spring Cloud Eureka实现服务注册与发现
java·spring cloud·微服务·eureka
多多*4 小时前
算法竞赛相关 Java 二分模版
java·开发语言·数据结构·数据库·sql·算法·oracle
爱喝酸奶的桃酥4 小时前
MYSQL数据库集群高可用和数据监控平台
java·数据库·mysql
唐僧洗头爱飘柔95275 小时前
【SSM-SSM整合】将Spring、SpringMVC、Mybatis三者进行整合;本文阐述了几个核心原理知识点,附带对应的源码以及描述解析
java·spring·mybatis·springmvc·动态代理·ioc容器·视图控制器
骑牛小道士5 小时前
Java基础 集合框架 Collection接口和抽象类AbstractCollection
java