独立游戏《星尘异变》UE5 C++程序开发日志6——实现存档和基础设置系统

news2024/12/22 22:51:37

        

目录

一、存档类

1.创建一个SaveGame类

2.存储关卡内数据

3.加载关卡数据

4.关于定时器

5.存储全局数据

6.加载全局数据

二、存档栏

1.存档栏的数据结构

2.创建新存档

3.覆盖已有存档

4.删除存档

三、游戏的基础设置

1.存储游戏设置的数据结构

2.初始化设置

3.修改设置


        本篇日志将会介绍如何实现一个模拟经营游戏中的存档系统以及能够调整游戏画质分辨率等的游戏设置菜单,效果如下图:

一、存档类

1.创建一个SaveGame类

        UE中存档的原理是我们建一个SaveGame类,然后我们在其中声明要存储的变量类型,再实例化一个该类的对象,将要存储的值传给该对象中声明的变量,再调用保存函数就能将数据以.sav文件的方式保存到本地,读档时也是从 该文件中实例化一个SaveGame对象,将该对象中的值赋给当前场景以实现数据的加载

        创建SaveGame类的子类,这里我们已经创建好了两个类,一个存储全局设置包括存档栏,另一个储存关卡数据:

      

2.存储关卡内数据

        每个关卡存档都包含关卡内必须要保存的数据,这里以玩家仓库为例,同时我们每个存档还有记录游玩总时长的功能,FTimeSpan是存储流逝的时间的结构,可以从秒数转化而来:

UCLASS()
class ASTROMUTATE_2_API USaveGameData : public USaveGame
{
	GENERATED_BODY()
public:

	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="SaveGame")
	FInventoryInformation PlayerStorage;//玩家仓库

	//游戏游玩的总秒数
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Time")
	int PlayedSeconds;

	//游戏游玩的总时间
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Time")
	FTimespan PlayedTime;
};

        接下来就可以实现保存游戏的函数了,首先传入存档的文件名,稍后在加载游戏是也是根据该存档的文件名来找到具体存档,SaveGameSlot中的第一个参数是要保存的SaveGame对象,第二个参数是本地存档文件的名字,第三个参数是玩家的索引,单机游戏使用0即可:

void ADebugActor::SaveGame(const FString& SaveFileName)
{
	UE_LOG(LogTemp,Warning,TEXT("Saving"));
    //实例化我们之前创建的SaveGame类
	USaveGameData* DataToSave{ Cast<USaveGameData>(UGameplayStatics::CreateSaveGameObject(USaveGameData::StaticClass())) };
    //该存档游玩的总秒数=之前读档是继承的总秒数+当前时间-进入游戏时获取的时间
	DataToSave->PlayedSeconds=PlayedSeconds+(FDateTime::Now()-StartTime).GetTotalSeconds();
    //将秒数转换为小时分钟
	DataToSave->PlayedTime=FTimespan::FromSeconds(DataToSave->PlayedSeconds);
	PlayedTime=DataToSave->PlayedTime;
    //从场景中获取玩家库存信息
	SetPlayerStorageEvent();
    //将玩家库存数据赋值给存档类
	DataToSave->PlayerStorage = PlayerStorage;
    //将存档保存到本地
	UGameplayStatics::SaveGameToSlot(DataToSave, SaveFileName, 0);
	UE_LOG(LogTemp,Warning,TEXT("game succesfully saved"));
}

3.加载关卡数据

        在加载存档前,无论是从主菜单加载还是从已经进入的关卡中加载,我们都需要重新打开这个关卡,在打开关卡之后,我们首先需要确保所有关键Actor初始化完成,如果是用c++定义的Actor,可以直接使用DispatchBeginPlay()来确保该actor执行完了BeginPlay中的所有步骤,没有用C++定义的actor比较麻烦,这里的实现方法时进入关卡后设置一个每0.5秒一检查的定时器,所有待加载的Actor都标记为已执行完BeginPlay后,再调用下面的LoadGame函数。

        因为我们实现加载游戏函数的Actor是在关卡内的,所以要实现从主菜单加载,就在玩家点击存档栏中的存档时,将游戏实例中的存档文件名设置成要加载的存档的名字,然后在进入关卡时如果检查到这个文件名不为空,则执行加载。

