在这一篇文章里,我们接着实现存档的功能,保存当前玩家的生成位置,游戏里有很多中方式去实现玩家的位置存储,这里我们采用检查点的方式,当玩家接触到当前检查点后,我们可以通过检查点进行保存玩家的状态,后续也能够实现检查点移动玩家等等功能。
实现定义角色生成位置
存档加载关卡后,需要一个出生位置,如果场景里有多个PlayerStart,我们如何确定让角色在哪个PlayerStart生成呢?
答案是,我们可以为每个PlayerStart设置标签,然后覆写GameMode的选择初始点的函数来实现。
首先,我们实现对数据的全局存储,这里需要使用到GameInstance,我们将其作为父类,实现一个派生类,来实现自定义的需求。
设置自定义命名
在类里,我们存储几个值,一个是切换关卡时需要获取的PlayerStart的标签命名,另外就是如果需要保存,所需的存档名称和索引。
UCLASS()
class RPG_API URPGGameInstance : public UGameInstance
{GENERATED_BODY()public://角色进入关卡后默认生成的PlayerStart的TagUPROPERTY()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设置是否为指定的Tagif(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。