UE5官方文档(第一人称射击游戏教程)解读 第十章
又到了我们学习的时间了。
其实我很喜欢写教程的时间,完整的思考和复盘,有种很充实的感觉,现实里面总是有很多烦心事,写教程的时候就不会去乱想。
如果有人能够因为我写的教程受益,我会很开心。
第十章了,也感谢所有点赞收藏评论的各位,我一直是有拖延症,想着可能还有人等着看,所以每天一有空就开始写教程。话说,要是这个教程写完了,我要不要写一个UE5渲染教程?但我写的可能还是根据其他优秀教程的补充,毕竟我不是专业人士。
管理物品和数据
Coder 05 Manage Item and Data in an Unreal Engine Game | Unreal Engine 5.7 Documentation | Epic Developer Community
在上一章,我们彻底完成了输入部分的学习那么这一章,我们看标题也知道是背包系统。
在开始前,我们思考一下,如何设计一个背包系统?
请手动在纸上写写尝试。
我们拾取到一个东西的时候,我们要把它放入背包(不知道大家知不知道MC的怪力史蒂夫,可以背n多组石头这个笑话),我们可以知道,背包系统的显示一般是这样:很多个格子,格子里放着物品,右下角标着物品数。所以如果我们拾取的东西如果在背包里已经有了此种类,那么就往对应物品的数量变量做加法,如果是丢物品就做减法,最后,如果数量为零,就删除背包里面的这个物品种类。
开始本章学习前,我们要知道一个事情,我们这里不会出现背包的显示界面开发,我们只会涉及背包系统的底层逻辑开发,请注意。
游戏中的数据组织
还是跟着官方文档过一遍。
大家可能不太能理解官方文档里面的“数据驱动型GamePlay”概念。
那我们一步一步来。(但我真的完全不是科班出身,所以可能会有很多说错的地方)
首先我们的电脑从硬件层面上来说,就是存储数据,传递数据,根据指令运算的“计算机”。我们的游戏程序,会实时读取很多资源实时加载使用。
为了防止资源混淆与冗余,譬如文档里面提到的角色对话数据,我们肯定是不能把角色对话数据一股脑全写入角色类,然后用if(角色处在某个剧情)就现实该对话,但是这样的话,我们运行时的宝贵内存就完全浪费了!虽然省去了从硬盘读取资源的时间!但是不用的对话数据也在此时占着茅坑不拉屎!
所以我们最好的方法,就是把对话资源独立成一个单独的文件,然后让角色类实时读取,虽然增加了从硬盘读取资源文件的的时间,但是我们节省了很多宝贵的内存。同时,分离时设计方便我们修改对话资源而不影响角色类本身。(分离式设计思想真的是无处不在,或者说,优化本身分成两个大部分,便于开发者的优化,对于程序本身的性能优化)
如果可以的话,大家可以去了解电脑里所有和存储相关的硬件,然后根据它们到运算相关硬件的距离思考。
所以在我们本节,物品数据会被按照一定规律设计成一个单独的资源文件,然后根据这个规律去在游戏运行时读取显示。
数据驱动型Gameplay元素
还是先跟着官方文档过一遍概念。
首先我们如何去理解“物品数据结构体”这个概念?
假设我们有一个物品,它的基础数据是这样的:物品名字,物品类型,物品用处简述。
struct 物品
{
string 物品名字;
enum 物品类型;
string 物品用处简述;
};
请不要纠结这个写得很烂的伪代码,毕竟只是概念演示,没有IDE我已经不会写正常代码了……(请思考一下为什么物品类型用的是枚举)
思考一下,我们这个结构体占的总长度是68字节(一般string是32字节,enum是4字节)
如果我们一个物品文件存了3个物品,总长度是204字节,存储是按照我们之前设计好的结构体。
如果我们想要从这个物品文件分开读取物品数据该怎么做?
对喽,就是按照我们已知的,设计好的规律去反向读取,起始字节读68个字节的数据就是我们第一个物品的数据(然后在这段数据里面32字节读物品名字数据,4字节读物品类型,32字节读物品用处简述)……以此类推,我们就可以完美地读取完所有数据,只要知道我们对于此数据的设计。
说起来,从硬盘读取数据加载的行为称作反序列化,而把数据写入硬盘存储的过程叫做序列化。
怎么去理解我们的“数据表资产”?我们可以把数据表资产简单理解为我们前面提到的,存储了所有物品数据的东西。
但它不是背包!它只是存储了所有物品数据!
而我们的背包是记录我们有多少种物品,每个物品有多少个,显示的时候实时读取数据资产表,加载对应物品的名字描述类型。
这也是一个分离式设计。
定义物品数据
还是跟着官方文档来一遍,这部分没什么好讲的。
创建结构体的头文件容器
还是先跟着官方文档来一遍。
大家有没有发现一个很神奇的东西,在我们之前写的角色类,游戏模式类,都是继承了某些基础类,在头文件那里会有我们的core文件,父类文件,最后一个是生成文件。
我们在这里可以看见,core有,父类文件没有是因为我们这里本来就没继承啥,但是,生成文件没有了。
而在我们的官方文档里面,由于我们的这个结构体需要能被数据表资产获取,所以需要引入相关头文件,但是,我们在游戏运行时,数据读取是实时的,也需要规律,所以我们要运行时知晓类信息,也就离不开生成文件。
然后前向声明就不赘述,如果不懂,写到后面就懂了。
定义物品属性
还是跟着官方文档来一遍
这是个啥意思呢,意思是创建了一个枚举类,名字是物品类型,在编辑器里面的表现是选择(工具or消耗品,只能在里面选一个,后面会有演示)。
UMETA里面什么意思呢?意思是在编辑器显示的时候对应设置用这两个双引号的名字。
但是我们还可以看见,枚举前面也有个宏标记,毕竟运行时获取类信息真是无处不在。
定义物品属性
还是根据官方文档来一遍。
我们在很早很早之前就讲过,结构体也是需要宏标记的。
可是我好像漏掉了一个东西GENERATED_BODY宏,运行时获取类信息,需要生成文件和各种宏标记同时运作,包括这个,详细的我就不讲了,因为我所知道的也只有这些。
但是我们看见枚举里面没有这个宏,为什么呢?枚举其实就相当于一个变量(其本质是整型变量),但是结构体和类里面存储了许许多多变量,登记的时候两者的区别相当于这样:我写了一个枚举变量叫xxxx,我写了一个结构体xxxx里面有一个变量叫xxxx下一个叫xxxx……。
回到我们的结构体本身,结构体里面存储了我们的物品名字,还有物品描述。
作业:FString和FText的区别?为什么这里要用FText?
创建物品数据结构体
还是跟着官方文档来一遍。
我们可以看见,这个结构体继承了FTable(表)Row(行)Base类,也就是说我们这个FItemData类就是我们的数据表资产的行格式。
一行里面有四个数据(等会儿可以打开查看),第一个是ID,第二个是物品类型,第三个是物品相关文本(物品名称,物品描述),第四个是目前不知道干嘛的UItemDefinition指针变量ItemBase。
其实大家还是无法理解这个ID是什么,因为我们第三个之前写好的结构体里面明明打包好了物品名称,为什么还需要一个唯一的ID。
假设我们在场景里面有三朵一模一样的花,它们的名字描述类型,所有数据都一样,我们怎么存入资产表呢?
ID的作用此刻就体现出来了,我们可以叫它们花朵一号,花朵二号,花朵三号。不过,拾取到背包里面肯定就只识别物品名称了。但我们这里就是为了在它们没进背包时在场景区分。
总结一下,到目前为止,我们设置的是资产表的格式,而资产表的格式从某种层面上来说是显示给开发者看的,方便开发,真正的数据存在了我们第四个指针变量指向的数据里面。查找的时候根据ID,读取的时候读取指针,数据表行里面显示的其他数据本身就是给开发者看的。
编译DataAsset物品定义
还是根据官方文档过一遍。
// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Data/ItemData.h" #include "ItemDefinition.generated.h" /** * Defines a basic item with a static mesh that can be built from the editor. */ UCLASS(BlueprintType, Blueprintable) class FIRSTPERSON_API UItemDefinition : public UDataAsset { GENERATED_BODY() public: // The ID name of this item for referencing in a table row. UPROPERTY(EditAnywhere, Category = "Item Data") FName ID; // The type of this item. UPROPERTY(EditAnywhere, Category = "Item Data") EItemType ItemType; // Text struct including the item name and description. UPROPERTY(EditAnywhere, Category = "Item Data") FItemText ItemText; // The Static Mesh used to display this item in the world. UPROPERTY(EditAnywhere, Category = "Item Data") TSoftObjectPtr<UStaticMesh> WorldMesh; };我们之前说过,我们资产表里面的各项都是为了方便开发者查看,这个类里面包装的才是我们真正的数据。我们多包装了一个静态网格体指针,可以理解为是模型数据填充位。
为什么是指针呢?我们说过是为了优化,实时读取,而不是急头白脸把所有数据一下子载入,而是先载入指针,然后根据需要,通过指针读取到需要的数据。
可能大家还是无法理解一件事情,这样一个结构体,为什么我们需要继承UDataAssest类。
因为继承了这个类,你就可以按照你写的模板在编辑器里面编辑这份数据了,不需要写代码了。
这部分作为一个作业了解。
创建数据资产实例
还是跟着官方文档来一遍。
其实过程就是写一份数据,然后把这份数据填充到数据表。
没什么好讲的了,今天的学习到此为止。
