<audio id="audio" title="第14讲 | 如何制作游戏资源包和保存机制？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/26/c5/26abded8e11ef1dde21c5177417b77c5.mp3"></audio>

我们要做一款打飞机游戏，里面有飞机图片、背景图片、飞机音效、碰撞音效等等非常多的素材。如果将这些资源都放置在一个目录下，将会变得非常混乱。如果按照素材内容来划分目录，程序读取的效率就不高，所以我们需要将这些素材打包在一个资源包内，然后将每个素材都放置在一个虚拟目录内。

因此，今天我们就来如何制作讲解资源包。简单来说，所谓的资源包，就是将游戏的所有资源和素材，进行打包分类，并且进行资源整合，亦或将资源和素材进行压缩，减少游戏体积。

## 什么是资源包？

我总结了一下，可以从这三个角度来理解什么是资源包。

<li>
资源包是一种将游戏的资源和素材进行分类、梳理，并且打包的一种包裹。
</li>
<li>
资源包可以用来压缩游戏资源和素材，减少游戏体积。
</li>
<li>
资源包里存在任何可能性，比如它可以包含图片文件、模型文件、音频文件、脚本文件等等，具体要看游戏开发人员的配置需求，来决定资源包里的内容。
</li>

现在很多游戏公司都不会编写特殊的资源包格式。因为设计一种资源包格式，需要经过一系列复杂的动作，包括包头、包身和包尾。

关于这个格式的设计，一会儿我会给你仔细分析。因为，和我们自定义网络协议包一样，一个好的资源包，能够很方便进行解包、打包、删除文件、插入文件的操作，以及游戏的在线更新、补丁更新、资源包的解包、打包、删除、插入、更新文件等操作。

而一个好的资源包格式，不会占用主程序大量的时间。因为在游戏中，需要直接读取包文件里面的内容。

比如我们之前在Pygame中读取的图片文件，在包裹格式中，可能会这么写伪代码：

```
load.image(‘package.pack/plane.png’)    

```

其中package.pack就是包裹，plane.png是存在在包裹里面的其中一幅图片文件。这样，打了包裹后的文件，就不会污染目录。一般一个包裹文件中存在大量资源，而我们只要按照包裹路径读取就可以了。

如果不编写特殊的资源包格式，那应该怎么制作资源包呢？答案是，**使用现成的压缩软件库，进行打包压缩，直接在程序内使用**。比如我们最常用的zip文件、rar文件，都是可以拿来做资源包文件的。在Python中有内置zip模块，可以直接读取zip文件。我们可以直接在Pygame中结合zip模块进行编程。

## 资源包的格式

我们要讲解的是资源包的制作，我将会用一种较为通用和简单易懂的方法，解释资源包都包含哪些内容，同时让你理解资源包是怎么制作的。

首先，从编程的格式来理解资源包，你需要了解下列这些内容。

<li>
**资源包头**，是一种标记，存放在包裹里最开始的几个字节，一般是2～4个字节。资源包头可以用来辨别这个资源包是哪个公司出品的。例如我后面准备举的一个例子，这里面就有INFO这样的标记，INFO可能是这家游戏公司的名字或者是缩写等等。
</li>
<li>
**资源包版本**，这个不是必须的。如果考虑到一款游戏各个版本之间变化很大，未来可能会修改资源包的格式，那么这个时候就需要版本号。版本号一般会使用2个字节的short类型来存储，或者直接用十六进制编辑器能看明白的字符串，来代表版本号，比如用10表示1.0。所以，结合资源包头，我们现在所看到的结构是INFO10。
</li>
<li>
**资源包是否进行压缩**，这个也不是必需的，但是有一些资源包会说明白，究竟是不是压缩资源包。如果是压缩就是1，不是压缩就是0。至于压缩格式，一般在编程的时候，就会指定清楚，不需要特别说明在资源包内。
</li>
<li>
**资源包的目录结构以及素材名文件名偏移量**，资源包内的目录结构都是虚拟的，所以你可以定义在资源包内类似于/game/res这样的目录结构。但是事实上，这只是方便程序调用，事实上目录是不存在的，这是一种只存在在包裹内的虚拟目录。
</li>

然后，我们需要规定素材的**文件名**和**偏移量**。比如/game/res/background.jpg。这是告诉我们在/game/res虚拟目录下，拥有background.jpg这个文件。随后需要告诉程序偏移量是多少，一般是存储4个字节的整型数字。

到目前为止，资源包的格式看起来可能是这样的：