void ADebugActor::BeginPlay()
{//省略了其他与存档系统无关的代码
	Instance = Cast<UAstromutateGameInstance>(GetWorld()->GetGameInstance());
	if (!Instance->IsValidLowLevel())
	{
		UE_LOG(LogTemp, Error, TEXT("BeginPlay in DebugActor failed,invalid pointer:Instance"));
		return;
	}	
	Super::BeginPlay();
	if(Instance->SaveFileName!="Empty")
	{
        //每0.5秒检查一次读档条件是否满足
		GetWorld()->GetTimerManager().SetTimer(LoadTimer,this,&ADebugActor::LoadGameFromMenuEvent,0.5,true,0.1);
	}

}

bool ADebugActor::LoadGame(const FString& SaveFileName)
{
    //确保必要组件被初始化完成
	Prime->DispatchBeginPlay();
	TradingSystem->DispatchBeginPlay();
	USaveGameData* DataToLoad{ Cast<USaveGameData>(UGameplayStatics::LoadGameFromSlot(SaveFileName,0)) };
    //没找到对应名字的存档
	if (!DataToLoad->IsValidLowLevel())
	{
		UE_LOG(LogTemp, Error, TEXT("LoadGame failed,save file: %s doesn't exist"),*SaveFileName);
		return false;
	}
	PlayedSeconds=DataToLoad->PlayedSeconds;
    //记录进入存档时的时间
	StartTime=FDateTime::Now();
	PlayerStorage = DataToLoad->PlayerStorage;
    //将加载的值赋给场景中
    SetNewPlayerStorage(PlayerStorage);
	return true;
}

4.关于定时器

        其实在之前几篇博客介绍的系统中也用到了定时器,这里代码直接用到了,所以我们详细介绍一下UEC++中定时器的用法。

        要使用定时器,首先需要声明一个定时器柄,用来绑定调用的事件,我们使用上面展示过的加载使用的定时器为例:

UPROPERTY(BlueprintReadWrite)
	FTimerHandle LoadTimer;

        我们详细看一下上面是怎么开始一个定时器的,首先所有定时器相关的函数都在TimerManager类中,要开始一个定时器,首先传入要绑定的定时器柄,然后是调用该函数的对象,一般使用this,接着是定时器委托FTimerDelegate,它必须是无输入参数和返回值的函数,定义格式如下,接着是定时器触发的时间间隔,单位是秒,后面的bool值表示是否循环,如果为true,则每隔一个我们设定的间隔就会调用一次绑定的定时器委托函数,最后一个参数是从定时器启动到第一次执行委托函数的时间间隔,如果<0,则该时间等于前面定义的定时器的时间间隔

GetWorld()->GetTimerManager().SetTimer(LoadTimer,this,&ADebugActor::LoadGameFromMenuEvent,0.5,true,0.1);

        还有一些常用的定时器相关的函数,只需要传入我们声明的定时器柄,这里一起来看一下:

//使定时器失效,解除器绑定的定时器委托
GetWorld()->GetTimerManager().ClearTimer(LoadTimer);
//暂停定时器
GetWorld()->GetTimerManager().PauseTimer(LoadTimer);
//取消暂停定时器
GetWorld()->GetTimerManager().UnPauseTimer(LoadTimer);
//返回定时器是否暂停
GetWorld()->GetTimerManager().IsTimerPaused(LoadTimer);
//返回定时器是否有效,失效的方式包括CLearTimer,和非循环定时器执行过一次委托,暂停时仍然有效
GetWorld()->GetTimerManager().IsTimerActive(LoadTimer);
//返回定时器设定的执行间隔时间
GetWorld()->GetTimerManager().GetTimerRate(LoadTimer);
//返回定时器距离上一次执行委托的时间
GetWorld()->GetTimerManager().GetTimerElapsed(LoadTimer);
//返回定时器距离下一次执行委托的时间
GetWorld()->GetTimerManager().GetTimerRemaining(LoadTimer);	

