简介
这系列文章计划有四篇,从基础的C++文本文件读写,到JSON文件读写,再到二进制文件读写。完整示例代码在本篇文章的最后。
ASCLL编码和UTF-8编码文件的读写
RapidJson解析ASCLL和UTF-8文件
数据持久化,关卡热重载
这篇文章只是简单探讨一下在数据存储的过程中需要考虑的一些问题。主要说的是本地存储,会涉及到少量网络、服务器、数据库之类的东西。
游戏数据
如果你之前没有接触过数据存储这方面,那么这里可以让你简单的了解一下,游戏中的数据到底是什么样子的。
俗话说耳听为虚眼见为实,我拿自己瞎写的例子肯定不行,所以找了英雄联盟的真实数据,下载地址也在文章末尾
下面是英雄联盟中英雄放逐之刃和爆破鬼才的属性数据,主要观察结构
{
"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 | 被动技能 |
我收起了比较复杂的节点,这里不关注游戏性的东西,只是简单观察结构,几个简单的节点足够了。
上面的Json文件一目了然,也不需要多说什么,如果你玩过英雄联盟,即使不会编程,也会感到十分熟悉,即使没有玩过英雄联盟,自己观察观察也能看出很多东西。
第一层标记了文件的基本信息,一个文件记录一个英雄,英雄有名字,头像,编号,背景介绍,分类,属性,技能等等数据。我只放了英雄的数据,这只不过是冰山一角,还有物品,地图等等其它资源,如果把这看成是一个数据库的表,那么还有很多关联表
游戏在本地储存的数据类型很多,这里关注的是,这些数据是怎么流入程序中?怎么去使用?
光看这些数据还不清楚,首先要搞清楚资源数据的导入的流程,也就是读的流程,读的时机也有很多,有些数据会在开始游戏时一次性读入,而有的需要在游戏中途读取,比如关卡,英雄。
假设英雄联盟游戏资源导入流程如下
-
开始:黑屏
-
程序首先进行本地化检测,根据配置表获得玩家设置的什么语言,获取该语言对应的数据路径
-
从数据中取出当前地图对应的加载页面的图片路径
-
打开
..\dragontail-10.16.1\10.16.1\data\zh_CN\champion\Riven.json
文件,读取10个英雄的数据,像上面那样的,创建英雄实例 -
使用英雄实例中的皮肤图片路径,在加载页面中显示10个英雄的皮肤图片
-
显示加载页面
-
从文件载入地图模型和物品图片,其它UI,以及本局中用到的粒子特效,骨骼动画
-
创建并显示UI,对UI进行属性绑定,有图片有文字(数字)。每个英雄实例下面都有主动技能,被动技能,生命值,蓝条,护甲,魔抗,攻速,攻击力,法强,移速,暴击率。
-
结束:显示游戏场景
这是一个读的场景,读取美术资源路径,再创建美术资源;读取英雄属性,创建英雄实例。其实就是在将数据恢复到内存中
用户配置文件就是一个写的场景。
当玩家在游戏中更改了语言,布局缩放,鼠标灵敏度等等设定之后,程序会写入到用户的配置文件中,然后再将配置文件上传到服务器覆盖之前的配置
这样的场景很多,这里就不多说了。说到最后,其实就是读和写,关键是怎么去读写,有哪些问题需要注意。接下来就是在数据存储上,需要关注的一些比较重要的方向,我自己总结的可能不太全,有错误有问题评论区留言
关注点
位置
物理位置
- 本地主机
- 远程服务器
软件位置
- 数据库
- 操作系统
本地数据不一定在本地,数据库不一定在服务器,要看站到哪个角度来考虑。
游戏和其他软件都差不多,小一点的,本机文件可能就足够了,大一点的加个数据库,再有网络功能的,云存档,聊天系统,商店系统,交易系统,那其实就和现实中的各种软件差不多了。其实就是在游戏的虚拟世界中嵌入各种现实中的软件
交换格式
这个概念一般只对文本文件有效。
比较熟悉的XML,JSON,CSV,普通文本。表格适合拿来记录某些配置
JSON在网络数据传输上非常流行,其实JSON和XML这样的格式都比较适合存储对象,可以作为开发调试的首选,但在这两者之间,JSON占用的空间要少得多。很多游戏的原始数据都是JSON格式。
基本上所有编程语言都有JSON库,你可以在www.json.org最下面找到各种语言的JSON库
这个格式的选择,用其中的一种还是多种都是可以的,也没有什么固定的方式。
文本格式
分为文本文件和二进制文件
简单来说,文本文件就是人读得懂的,上面的JSON,XML就是文本文件,二进制文件用文本打开之后人看着就是乱码。区别就在于保存形式是否为字符串
二进制的优点是更加高效,体积更小,缺点就是不方便阅读。
那么要想同时兼顾性能和可读性,一种可行的办法是写一段程序,将文本文件转化二进制文件。数据用文本的方式导入程序,程序再将文本格式转成二进制格式。最后的发布版本使用二进制格式。在第四篇文章中实现了这个功能,把一个1000字节的Json文件转换成36字节的二进制文件。
但这样做就需要额外编写转换程序和进行有效的版本控制。
编码格式
注意这里的编码方式指的是字符串的编码方式,比如UTF-8,ASCLL。对于数值不需要考虑
编码在开始的时候就要选好,如果只支持英文,那基本上不会有任何问题,因为都兼容ASCLL。
如果是其它语言就得先调好,不然后面得出大问题,动不动就乱码、报错。
内容
我们可以看一下Minecraft
的数据目录
数据的来源我已经记不清楚了,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\
下你会看到这样的文件结构,这就是一种标准的本地化语言的目录样式。
再打开繁体中文(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格式化的网站进行查看
示例代码下载
示例代码:Github