```
INFO100/game/res/background.jpg,[四个字节的偏移量]

```

在这里，我们看到，偏移量之前多加了一个逗号“,”。这是一个**分隔符**，也就是告诉程序，这一段在哪里结束。

随后是四个字节的偏移量。所谓的**偏移量**，就是告诉程序，你要到这个包裹的第几个字节去寻找这个文件的具体内容。

<li>
**资源包的素材本体**。每个本体都可能是一个二进制文件、文本文件或其他任何文件。这些文件的文件名在资源包的素材文件名中都被定义好了。在资源包的素材本体中，我们可能会碰到各种各样的二进制字符，那么我们怎么知道这些素材是从哪里开始哪里结束的呢？
</li>
<li>
**资源包的素材长度**，规定素材的长度有两种方法，**一种方法**是在定义资源包的目录结构以及素材偏移量的时候，再加上一个素材长度，也是四个字节的整型数字。这种方法的好处是，不需要添加某个分隔符告诉程序，这个素材的本体到这里结束。**第二种方法**是在本体结束的位置添加分隔符，比如一个逗号或者分隔符号|。这种方法的好处是，不需要知道文件长度是多少。但是坏处是，分割符号可能会和素材本体重叠。
</li>

比如素材的本体是个二进制文件，分隔符比如是!@#$，素材的本体里面也存在!@#$这样的内容，这样的情况下，就会出现读取中断，因为程序以为素材内的!@#$就是结束符号，事实上这只是素材本身的内容而已。

- **资源包结束符**，这个也不是必须的。我们要结束资源包，必须在资源包的结尾添加结束符，这个结束符是告诉程序，资源包已经结束了。

我们来看一个完整的资源包，大概是什么样子的。

```
[资源包头][版本号][是否压缩][资源包目录/素材文件名A][文件A偏移量][文件A长度]…[资源包目录/素材文件名N][文件N偏移量][文件N长度][素材A本体]….[素材N本体][结束符]

```

了解了资源包的格式内容，我们可以很方便地利用Python或者C语言等来编写相应格式的资源包。

我来给这部分做一个总结：

资源包的存在，有两个目的，一是让游戏目录干净整洁，不然看上去都是乱七八糟的图片和各种配置，二是让游戏程序能更快地从内存中读取游戏资源制作的包裹文件，加速游戏的运行效率。这个包裹文件中含有虚拟目录、资源、资源位置、资源名字等等信息。我们不需要从文件目录中去读取单一文件，只需要从内存中载入的资源包中取出某个文件即可。

## 如何制作游戏的保存机制？

每一个游戏几乎都有保存和载入的机制。首先你需要知道，只有保存了数据，我们才能载入数据。那么游戏的保存机制是怎么做的呢？

事实上，游戏的保存和游戏的地图编辑器中保存地图的原理，可以说是异曲同工。如果一个游戏中，有地图、坐标、人物、装备、分数，这些都需要被记录下来，那么我们不可能将地图、坐标、人物、装备、分数等全部转换成二进制文件记录下来。那应该怎么做呢？

首先，如果是记录地图，有地图1或者地图2，我们只需要记录地图的ID就好了。假如是地图2，坐标是（x，y）。人物只需要记录人物的ID，再关联到人物。一个游戏中，玩家建立了一个人物角色，就会将这个人物角色进行保存，不至于丢失人物角色。所以，在读取游戏的时候，需要先读取人物角色，再读取保存的游戏内容。

至于分数就很好记录了，记录分数其实就是记录数字，所以记录起来会很方便。

那么装备呢？如果是装备，一般会将装备的所有内容记录下来，如果做得精致的游戏，还会将地图中那些掉落的装备和死去的NPC进行记录。

还有一种做法是，将游戏保存的文件直接导出成一个脚本文件，以后每次读取数据就只需要使用程序读取脚本就可以了。

## 小结

今天我讲解了资源包的制作以及游戏进度的保存，你需要你记住这些内容。

- 制作资源包的目的是为了厘清游戏素材以及游戏素材的存放结构。资源包的结构与压缩包的结构比较相似，但是为了更贴合游戏程序读取，会对虚拟目录和素材文件名等，做一些修改。
- 另外，为了方便保存游戏进度，我们可以做成游戏脚本，第二次打开游戏直接载入保存的脚本即可。

给你留一个小思考题吧。

在《GTA》中，汽车会有不同程度的损毁，当你保存完游戏重新进入的时候，汽车又复原了，请问这是为什么呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！