5.存储全局数据

        全局数据包括存档栏、游戏的设置,教程是否出现过等,这里我们只展示存档栏和显示设置,其数据结构如下,存档栏和显示设置的结构后面用到的时候再说:

UCLASS()
class ASTROMUTATE_2_API UGameSettingSave : public USaveGame
{
	GENERATED_BODY()
public:
	//存档栏
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Saving")
	TArray<FSaveSlot> SaveSlots;

	//游戏设置参数
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Saving")
	FGameSettingsData GameSettings;
};

        这里全局设置在存储时使用的对象直接为加载的全局数据存档,因为我们在进入游戏时已经确保了其一定存在

void UAstromutateGameInstance::SaveGameSetting()
{
	auto DataToSave{ Cast<UGameSettingSave>(UGameplayStatics::LoadGameFromSlot("GameSettingsSaves",0)) };
	DataToSave->SaveSlots=SaveSlots;
	DataToSave->GameSettings=GameSettingsData;
	UGameplayStatics::SaveGameToSlot(DataToSave, "GameSettingsSaves", 0);
}

6.加载全局数据

        如果玩家是第一次打开游戏,就要创建一个全局数据的存档,同时初始化全局数据,如果已有存档就直接加载

void UAstromutateGameInstance::LoadGameSetting()
{
	auto DataToSave{ Cast<UGameSettingSave>(UGameplayStatics::LoadGameFromSlot("GameSettingsSaves",0)) };
    //第一次进入游戏
	if (!DataToSave->IsValidLowLevel())
	{
		UGameSettingSave* DataToSave2{ Cast<UGameSettingSave>(UGameplayStatics::CreateSaveGameObject(UGameSettingSave::StaticClass())) };
		UGameplayStatics::SaveGameToSlot(DataToSave2, "GameSettingsSaves", 0);
		GameSettingsData=FGameSettingsData();
        //这是用来还原设置更改的变量,后面会介绍
		LastSavedGameSetting=GameSettingsData;
		SaveGameSetting();
	}
	else
	{
		SaveSlots=DataToSave->SaveSlots;
		GameSettingsData=DataToSave->GameSettings;
	}
}

二、存档栏

        我们需要用存档栏来展示玩家的存档,同时包括创建存档时的命名,如不命名自动命名为当前时间,以及修改命名和删除存档的功能

1.存档栏的数据结构

        首先是存档栏数组中元素的数据结构,文件名用于保存和加载的调用,命名,游戏时间,保存时间用于展示:

USTRUCT(BlueprintType)
struct FSaveSlot
{
	friend bool operator==(const FSaveSlot& Lhs, const FSaveSlot& RHS)
	{
		return Lhs.FileName == RHS.FileName;
	}

	friend bool operator!=(const FSaveSlot& Lhs, const FSaveSlot& RHS)
	{
		return !(Lhs == RHS);
	}

	FSaveSlot() = default;
	GENERATED_BODY()

	//存档文件名
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Save")
	FString FileName;

	//玩家命名的存档名
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Save")
	FText SaveName;

	//该存档总游戏时间
	UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Save")
	FTimespan PlayedTime;

	//保存时的时间
	UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Save")
	FDateTime SavedTime;
};

2.创建新存档

        在玩家创建存档时,如果没有输入自定义的存档名,就使用当前时间作为存档名,文件名使用玩家定义的命名+当前时间,这样玩家可以创建多个重名存档而不会产生冲突

