独立游戏《星尘异变》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,也都可以循环,首先来看窗口模式的修改,如果全屏和窗口化之间有切换,那么当前的分辨率选项也要跟着改变,这里就使其变为对应分辨率数组的最后一个元素

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

4.应用设置

        分辨率和窗口模式设置的接口都在GameUserSetting中,这里要说的是在应用后生成一个UI,提示玩家确认否则在15秒后还原修改,因为如果分辨率设置错误的话,可能带来严重后果

序列中Then2后面的节点:

当玩家确认更改时,更新上一次应用的设置为当前设置:

玩家撤销时还原设置,调用的函数只是将上面应用设置的部分去掉了生成确认UI:

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

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

相关文章

在国产芯片上实现YOLOv5/v8图像AI识别-【1.3】YOLOv5的介绍及使用(训练、导出)更多内容见视频

本专栏主要是提供一种国产化图像识别的解决方案&#xff0c;专栏中实现了YOLOv5/v8在国产化芯片上的使用部署&#xff0c;并可以实现网页端实时查看。根据自己的具体需求可以直接产品化部署使用。 B站配套视频&#xff1a;https://www.bilibili.com/video/BV1or421T74f 数据…

5.5 软件工程-系统测试

系统测试 - 意义和目的 系统测试 - 原则 系统测试 - 测试过程 系统测试 - 测试策略 系统测试 - 测试方法 真题 系统测试 - 测试用例设计 黑盒测试 白盒测试 真题 系统测试 - 调试 系统测试 - 软件度量 真题

vue 实现打字机效果

打字机效果组件&#xff0c;支持像打字机一样模仿键入文本。支持vue 插值语法和表格等打印 ps: 灵感来着于vue-type-writer 但是 这个组件过于简单 就自己整了一个 一、预览 二、代码 组件&#xff1a; <template><div :style"{ visibility: visibility }&qu…

AI 模型本地推理 - YYPOLOE - Python - Windows - GPU - 吸烟检测(目标检测)- 有配套资源直接上手实现

Python 运行 - GPU 推理 - windows 环境准备python 代码 环境准备 FastDeploy预编译库下载 conda config --add channels conda-forge && conda install cudatoolkit11.2 cudnn8.2 pip install fastdeploy_gpu_python-0.0.0-cp38-cp38-win_amd64.whlpython 代码 impo…

虚拟机的状态更新

文章目录 虚拟机的更新一、检查虚拟机的配置1.已连接状态2. 保证镜像源挂载 二、进行更新三、其余事项 虚拟机的更新 虚拟机的更新是确保系统软件包和库的更新&#xff0c;以获得最新的修复和改进&#xff1b;如果长期没有打开单机或者集群&#xff0c;可以考虑先进行一次更新…

Artix7系列FPGA实现SDI视频编解码,基于GTP高速接口,提供3套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本方案在Xilinx--Kintex系列FPGA上的应用本方案在Xilinx--Zynq系列FPGA上的应用 3、详细设计方案设计原理框图SDI 输入设备Gv8601a 均衡器GTP 高速接口-->解串与串化SMPTE SD/HD/3G SDI IP核BT1120转…

Docker容器下安装Matlab,无需挂载

Matlab的安装需要这些文件 传入ubuntu后&#xff0c;改过相关的文件权限后&#xff0c;发现还是无法挂载 这有可能是docker的安全管理策略导致容器不能挂载&#xff0c;因此采用不挂载形式&#xff0c;直接解压的方式安装Matlab 1.将iso改成zip&#xff0c;并解压 2.解压rar文件…

电机泵盖机器人打磨去毛刺,选德国进口高精度主轴

机器人打磨去毛刺该如何选择主轴呢&#xff1f;首先我们需要考虑的是工件的材质&#xff0c;电机泵盖通常使用铸铁、不锈钢、合金钢等金属材质&#xff0c;因此这类保持的硬度较高&#xff0c;一般会选择功率、扭矩较大的德国进口高精度主轴Kasite 4060 ER-S。 Kasite 4060 ER-…

设计分享—国外网站设计赏析

今天还是给大家分享一些国外的网站设计案例&#xff5e; 蓝蓝设计是一家专注而深入的界面设计公司&#xff0c;为期望卓越的国内外企业提供卓越的大数据可视化界面设计、B端界面设计、桌面端界面设计、APP界面设计、图标定制、用户体验设计、交互设计、UI咨询、高端网站设计、平…

