欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

(一)C++游戏开发-本地存储-介绍

程序员文章站 2022-03-27 17:09:19
简介这系列文章计划有四篇文章,从基础的C++文本文件读写,到JSON文件读写,再到二进制文件读写。这篇文章将以英雄联盟的例子简单探讨一下我对游戏中数据存储的认识后面会有一些存储的小游戏案例,但是不会涉及到游戏引擎,思路应该能够通用。主要说的是本地存储,所以不会涉及到太多网络、服务器、数据库之类的东西,但是有了本地存储的基础,可以很快建立远程存储英雄联盟英雄数据如果你之前没有接触过数据存储这方面,那么这里可以让你简单的了解一下俗话说耳听为虚眼见为实,我拿自己瞎写的例子肯定不行,所以找了英雄联盟的...

简介

这系列文章计划有四篇,从基础的C++文本文件读写,到JSON文件读写,再到二进制文件读写。

这篇文章将以英雄联盟的例子简单探讨一下我对游戏中数据存储的认识,进行一个总结

后面会有一些纯C++的有关存储的小游戏案例,但是不会涉及到游戏引擎,思路应该能够通用。

主要说的是本地存储,所以不会涉及到太多网络、服务器、数据库之类的东西,但是有了本地存储的基础,可以很快建立远程存储

观察真实的游戏数据

如果你之前没有接触过数据存储这方面,那么这里可以让你简单的了解一下

俗话说耳听为虚眼见为实,我拿自己瞎写的例子肯定不行,所以找了英雄联盟的真实数据,下载地址在文章末尾

下面是英雄联盟中英雄放逐之刃和爆破鬼才的属性数据,主要观察结构

{
    "type":"champion",
    "format":"standAloneComplex",
    "version":"10.16.1",
    "data":{
        "Riven":{
            "id":"Riven",
            "key":"92",
            "name":"放逐之刃",
            "title":"锐雯",
            "image":Object{...},
            "skins":Array[13],
            "lore":"<p>曾担任诺克萨斯军队剑士长的锐雯,如今……</p>",
            "blurb":"<p>曾担任诺克萨斯军队剑士长的锐雯,如今……</p>",
            "allytips":[
                "- 锐雯的折翼之舞会朝着你在施法时的鼠标悬停位置施放。如果你想用这招穿插你的敌人,请确保你的敌人们处在锐雯和你的鼠标悬停位置之间。",
                "- 锐雯血少防低,但作为补偿,她的连招爆发很高。折翼之舞和震魂怒吼可以用来切入战斗,勇往直前可以用来逃跑和反骚扰。"
            ],
            "enemytips":[
                "- 锐雯的机动性非常出色,但单个技能不能使她移动得太远。在技能间隙使用束缚/沉默会极大削减她的影响。",
                "- 锐雯的所有伤害都是物理伤害,如果对面的锐雯已经失控了,那么优先堆护甲。",
                "- 锐雯擅长同时对付多个近战,如果要联手对付她,请在她放完她的整套连招前不要都进入肉搏范围。"
            ],
            "tags":[
                "Fighter",
                "Assassin"
            ],
            "partype":"无",
            "info":{
                "attack":8,
                "defense":5,
                "magic":1,
                "difficulty":8
            },
            "stats":{
                "hp":560,
                "hpperlevel":86,
                "mp":0,
                "mpperlevel":0,
                "movespeed":340,
                "armor":33,
                "armorperlevel":3.2,
                "spellblock":32.1,
                "spellblockperlevel":1.25,
                "attackrange":125,
                "hpregen":8.5,
                "hpregenperlevel":0.5,
                "mpregen":0,
                "mpregenperlevel":0,
                "crit":0,
                "critperlevel":0,
                "attackdamage":64,
                "attackdamageperlevel":3,
                "attackspeedperlevel":3.5,
                "attackspeed":0.625
            },
            "spells":[
                Object{...},
                Object{...},
                Object{...},
                Object{...}
            ],
            "passive":Object{...},
            "recommended":[
                Object{...},
                Object{...},
                Object{...},
                Object{...},
                Object{...},
                Object{...}
            ]
        }
    }
}
{
    "type":"champion",
    "format":"standAloneComplex",
    "version":"10.16.1",
    "data":{
        "Ziggs":{
            "id":"Ziggs",
            "key":"115",
            "name":"爆破鬼才",
            "title":"吉格斯",
            "image":Object{...},
            "skins":Array[9],
            "lore":"炸弹越大越好,引线越短越好,带着...",
            "blurb":"炸弹越大越好,引线越短越好,带着...",
            "allytips":[
                "- 即使远离团战发生地,你也可以在远处用【R科学的地狱火炮】来帮助队友。",
                "- 用【E海克斯爆破雷区】来减速你的敌人,会使你的其它技能更易命中。",
                "- 用【W定点爆破】来穿墙,可以在追杀或逃命时如虎添翼。"
            ],
            "enemytips":[
                "- 别踩到吉格斯的地雷上!它们会使你减速,并且会让吉格斯的其它技能更易命中你。",
                "- 吉格斯的很多技能冷却时间都很长。可以在他刚用过技能时发起攻击。",
                "- 吉格斯的终极技能,科学的地狱火炮,会在爆炸的中心区域造成更多伤害。"
            ],
            "tags":[
                "Mage"
            ],
            "partype":"法力",
            "info":{
                "attack":2,
                "defense":4,
                "magic":9,
                "difficulty":4
            },
            "stats":{
                "hp":536,
                "hpperlevel":92,
                "mp":480,
                "mpperlevel":23.5,
                "movespeed":325,
                "armor":21.544,
                "armorperlevel":3.3,
                "spellblock":30,
                "spellblockperlevel":0.5,
                "attackrange":550,
                "hpregen":6.5,
                "hpregenperlevel":0.6,
                "mpregen":8,
                "mpregenperlevel":0.8,
                "crit":0,
                "critperlevel":0,
                "attackdamage":54.208,
                "attackdamageperlevel":3.1,
                "attackspeedperlevel":2,
                "attackspeed":0.656
            },
            "spells":[
                Object{...},
                Object{...},
                Object{...},
                Object{...}
            ],
            "passive":Object{...},
            "recommended":[
                Object{...},
                Object{...},
                Object{...},
                Object{...},
                Object{...},
                Object{...},
                Object{...}
            ]
        }
    }
}