void UAstromutateGameInstance::AddSave(const FText SaveName)
{
	FSaveSlot Temp{FSaveSlot()};
	Temp.SaveName=SaveName;
    //玩家没有输入命名
	if(SaveName.IsEmpty())
	{
		Temp.SaveName=FText::FromString(FDateTime::Now().ToString());
	}
    //获取当前时间
	const auto Time {FDateTime::Now()};
	Temp.SavedTime=Time;
	FString FileName{SaveName.ToString()+Time.ToString()};
    //因为这个函数在游戏实例中,存档函数在关卡的中控Actor中,所以要找一下中控的Actor
	for (TActorIterator<ADebugActor>it(GetWorld()); it; ++it)
	{
		if (IsValid(*it))
		{
			it->SaveGame(FileName);
			Temp.PlayedTime=it->PlayedTime;
			break;
		}
		UE_LOG(LogTemp,Error,TEXT("AddSave failed,invalid pointer:debugactor"));
	}
	Temp.FileName=FileName;
	SaveSlots.Add(Temp);
    //更新全局数据存档中的存档栏信息
	SaveGameSetting();
}

3.覆盖已有存档

        在覆盖已有存档时要注意更新游戏时间和保存的时间

void UAstromutateGameInstance::CoverSave(const int& Index)
{
    //检查索引是否合法
	if(Index<0||Index>=SaveSlots.Num())
	{
		UE_LOG(LogTemp,Error,TEXT("CoverSave failed,invalid index:%d"),Index);
	}
    //和创建新存档一样的原因,要找一下负责中控的Actor
	for (TActorIterator<ADebugActor>it(GetWorld()); it; ++it)
	{
		if (IsValid(*it))
		{
			it->SaveGame(SaveSlots[Index].FileName);
			USaveGameData* DataToSave{ Cast<USaveGameData>(UGameplayStatics::LoadGameFromSlot(SaveSlots[Index].FileName,0)) };
			SaveSlots[Index].SavedTime=FDateTime::Now();
			SaveSlots[Index].PlayedTime=DataToSave->PlayedTime;
			break;
		}
		UE_LOG(LogTemp, Error, TEXT("CoverSave failed,invalid pointer:debugactor"));
	}
}

4.删除存档

        删除存档用到的DeleteGameInSlot函数需要传入存档的文件名和玩家索引

void UAstromutateGameInstance::RemoveSave(const int& Index)
{
    //检查索引是否合法
	if(Index<0||Index>=SaveSlots.Num())
	{
		UE_LOG(LogTemp,Error,TEXT("RemoveSave failed,invalid index:%d"),Index);
	}
    //删除本地文件
	UGameplayStatics::DeleteGameInSlot(SaveSlots[Index].FileName, 0);    
    //删除存档栏中的元素
	SaveSlots.RemoveAt(Index);
    //更新全局数据中的存档栏信息
	SaveGameSetting();
}

三、游戏的基础设置

1.存储游戏设置的数据结构

         虚幻中提供了GameUserSetting这个类来设置游戏音量显示画质等,也提供了保存和读取的功能,但为了统一管理,这里我们都使用自定义的存档系统,这里以窗口模式和分辨率为例,窗口模式是一个赋值0-2的枚举,分别是全屏,窗口化全屏,窗口化,分辨率是FIntPoint结构,也就是两个整数

USTRUCT(BlueprintType)
struct FGameSettingsData
{//这里仅展示分辨率和窗口模式
	FGameSettingsData() = default;
	GENERATED_BODY()

	//全屏模式
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="GameSetting")
	int WindowMode{0};

	//分辨率
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="GameSetting")
};

2.初始化设置

       因为是在同事写的蓝图的基础上改进的,所以这里也使用蓝图展示,首先我们要获取适合本机的分辨率,窗口化和全屏(全屏包括窗口化全屏)所适用的分辨率是不同的:

然后从存档中获取之前保存的信息,

再根据当前的窗口模式到对应的数组中找到匹配的,给当前选项的索引赋值

3.修改设置

        UI是同事布置的,这里只展示我写的按下按钮后的事件,一个+按钮一个-按钮,按一下对应设置选项的索引就会+1或-1,也都可以循环,首先来看窗口模式的修改,如果全屏和窗口化之间有切换,那么当前的分辨率选项也要跟着改变,这里就使其变为对应分辨率数组的最后一个元素

分辨率的改变比较简单,因为不太会整理蓝图,所以只给大家看一个+按钮的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1933783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript基础 第四弹 学习笔记