JVM和类加载机制-01[JVM底层架构和JVM调优]

JVM底层 Java虚拟机内存模型JVM组成部分五大内存区域各自的作用虚拟机栈(线程栈)栈帧内存区域 本地方法栈程序计数器为什么jvm要设计程序计数器&#xff1f; 堆方法区 JVM优化-堆详解JVM底层垃圾回收机制jvm调优工具jvisualvm.exeArthas工具使用 Java虚拟机内存模型 JVM跨平台原…

SpringBoot之全局异常处理

默认情况下的异常现象 创建一个接口 &#xff08;接口需要传递参数key&#xff09; RestController RequestMapping("/exception") public class ExceptionController {GetMapping("/accept")public String acceptKey(RequestParam("key") Str…

C语言第5天作业 7月16日

目录 1.求1000以内所有的质数。 2.有1、2、3、4个数字&#xff0c;能组成多少个互不相同且无重复数字的三位数&#xff1f;都是多少&#xff1f; 3.猴子吃桃问题 4.判断最大值 1.求1000以内所有的质数。 质数&#xff1a;只能够1和它本身整除 #include <stdio.h> in…

Camera Raw:首选项

Camera Raw 首选项 Preferences提供了丰富的配置选项&#xff0c;通过合理设置&#xff0c;可以显著提升图像处理的效率和效果。根据个人需求调整这些选项&#xff0c;有助于创建理想的工作环境和输出质量。 ◆ ◆ ◆ 打开 Camera Raw 首选项 方法一&#xff1a;在 Adobe Bri…

Linux系统学习日记——vim操作手册

Vim编辑器是linux下的一个命令行编辑器&#xff0c;类似于我们windows下的记事本。 目录 打开文件 编辑 保存退出 打开文件 打开 hello.c不存在也可以打开&#xff0c;保存时vim会自动创建。 效果 Vim打开时&#xff0c;处于命令模式&#xff0c;即执行命令的模式&#x…

解决IDEA 中出现已有类、函数找不到的情况

缓存导致部分索引失效&#xff0c;需要刷新缓存并重启idea即可 1、File > Invalidate Cache / Restart... 2、Invalidate and Restart

聊聊常见的分布式ID解决方案

highlight: xcode theme: vuepress 为什么要使用分布式ID&#xff1f; 随着 Web 开发技术的不断发展&#xff0c;单体的系统逐步走向分布式系统。在分布式系统中&#xff0c;使用分布式 ID(Distributed IDs)主要是为了在没有单点故障的情况下生成唯一标识符。这些唯一标识符在很…

C++【OpenCV】图片亮度色度归一化

#include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp> #include <iostream>using namespace cv; using namespace std;int main() {Mat image imread("SrcMF.jpg");// 灰度、Gamma归一化亮度cv::Mat m_gray;cv::cvtColor(image, m_gra…

Linux-CentOS7忘记密码找回步骤

虚拟机版本 一、进入开机页面&#xff0c;先按上下&#xff08;↑↓&#xff09;键&#xff0c;以免系统自动启动。 二、按“e”键进入编辑页面,找到如下图位置&#xff0c;输入&#xff1a;init/bin/sh 按CTRLX 进入单用户模式。 三、 输入 mount -o remount,rw / 然后按 ent…

【ARMv8/v9 GIC- 700 系列 2 -- GIC-700 上电控制寄存器 GICR_PWRR】

请阅读【ARM GICv3/v4 实战学习 】 文章目录 GIC-700 上电GICR_PWRR 寄存器字段介绍GICR_PWRR 功能说明GICR_PWER 代码配置GICR_PWRR 使用场景GICR_PWRR 注意事项GIC-700 上电 GICR_PWRR(功耗寄存器)是ARM GICv4架构中用于控制GIC-700是否可以关闭电源的寄存器。它通过几个位…

Go语言并发编程-Goroutine调度

goroutine 概念 在Go中&#xff0c;每个并发执行的单元称为goroutine。通常称为Go协程。 go 关键字启动goroutine go中使用关键字 go 即可启动新的goroutine。 示例代码&#xff1a; 两个函数分别输出奇数和偶数。采用常规调用顺序执行&#xff0c;和采用go并发调用&…