中英文对照表

英文 中文
image 英雄头像
skins 英雄皮肤
lore/blurb 简介,背景
allytips 特点
enemytips 小技巧
tags 英雄类型
partype 蓝条类型
info 对英雄的衡量
states 英雄属性
spells 主动技能
passive 被动技能

我收起了几个比较复杂的节点,因为我们这里只是单纯观察结构,几个简单的节点足够了。

实际上感觉也不需要我多说什么,如果你玩过英雄联盟,即使不会编程,也会感到十分熟悉,即使没有玩过英雄联盟,自己观察观察也能看出很多东西。我只放了英雄的数据,这只不过是冰山一角,还有物品,地图等等其它资源,如果把这看成是一个数据库的表,那么还有很多关联表

光看这些数据可能还不清楚,首先要搞清楚这些个资源的导入的流程,也就是读的流程

假设英雄联盟游戏资源导入流程如下

开始:黑屏

程序首先根据配置表获得玩家设置的什么语言,比如简体中文

读取本地化目录下简体中文版的数据

从数据中取出当前地图对应的加载页面的图片路径

打开..\dragontail-10.16.1\10.16.1\data\zh_CN\champion\Riven.json,读取了瑞雯的所有数据,
读取了所有盖伦的数据

现在读取了当前场上选择的10个英雄的英雄数据

根据英雄数据创建英雄实例

使用英雄实例中的皮肤图片路径,在加载页面中显示10个英雄的皮肤图片

显示加载页面

从文件载入地图模型

从文件载入模型

从文件载入粒子特效

从文件载入骨骼动画

从文件载入物品图片

创建UI

对UI进行属性绑定,有图片有文字(数字)。每个英雄实例下面都有主动技能,被动技能,生命值,蓝条,护甲,魔抗,攻速,攻击力,法强,移速,暴击率。

创建模型

。。。

结束:显示游戏场景

这是一个读的场景,读取美术资源路径,再创建美术资源;读取英雄属性,创建英雄实例。