函数 1、为什么需要函数&#xff1f;可以实现代码复用&#xff0c;提高开发效率。 函数的定义 &#xff1a;函数function&#xff0c;是被设计为执行特定任务的代码块。 函数可以把具有相同或相似逻辑的代码‘包裹’起来&#xff0c;通过函数调用执行这些被“包裹”的代码逻…

羊大师:羊奶精华,补钙免疫,养颜促消化

在浩瀚的自然馈赠中&#xff0c;羊奶以其独特的精华&#xff0c;成为了现代人追求健康生活的优选。它不仅仅是一种饮品&#xff0c;更是大自然赋予我们的宝贵滋养圣品。 补钙免疫&#xff0c;守护健康基石 羊奶中富含的钙质&#xff0c;是构建强健骨骼的基石。其高吸收率的特性…

免杀笔记 ----> 动态调用

前一段时间不是说要进行IAT表的隐藏吗&#xff0c;终于给我逮到时间来写了&#xff0c;今天就来先将最简单的一种方式 ----> 动态调用&#xff01;&#xff01;&#xff01; 1.静态查杀 这里还是说一下我们为什么要对他进行隐藏呢&#xff1f;&#xff1f;&#xff1…

HBuilderX打包流程(H5)?HBuilder如何发布前端H5应用?前端开发怎样打包发布uniapp项目为h5?

打包步骤&#xff1a; 1、打开hbuilder x》发行》网站-PC Web或手机H5(仅适用于uni-app)(H) 2、面板里的所有信息都可以不填&#xff0c;也不用勾选》直接点击【发行】即可 3、打包成功&#xff1a; 4、部署 按照打包后的路径&#xff0c;找到打包好的文件夹&#xff0c;把文…

【前端数据层高可用架构】

前端数据层高可用架构 前后端架构模式如下图 在这个架构下,客端数据可用率计算方式: 因此整体数据可用性分析表如下: 只有在客端和 BFF 都正常的情况下数据才能可用,而这种情况占比不是很高,因此整体的用户体验就不是很好。 本次建设目标 本文的设计方案就是要解决…

【前端】表单密码格式—校验。

如图&#xff1a;实现表单输入密码和确认密码的时候进行表单校验。 实现方式&#xff1a; 1.在代码的data里面定义&#xff0c;函数验证的方法。如图所示,代码如下 【代码】如下&#xff1a; const validatePassword (rule, value, callback) > {if (value ) {callback(n…

Java SpringBoot 若依 后端实现评论“盖楼“,“楼中楼“功能 递归查询递归组装评论结构

效果图 数据库设计 还可以使用路径模块 一级评论id,二级评论id, 用like最左匹配原则查询子评论 因为接手遗留代码&#xff0c;需要添加字段&#xff0c;改动数据库&#xff0c;我就不改动了&#xff0c;导致我下面递归查询子评论不是很好。 业务代码 Overridepublic List<S…

C++类与对象(补)

感谢大佬的光临各位&#xff0c;希望和大家一起进步&#xff0c;望得到你的三连&#xff0c;互三支持&#xff0c;一起进步 个人主页&#xff1a;LaNzikinh-CSDN博客 文章目录 前言一.默认成员函数二.static三.友元四.匿名对象总结 前言 类的默认成员函数&#xff0c;默认成员…

充电宝选哪个好?选充电宝主要看什么?充电宝攻略请收下!

当我们的手机、平板等设备电量告急时&#xff0c;充电宝就如同一位救星&#xff0c;为我们解决燃眉之急。然而&#xff0c;面对市场上琳琅满目的充电宝产品&#xff0c;“充电宝选哪个好&#xff1f;”这一问题常常让我们感到困惑。选择一款合适的充电宝并非易事&#xff0c;需…

Qt支持LG高级汽车内容平台

Qt Group与LG 电子&#xff08;简称LG&#xff09;正携手合作&#xff0c;将Qt软件框架嵌入其基于 webOS的ACPLG车载娱乐平台&#xff0c;用于应用程序开发。该合作旨在让原始设备制造商&#xff08;OEM&#xff09;的开发者和设计师能为汽车创建更具创新性的沉浸式汽车内容流媒…

