和一般应用软件有点不同,大部份游戏软件都需要使用大量的游戏数据(game data)──或称为资源(resource)、资产(game asset, 但通常asset包括数据的原始格式, 不是最终运行游戏所需的数据)。在制作游戏时,如何管理这些数据是一个非常重要的问题。曾经看过和使用过不同的方案,现在归回原点,分析基本的需求,随笔记录笔者所选的方案思路。
游戏数据管理的重要性
如果从游戏软件的产出(deliverables) 来分析,一个游戏软件可以分为三部份:
- 游戏引擎: 比较固定的、和游戏性无直接关系的程式
- 游戏唯读数据: 脚本、图像、音效、关卡、影像
- 游戏读写数据: 游戏存档、游戏设定、玩家自制内容
从现今的游戏实际容量来看,游戏引擎可能占1~10MiB,游戏读写数据10KiB~10MiB,其余以GiB为单位的全是唯读数据。以开发人员来计算,开发引擎可能是数人,但制作那些唯读数据的队伍是以百计的,主要是游戏性程式设计师、人物美工、场境美工、关卡设计师等等。 本文主要谈游戏唯读数据,以下「数据」或「游戏数据」皆指「游戏唯读数据」。
游戏数据可以有几多笔?
接着笔者随便估计一个"小"游戏需要的数据笔数(笔数在下节定义)
- 20 关卡 × (200关卡贴图 + 100关卡模型 + 200游戏物件) = 10000
- 40 人物 × (3人物贴图+ 2人物模型 + 20人物动画) = 1000
- 200视觉效果 × (5效果贴图 + 5动画) = 1000
- 20使用者接口 × 25贴图 = 500
- (100物件脚本 + 80人物脚本 + 20使用者介面脚本) × 5原始代码文件 = 1000
- 180音效 + 20音乐 = 200
- 总和 = 13,700
以这个推算游戏的数据笔数,其数量级一般在104 至106。笔者觉得极限只是会到107,因为大量数据是模型、贴图、动画等,平均值不会少于10KiB,而107 × 10KiB 已经是10GiB。
游戏数据的概念分析
笔者心目中的游戏数据是有以下特点
- 唯一性(uniqueness): 每笔数据有它唯一的标识符(identifier),透过一个标识符可以读取一笔数据。
- 不可分割(atomicity): 当把一些数据分割成多笔数据是没意义的,就算是一笔数据。例如假设一张贴图不会读取、使用其中一部份,则视为一笔数据。
- 依存关系(dependency): 一笔数据可能会靠identifiers引用其他数据。这和上不可分割属性也有关系,因为是用identifier依存某一个数据,而不是该数据内的一部份。笔者未想到会有循环的依存关系,暂时把依存关系当作一个Directed Acyclic Graph (DAG)。
一般文件系统的文件,和这里的数据有点不同。文件的路径可以视为Identifier,但很多文件的内容是可以再分割的,而且没有显性(explicit)的依存关系。
选择标识符
笔者看过和想到的数据标识符(identifier)有:
- 路径 (path): 例如 Texture/wall.jpg
- 统一资源定位符(URL): 例如Texture/wall.jpg、http://www.mysite.com/news.jpg
- 整数: 例如 0x54AF4C58
- 全局唯一标识符(GUID): 例如{3F2504E0-4F89-11D3-9A0C-0305E82C3301}
路径
档案系统的路径可能是最直觉的数据标识符。我以前做过的引擎,和很多商用引擎也会使用文件和路径这个表达方式。路径通常是相对于某一个目录、或一个压缩文件(例如id公司的引擎会把档案压缩在一个zip文件里)。
路径的好处是和我们日常用的操作系统管理档案的方式一样。层阶式(hierarchical)的目录结构让使用者可以自行分类管理,你亦可以历遍(traverse)目录结构,例如读取某一目录的*.lua。它实作简单,因为操作系统已提供所需功能。也可以使用一些现存的版本管理工具去直接管理这些数据,如SVN、Perforce等。
路径的坏处包括: 大小写问题、多国文字问题、储存效率差、运行效率差。前二者太概都容易明白,不再详述。储存效率差是指用字串去记录每个标识符,包括引用时的标识符都会花费不少空间,而且路径(不单是档案名)的长度可以很长(例如可能要256 位元组)。而运行效率差是因为比较两个路径慢,计算一个路径的hash code (例如用路径identifier做hash_map或hash_set 的key)也很慢,並且不是常数速度。在游戏运行期,这些路径文字资讯是冗余的,因为玩家不会看到。
统一资源定位符
统一资源定位符(Uniform Resource Locator, URL)算是路径的延伸。这延伸的好处是它是一个标准(不会有平台相关、大小写、多国语言等问题),而且有些XML里也会用URL作为引用的标识符。另外,可以选择性支持不同的协定(protocol),例如透过http存取互联网上的资源。其他坏处和路径差不多。
整数
整数是最简单的标识符。通常关联式数据库(relational database)的表(table),都以一个整数栏作为列的主键(primary key)。相对前两种标识符,整数的好处是储存量小而速度快。整数标识符的缺点是,整数对使用者而言没有任何意义,也没有层阶式的管理系统。
另外,多少位元才足够呢? 从之前的估计,107笔数据用32-bit就足够了。用另一个角度想,如果一个5年的项目有100个成员会添加数据,一周工作7天,平均每人每日可增加23534笔数据。这应该足够吧,除非数据经常大量删去又创建数据(每次都产生新的标识符)。
要澈底杜绝标识符不够用,可以重用旧的删去了的标识符,或是把现有的所有标识符重新编号。 整数标识符在多人同时建立数据的时候,还要考虑如何令标识符不重复。解决办法之一,是连线到一个有标识符产生器服务的伺服端程序。
全局唯一标识符
最后,全局唯一标识符(GUID)的好处是确保在不连线情況下,每人也可以得到不会重复的标识符。缺点是GUID的大小一般为128-bit,是32-bit的4倍。
之后
笔者将继续写有关这些游戏数据的使用流程,并最后阐述现在做的引擎在这方面的设计。
本文原来是繁体中文,在2009-03-13发表于http://miloyip.seezone.net/?p=104,本文經過修正。