用户配置文件就是一个写的场景。
当玩家在游戏中更改了语言,布局缩放,鼠标灵敏度等等设定之后,程序会写入到用户的配置文件中,然后再将配置文件上传到服务器覆盖之前的配置

这样的场景很多,这里就不多说了。

接下来就是在数据存储上,需要关注的一些重要问题,我自己总结的可能不太全,有错误有问题评论区留言

关注点

位置

从物理上看分成本地主机和远程服务器

从在计算机上面的位置来看,又分成数据库和非数据库,非数据库就是直接在操作系统的文件系统里面存,数据库又分关系型数据库和非关系型数据库。

这里指的数据主要指的是游戏变量,其它静态数据,比如图片和这里的英雄数据,大部分都在本地主机上,有时候如果资源太多,一时半会又用不着,那么可能先存在服务器上,玩家需要的时候再进行下载。

我感觉其实游戏软件和普通软件都差不多,小程序本机JSON文件就足够了,再大一点的弄一个云存档,联机的话,聊天系统,商店系统,交易系统,那其实服务端就和现实中的各种在线商务系统差不多了,就是在游戏的虚拟世界中嵌入各种现实中的软件,也就是说只要现实里面有的软件,稍加修改就能搬进游戏里面。

交换格式

比较熟悉的XML,JSON,CSV,普通文本

CSV表格、适合拿来记录某些配置

JSON和XML这样的格式其实比较适合存储对象,可以作为开发调试的首选,在这两者之间,JSON占用的空间要少得多,很多游戏的原始数据都是JSON格式,JSON有多好用就不用多说了,在后面的教程中将讲解C++的一些JSON库的具体用法

基本上所有编程语言都有JSON库,你可以在www.json.org最下面找到各种语言的JSON库
(一)C++游戏开发-本地存储-介绍

这个格式的选择,用其中的一种还是多种都是可以的,也没有什么固定的方式,觉得什么好用就用什么。

文本格式

分为文本文件和二进制文件

简单来说,文本文件就是人读得懂的,二进制数据打开用文本打开之后人看着就是乱码。

二进制的优点是更加高效,体积更小,缺点就是不方便阅读。

比较好的解决办法是,数据用文本的方式导入程序,程序再将文本格式转成二进制格式,最后打包的时候不包含文本格式,但这样做就需要额外编写转换程序和进行有效的版本控制。在后面的文章里面将实现这些个功能。

编码格式

编码格式太多了,举两个例子,UTF-8,ASCLL

编码在开始的时候就要选好,如果只支持英文,那基本上不会有任何问题,如果是其它语言就得先调好,不然后面得出大问题,动不动就乱码、报错。

内容

我们可以看一下Minecraft的数据目录
(一)C++游戏开发-本地存储-介绍
数据的来源我已经记不清楚了,Minecraft Wiki应该有

好了,从这个文件夹的目录就能看出来,音频、视频、粒子特效,材质、语言包、动画、模型、游戏数据、选项设置、缓存数据等等。

这个就因游戏而异了,有的游戏可能就一个本地存储的排行榜,有的可能有一套完整的账号数据库。

协议

这里“文件协议”其实和网络协议很类似,或者可以理解成一种编码方式,或者说是一种标准,

方便在导入和导出的时候进行解码和编码。

比如前面的每个英雄数据头部的type注明了这个文件是一个英雄的信息

    "type":"champion",
    "format":"standAloneComplex",
    "version":"10.16.1",

而在..\dragontail-10.16.1\10.16.1\data\zh_CN\item.json文件中头部,注明了这个文件是Item的信息

