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

        

目录

一、存档类

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中,所以要找一下中控的Actorfor (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);}//和创建新存档一样的原因,要找一下负责中控的Actorfor (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://xiahunao.cn/news/3247671.html

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

相关文章

链表面试练习习题集(Java)

1. 思路&#xff1a; 因为杨辉三角是由二维数组构成&#xff0c;所以要先创建一个二维数组&#xff0c;如何用顺序表表示二维数组&#xff0c;可以通过List<List<Interger>>来表示一个二维数组&#xff0c;可以这样理解&#xff1a;先创建一个一维数组List&#x…

智慧消防建设方案(完整方案参考PPT)

智慧消防系统建设方案旨在通过物联网、大数据与云计算技术&#xff0c;集成火灾自动报警、智能监控、应急指挥等功能于一体。方案部署智能传感器监测火情&#xff0c;实时数据分析预警&#xff0c;实现火灾早发现、早处置。构建可视化指挥平台&#xff0c;优化应急预案&#xf…

Redis之List列表

目录 一.列表讲解 二.列表命令 三.内部编码 四.应用场景 Redis的学习专栏&#xff1a;http://t.csdnimg.cn/a8cvV 一.列表讲解 列表类型是用来存储多个有序的字符串&#xff0c;如下所示&#xff0c;a、b、c、d、e五个元素从左到右组成了一个有序的列表&#xff0c;列表中的…

android R ext4 image打包脚本介绍

一、Android R打包指令使用介绍 &#xff08;1&#xff09;mkuserimg_mke2fs #./mkuserimg_mke2fs --help usage: mkuserimg_mke2fs [-h] [--android_sparse] [--journal_size JOURNAL_SIZE][--timestamp TIMESTAMP] [--fs_config FS_CONFIG][--product_out PRODUCT_OUT][--b…

目标检测入门:4.目标检测中的一阶段模型和两阶段模型

在前面几章里&#xff0c;都只做了目标检测中的目标定位任务&#xff0c;并未做目标分类任务。目标检测作为计算机视觉领域的核心人物之一&#xff0c;旨在从图像中识别出所有感兴趣的目标&#xff0c;并确定它们的类别和位置。现在目标检测以一阶段模型和两阶段模型为代表的。…

常见漏洞之SSRF

一、SSRF简介 服务器端请求伪造&#xff08;SSRF&#xff09;是一种安全漏洞&#xff0c;允许攻击者通过构造恶意请求并利用存在缺陷的Web应用作为代理&#xff0c;向内外网发送请求&#xff0c;以实现攻击目的。SSRF攻击主要利用了服务端提供的某些功能&#xff0c;这些功能能…

基于jeecgboot-vue3的Flowable流程仿钉钉流程设计器-支持VForm3表单的选择与支持

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、初始化的时候加载表单 /** 查询表单列表 */ const getFormList () > {listForm().then(res > formOptions.value res.result.records) } 2、开始节点的修改&#xff0c;增加表…

【转盘案例-开始选号按钮-旋转 Objective-C语言】

一、接下来,我们来说这个“开始选号”按钮, 1.我们之前已经可以自旋转了,当我点击开始选号按钮之后,我让它快速的去旋转,5圈儿,然后停在最上方, 我先把ViewController的startRotate这句话啊,注释掉,先不让它自旋转呢, 把这句话注释掉, 接下来,我们command + R, …

Java---抽象类

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 &#xff0c;Java 欢迎大家访问~ 创作不易&#xff0c;大佬们点赞鼓励下吧~ 文章目录 抽象类什么的抽象类&…

stm32入门-----EXTI外部中断(上 ——理论篇)

目录 前言 一、中断系统 1.基本概念 2.执行过程 二、stm32中断 1.stm32中断类型 2.NVIC总管 3.NVIC的优先级分组 三、EXIT外部中断 1.基本概念 2.AFIO复用IO口 3.EXIT执行过程 前言 本期我们就开始进入到学习stm32的中断系统了&#xff0c;在此之前我们学习过51的知道中…

KAFKA搭建教程

KAFKA搭建教程 期待您的关注 KAFKA学习笔记 帮助更多人 目录 KAFKA搭建教程 1.下载Kafka并解压 2.添加环境变量 3.修改 server.properties 文件 4.将kafka复制到其它节点 5.修改node1、node2节点的broker.id 6.将master的环境变量同步到node1、 node2 7.启动zookeeper…

乐鑫ESP-IoT-Bridge方案简化设备智能联网通信,启明云端乐鑫代理商

随着物联网技术的快速发展&#xff0c;设备联网已成为实现智能化的关键一步。然而&#xff0c;不同设备之间的通信协议、接口等差异&#xff0c;使得设备联网变得复杂且困难。 乐鑫推出的ESP-IoT-Bridge联网方案&#xff0c;正是为了解决这一难题&#xff0c;为物联网场景下的…

【iOS】类对象的结构分析

目录 对象的分类object_getClass和class方法isa流程和继承链分析isa流程实例验证类的继承链实例验证 类的结构cache_t结构bits分析实例验证属性properties方法methods协议protocolsro类方法 类结构流程图解 对象的分类 OC中的对象主要可以分为3种&#xff1a;实例对象&#xf…

HTML2048小游戏(最新版)

比上一篇文章的2048更好一点。 控制方法&#xff1a;WASD键&#xff08;小写&#xff09;或页面上四个按钮 效果图如下&#xff1a; 源代码在图片后面 源代码 HTML <!DOCTYPE html> <html lang"en"> <head><meta charset&…

Qt日志库QsLog使用教程

前言 最近项目中需要用到日志库。上一次项目中用到了log4qt库&#xff0c;这个库有个麻烦的点是要配置config文件&#xff0c;所以这次切换到了QsLog。用了后这个库的感受是&#xff0c;比较轻量级&#xff0c;嘎嘎好用&#xff0c;推荐一波。 下载QsLog库 https://github.c…

Python、Rust与AI的未来展望

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 非常期待和您一起在这个小…

leetcode简单题27 N.119 杨辉三角II rust描述

// 直接生成杨辉三角当前行 pub fn get_row(row_index: i32) -> Vec<i32> {let mut row vec![1; (row_index 1) as usize];for i in 1..row_index as usize {for j in (1..i).rev() {row[j] row[j] row[j - 1];}}row } // 空间优化的方法 pub fn get_row2(row_ind…

【C#】计算两条直线的交点坐标

问题描述 计算两条直线的交点坐标&#xff0c;可以理解为给定坐标P1、P2、P3、P4&#xff0c;形成两条线&#xff0c;返回这两条直线的交点坐标&#xff1f; 注意区分&#xff1a;这两条线是否垂直、是否平行。 代码实现 斜率解释 斜率是数学中的一个概念&#xff0c;特别是…

Windows 2012安装之实现远程连接

新建虚拟机 点击稍后安装操作系统 点击Microsoft Windows(W) 选择Windows Server 2012 设置虚拟机名称、安装位置 选择你的电脑核数 点击编辑虚拟机设置 点击CD/DVD(SATA) 使用ISO映像文件(M) 配置完之后点击确定 然后开启虚拟机 下一步&#xff1a; 点击现在安装&#xff1a…

【LeetCode】删除排序链表中的重复元素 II

目录 一、题目二、解法完整代码 一、题目 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,3,4,4,5] 输出&#xff1a;[1,2,5] 示例 …