109. UE5 GAS RPG 实现检查点的存档功能
在这一篇文章里,我们接着实现存档的功能,保存当前玩家的生成位置,游戏里有很多中方式去实现玩家的位置存储,这里我们采用检查点的方式,当玩家接触到当前检查点后,我们可以通过检查点进行保存玩家的状态,后续也能够实现检查点移动玩家等等功能。
实现定义角色生成位置
存档加载关卡后,需要一个出生位置,如果场景里有多个PlayerStart,我们如何确定让角色在哪个PlayerStart生成呢?
答案是,我们可以为每个PlayerStart设置标签,然后覆写GameMode的选择初始点的函数来实现。
首先,我们实现对数据的全局存储,这里需要使用到GameInstance,我们将其作为父类,实现一个派生类,来实现自定义的需求。
设置自定义命名
在类里,我们存储几个值,一个是切换关卡时需要获取的PlayerStart的标签命名,另外就是如果需要保存,所需的存档名称和索引。
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的标签
//角色切换关卡后默认生成位置的PlayerStart的标签
UPROPERTY(EditDefaultsOnly)
FName DefaultPlayerStartTag;
//覆写父类的选择PlayerStart函数,修改为可以通过Tag获取生成位置
virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override;
函数实现这里,我们会获取到关卡里的所有的PlayerStart,然后从GameInstance获取需要生成的标签,遍历获取到对应的PlayerStart生成,所以,只需要在进入关卡前,将GameInstance的标签修改了然后进入场景时,就可以自动寻找对应的PlayerStart去生成。
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标签配置属性。
//存储玩家关卡出生位置的标签
UPROPERTY()
FName PlayerStartTag;
在创建新存档时,使用GameMode设置的默认PlayerStart标签
接着在GameMode存储存档时,将存档的视图模型的中的PlayerStart标签存储到存档
存储没问题了,就是读取存档时,将存档里存储的PlayerStart标签设置给存档的视图模型
最后,在我们在加载界面视图模型进入游戏的函数里,在调用加载关卡之前,将PlayerStart标签存储到GameInstance里,进入关卡后,然后再通过我们覆写的函数获取对应标签的PlayerStart
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的位置生成。
创建检查点
在正常游戏流程里,为了保存游戏进度,开发者会使用某种方式让进度保存下来,比如到达某个进度后自动保存,又或者像生化危机里的打字机。
这里,我们将实现一种检查点的类,在角色接触后,自动保存当前进度。
命名为检查点类,我们将在里面增加一些额外的内容。
在类里,我们将增加两个属性,用于显示检查点的模型和触发保存游戏的碰撞盒子
private:
//检查点显示的模型
UPROPERTY(VisibleAnywhere)
TObjectPtr<UStaticMeshComponent> CheckpointMesh;
//检查点模型使用的碰撞体
UPROPERTY(VisibleAnywhere)
TObjectPtr<USphereComponent> Sphere;
然后增加一个函数,用于玩家角色和碰撞球碰撞后的逻辑处理
/**
* 球碰撞体和物体发生碰撞后的回调
* @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);
接着增加一个函数,玩家角色碰撞后的函数处理,主要里面是创建了一个新的材质实例,修改自发光,表示检查点已经激活。
//当玩家角色和检测点产生碰撞后,检查点被激活触发此函数
void HandleGlowEffects();
由于自发光亮起需要时间轴,这个比较方便在蓝图里实现,我们再增加一个需蓝图实现的函数。
/**
* 检查点激活后的处理,需要在蓝图中对其实现
* @param DynamicMaterialInstance 传入检查点模型的材质实例
*/
UFUNCTION(BlueprintImplementableEvent)
void CheckpointReached(UMaterialInstanceDynamic* DynamicMaterialInstance);
以下是整个.h文件
// 版权归暮志未晚所有。
#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里,我们对函数进行实现,首先在构造函数里,我们实例化模型和碰撞体。
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类型物体产生重叠事件
}
在游戏开始时,绑定球碰撞体的重叠函数
void ACheckPoint::BeginPlay()
{
Super::BeginPlay();
//绑定重叠事件
Sphere->OnComponentBeginOverlap.AddDynamic(this, &ACheckPoint::OnSphereOverlap);
}
接着实现重叠函数,在触发重叠时,我们需要实现保存当前的检查点标签,然后在调用碰撞后处理函数
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();
}
}
然后我们取消碰撞检测,提升性能,并创建一个新的材质实例,调用蓝图函数实现渐变发光效果。
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类里增加两个函数,一个用于获取当前使用的存档,另一个是将修改后的存档保存下来。
//获取到当前游戏进行中所使用的存档数据
ULoadScreenSaveGame* RetrieveInGameSaveData() const;
/**
* 保存游戏中的进度
* @param SaveObject 需要保存的数据
*/
void SaveInGameProgressData(ULoadScreenSaveGame* SaveObject) const;
实现这里,我们可以在GameInstance身上获取到存档使用的Name和Index,通过这两项获取到存档数据。
保存函数这里,我们还需要使用存档的标签去修改GameInstance身上的PlayerStart的标签。然后保存。
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);
}
接着在玩家角色接口这里增加一个函数,用于碰撞触发后保存存档使用
//保存游戏进度
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void SaveProgress(const FName& CheckpointTag);
在玩家基类里覆写
virtual void SaveProgress_Implementation(const FName& CheckpointTag) override;
实现这里,我们获取到GameMode,然后获取存档,修改存档数据,并保存回去。
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类增加一个参数,用于存储检查点数组,用于存储角色已经激活的检查点
//当前已经激活的检测点
UPROPERTY()
TArray<FName> ActivatedPlayerStatTags = TArray<FName>();
然后将检查点类的激活函数修改为public,这样,可以在类以外调用
接着,我们增加一个私有函数用于高亮已经激活的检查点
private:
//高亮已经激活的检查点
void HighlightEnabledCheckpoints(TArray<AActor*> CheckPoints) const;
函数实现,我们将获取存档,并遍历所有的检查点,如果检查点的Tag存在于已激活的数组内,我们将检查点进行高亮显示。
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。