{
    "type":"item",
    "version":"10.16.1",
    "basic":Object{...},
    "data":{
        "1001":{
            "name":"速度之靴",
            "description":"<groupLimit>限购1个鞋类装备。</groupLimit><br><br><unique>唯一被动—强化移动:</unique>+25移动速度",
            "colloq":";suduzhixue;sdzx",
            "plaintext":"略微提升移动速度",
            "into":Array[8],
            "image":Object{...},
            "gold":Object{...},
            "tags":Array[1],
            "maps":Object{...},
            "stats":{
                "FlatMovementSpeedMod":25
            }
        },
        "1004":{
            "name":"仙女护符",

当程序读取之后,程序怎么知道这是个什么类的数据,靠的就是文件/文件夹的名称,或者文件内部的某些标记。这儿就告诉程序,这是一个英雄、物品的数据,然后用什么格式去读取,这个数据是什么版本的。读取之后实例化一个Hero类还是一个Item类。总之,就是建立类和JSON文件之间的映射关系。

这里看到的静态数据只是游戏数据的一部分,只是游戏中的数据协议的一种,从种类上来看,各种格式的数据都有自己的协议,比如模型、图片、视频、文本等等数据,在导入和导出的时候会用到所有数据。

如果是商业引擎那就没有必要考虑太多,各种文件的导入和格式都给你写好了,用就完事了。否则的话,各种协议可以由程序员制定的,这个问题如果要深入探讨的话,也

本地化

对各种语言的支持

在路径..\dragontail-10.16.1\10.16.1\data\下你会看到这样的文件结构
(一)C++游戏开发-本地存储-介绍

再打开繁体中文(zh_TW)文件夹下的Riven.json文件
..\dragontail-10.16.1\10.16.1\data\zh_TW\champion\Riven.json

{
    "type":"champion",
    "format":"standAloneComplex",
    "version":"10.16.1",
    "data":{
        "Riven":{
            "id":"Riven",
            "key":"92",
            "name":"雷玟",
            "title":"破刃放逐者",
            "image":Object{...},
            "skins":Array[13],
            "lore":"曾經是諾克薩斯頂尖劍士的雷玟現在流放於過去她亟欲征服的土地。她曾…",
            "blurb":"曾經是諾克薩斯頂尖劍士的雷玟現在流放於過去她亟欲征服的土地。她曾…",
            "allytips":Array[2],
            "enemytips":Array[3],
            "tags":[
                "Fighter",
                "Assassin"
            ],
            "partype":"無",
            "info":{
                "attack":8,
                "defense":5,
                "magic":1,
                "difficulty":8
            },
            "stats":{
                "hp":560,
                "hpperlevel":86,
                "mp":0,
                "mpperlevel":0,
                "movespeed":340,
                "armor":33,
                "armorperlevel":3.2,
                "spellblock":32.1,
                "spellblockperlevel":1.25,
                "attackrange":125,
                "hpregen":8.5,
                "hpregenperlevel":0.5,
                "mpregen":0,
                "mpregenperlevel":0,
                "crit":0,
                "critperlevel":0,
                "attackdamage":64,
                "attackdamageperlevel":3,
                "attackspeedperlevel":3.5,
                "attackspeed":0.625
            },
            "spells":Array[4],
            "passive":Object{...},
            "recommended":Array[6]
        }
    }
}

两个瑞雯的路径分别是

..\dragontail-10.16.1\10.16.1\data\zh_CN\champion\Riven.json
..\dragontail-10.16.1\10.16.1\data\zh_TW\champion\Riven.json

现在你可能已经发现了,两个Riven.json,拥有完全相同的文件结构。

在游戏中,使用的就是一种映射的方法,比如一个显示英雄名字的控件,不是直接用某个文件中的名称去设置,而是在中间加了一层Map。

把控件的文本和这个Map中Key对应的Value进行绑定,在更换语言的时候,只需将Value换成新语言对应的数据包下面对应的Value,不用直接去更改控件的Text,自动就完成了语言的切换。

一般商业引擎都自带的有,现成的库网上也有,自己做一个简单一点的也是可以的

这只是本地化的一种实现,还有的本地化策略,虽然也是使用映射的方法,但是映射的双方不一样。比如可以把不需要本地化数据和需要本地化的数据分开,不需要本地化的数据就一份,像Minecraft里面一样,使用若干个语言包,每个语言包里面包含某个Key的本地化语言。

本地化的概念并不仅仅局限于显示的语言,时间日期,货币,数量等等细节都是本地化的操作,如果放到C++里面,刚学的时候,肯定以为std::cout能打印个中文,std::fstream读写个文件就完事大吉了,实际上后面还有个本地化等着呢。

数据来源

RiotGames英雄联盟开发文档,下载之后,包含所有的静态数据,感兴趣可以自己去下载

下载之后的文件因为没有缩进和换行不方便阅读,可以随便找一个JSON格式化的网站进行查看

本文地址:https://blog.csdn.net/qq_30919603/article/details/108254934