ClickHouse集成LDAP实现简单的用户认证

1.这里我的ldap安装的是docker版的 docker安装的化就yum就好了 sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl start docker 使用下面的命令验证sudo docker run hello-world docker pull osixia/openl…

SQL Server Query Store Settings (查询存储设置)

参考&#xff1a;Query Store Settings - Erin Stellato 在 SQL Server 2017 中&#xff0c;有九 (9) 个设置与查询存储相关。虽然这些设置记录在sys.database_query_store_options中&#xff0c;但我经常被问到每个设置的值“应该”是多少。我在下面列出了每个设置&am…

Puppeteer动态代理实战:提升数据抓取效率

引言 Puppeteer是由Google Chrome团队开发的一个Node.js库&#xff0c;用于控制Chrome或Chromium浏览器。它提供了高级API&#xff0c;可以进行网页自动化操作&#xff0c;包括导航、屏幕截图、生成PDF、捕获网络活动等。在本文中&#xff0c;我们将重点介绍如何使用Puppeteer…

移动打车项目

1.技术栈&#xff1a; AndroidJNIHTTPSlibeventmysql/redis高德地图 2.概要流程设计 1.注册 2.登录 3.司机地理位置上传更新 4.乘客地理位置上传更新 5.乘客下单流程 6.司机完成订单流程

[AWS]EKS启动HPA,HPA指标<unknown>,报错:error: Metrics API not available

背景&#xff1a;在AWS上创建的EKS集群&#xff0c;想要对于deployment部署HPA&#xff0c;来autoscling副本数。 1.HPA一般基于CPU或者内存对副本数进行控制&#xff0c;所以必须需要Metrics Server。 &#xff08;Metrics Server 是 Kubernetes 集群的一个关键组件&#xff0…

7、自定义管理站点

目录 1、自定义后台表单2、添加关联的对象3、自定义后台更改列表4、自定义后台界面和风格&#xff08;1&#xff09;自定义你的工程的模板&#xff08;2&#xff09;自定义你应用的模板 1、自定义后台表单 通过admin.site.register(Question)注册Question模型&#xff0c;Djan…

AI自动生成PPT哪个软件好?高效制作PPT优选这4个

7.15初伏的到来&#xff0c;也宣告三伏天的酷热正式拉开序幕~在这个传统的节气里&#xff0c;人们以各种方式避暑纳凉&#xff0c;享受夏日的悠闲时光。 而除了传统的避暑活动&#xff0c;我们还可以用一种新颖的方式记录和分享这份夏日的清凉——那就是通过PPT的方式将这一传…

抖音视频素材去哪里找啊?视频素材网站库分享

在这个视觉盛宴的抖音平台上&#xff0c;高质量和有趣的视频素材常常是吸引观众的重要钥匙。如果你也正在寻找那些能让你的视频作品更加出色的资源&#xff0c;那么恭喜你&#xff0c;今天我将为你介绍10个超实用的视频素材网站&#xff0c;让你的抖音视频创作充满创意和效率。…

只用 CSS 能玩出什么花样?

在前端开发领域&#xff0c;CSS 不仅仅是一种样式语言&#xff0c;它更像是一位多才多艺的艺术家&#xff0c;能够创造出令人惊叹的视觉效果。本文将带你探索 CSS 的无限可能&#xff0c;从基本形状到动态动画&#xff0c;从几何艺术到仿生设计&#xff0c;只用 CSS 就能玩出令…

linux开机后不用登陆,无法正常进入系统,出现:/#的提示符

linux开机后不用登陆&#xff0c;无法正常进入系统&#xff0c;出现:/#的提示符 解决方案&#xff1a; 1、输入命令 ls /dev/mapper 此时会出现3个文件。其中rhel-root文件 是我们下面所要用的文件。 ls的目的就是为了让大家能知道自己带"-root" 文件的前缀是什…