重构 Dream Engine2 资源管理

开发完 MMO Demo 之后,开始重构引擎了,首要解决的就是之前 Artists 对资源文件管理复杂的问题,而从这个问题,逐渐引伸出资源管理的问题,最后演变成为一个大规模的重构方案。

首先说明 DE2 之前的资源管理设计,在开发 DE2 初始时,并没有对资源管理进行详细的设计,所以 DE2 的资源管理基本上延用了 DE3D 的方式。具体分为以下几点:

1.资源文件结构。典型的资源有几种,比如纹理、几何数据、动画、材质、场景图节点等等,资源文件都是单独存储成文件的,这样的设计初衷是为了实现更大的灵活性,例如:不同的材质文件可以引用相同的纹理文件,不同的场景图节点描述文件可以引用相同的几何数据或者材质数据等等,但事实上在实际的游戏开发中并没有设想的那样美好,稍后将详细描述实际使用中存在的问题。

2.资源引用关系。资源和资源之间往往存在着引用关系,例如材质使用了一张纹理,场景图节点使用了一个几何数据或者是材质等,在 DE3D 中我们使用了最直观也是最容易想到的方式:资源文件中直接记录所引用资源的相对资源总目录的文件路径。

3.资源对象的管理,由每个资源管理器自己管理。资源对象的查找和生命周期管理也是由各自的资源管理器完成。资源对象是通过引用计数来控制生命周期的,当引用计数为零时,自动释放资源对象。每个资源对象在引用其他资源对象时是直接设置的,比如材质对象中包含了对纹理对象的引用,在材质中直接使用纹理对象指针。

4.异步资源加载,是通过多线程完成的。在多线程中实现资源从磁盘到 RAM,从 RAM 到 Build 资源对象的过程,当异步加载资源时指定资源创建的回调。引擎在资源加载和创建完成时进行回调。

以上基本上就是之前引擎资源管理的设计。那么,这样的设计都有哪些问题?为什么要重构?重构的方案是什么?下面一一叙述。

问题

1.资源文件数量过多。每个资源都是单独的资源文件,这种方式看似灵活,实则管理麻烦。在实际开发中,美术要面对多种类型的资源文件,纹理、几何数据、材质、动画、骨骼、场景图节点,在 DE2 中,资源类型又增加了,美术就更头大。举个例子来说,一个骨骼动画模型在 max 导出时要导出至少 5 个文件,一个骨骼动画节点描述,一个骨骼文件、一个蒙皮几何文件、一个蒙皮节点描述文件,和至少一个动画文件。在开发中需要将导出的这些资源导出到引擎指定的目录下,并且提交到服务器中,如果漏掉任意一个,都导致资源不能正常加载。当资源越来越多时,资源文件数量爆炸性增长,文件管理上的复杂性就可想而知了。这对比较感性的美术们来说,是很痛苦的事情。即使是程序员的我,在实际的使用过程中,也感觉到非常麻烦。另外,之前设计方案中的资源文件复用的灵活性,其实在开发中也体现不明显,比如为了实现纹理的复用性,美术需要将几张贴图合并为一张,但是有这几张贴图可能属于完全不同的关卡,甚至不可能同时出现在一个场景中,这样导致渲染物体的显存资源浪费,还有像复用几何文件这种情况几乎就没有发生过。其实资源的复用完全可以通过编辑器中对象级别的复用来实现,这样资源文件级别的复用在开发中显得微不足道,甚至有时因为考虑资源复用增加了美术的资源管理的复杂度。

2.记录引用资源路径的方式不灵活。这种直接记录资源路径的方式,导致资源文件路径发生改变时,引用这个资源的资源很难做出相应的调整,进而无法加载改变路径后的资源。尽管我们可以要求美术在一开始就设定好资源存储路径,但是依然不能防止这种情况的发生,而且有时侯处于项目管理的考虑,资源路径的改变是很有必要的。虽然我用了一些实现技巧,但还是没有彻底解决。

3.不能预读引用的资源信息。由于在资源文件的数据中记录了引用的资源文件路径,只能在解析资源数据过程中(或者称之为 Build 资源对象的过程中)才能知道引用的是哪个资源,这种方式,外部无法控制资源的加载顺序。此外,这种方式还造成了异步加载资源时,外部无法设置引用资源的加载方式的问题(相对于当前的加载线程是同步还是异步?)。

4.没有实现统一资源管理,缺少统一的资源对象信息描述。由于每种类型的资源由自己的管理器管理,导致资源不能用统一的方式访问和操作。

5.多线程异步加载的实现复杂。比如要求在异步读取资源时,必须指定回调函数,接口上过于严格,应用程序显示的知道了多线程资源加载的实现机制,没有做到屏蔽细节,而且回调还打乱了游戏的代码流程,需要游戏客户端单独处理回调后的流程,导致客户端实现复杂。还有些异步加载资源时的特殊情况没有处理,例如当异步读取某个资源时,主线程同步的需要这个资源对象等等。另外,由于在多线程中创建资源对象,导致资源的管理必须是线程安全,线程同步过多,降低了多线程并行的能力,这一点在 MMO 开发中表现的非常明显。

Dream Engine2 资源管理解决方案

根据上述讨论,制定出新的资源管理解决方案。

目标

1.降低资源文件管理复杂度,减少资源文件数量。

2.提供灵活的资源文件管理,当资源路径发生变化时,引擎能自动识别变化并作出相应的调整。

3.提供灵活的资源引用机制,快速获取引用资源的信息,以及资源被引用的信息。

4.实现统一的资源管理,统一资源信息描述。

5.降低多线程资源加载复杂性,提高多线程加载性能,降低线程同步次数,对游戏层屏蔽实现细节,实现灵活的加载机制。

实现

1.使用统一资源数据存储文件:资源包。资源包内部可以容纳任何资源数据,多个资源可存在于一个包内,在资源包头记录包内资源的信息,以及包内资源引用的资源信息。包内资源可继续分组,类似于文件系统机制,通过名字区分。例如包名是A,资源是 texture1,这个资源的完整名字就是 A.texture1,如果在 Group1 分组下,则为 A.Group1.texture1,如果Group1下还有分组,则以此类推,在引用资源时直接使用这个资源名。由于资源存在于包内,资源的生成只能在引擎编辑器中完成,资源的存储控制就完全交由引擎编辑器完成,这样当引用的资源存储位置发生变化时,实际上是名字发生了变化,可以通过引擎编辑器自动完成相应的改变,实现了当资源路径发生变化时能自动识别变化并作出相应的调整,无需人工干预的目的。由于在包头记录了包内资源的基本信息以及包内资源的引用资源的信息,这样只需要解析包头就可获取相关的资源信息,也实现了快速获取引用资源信息的目的,游戏逻辑可以预读资源包信息即可构建完整的资源信息。同时,资源包的设计实现了降低资源文件管理的复杂度和减少文件数量的目的。

2.资源数据库。引擎中实现资源数据库机制。通过分析发现,资源和资源之间往往存在着引用关系,这种关系实际上是构成了 DAG(有向非循环图)这样的数据结构,每个资源节点都可以引用多个资源,同时又会被多个资源引用,但不会形成循环引用关系,所以我在引擎中实现了这样的资源数据结构,我称之为 Resource Info DAG,图中每个节点并不是资源对象,而是资源描述,因为资源可能随时被使用,资源描述可以一直存在于整个游戏过程中而不必被删除(当然想删也可以,只是需要这个资源时还要重新读一次资源包头)。资源信息是轻量的数据,只是记录了资源的类型、名字(我们实现了名字系统,一个整形即可代表一个完整的名字)、资源大小、在资源包中的数据偏移(用于资源加载)、所在的资源包、所引用的资源名字列表以及真正的资源对象指针。同时,为了快速索引到需要的资源,只有这样一个 DAG 是不够的,还需要一个名字和资源信息的映射列表,这样,Resource Info DAG 和资源映射表,就形成了引擎中的资源数据库。资源信息的出现实现了统一资源信息描述的目的,而且为实现统一资源管理铺平了道路。

3.改进资源的异步加载框架。异步资源加载可以是单线程也可以是多线程,无论在哪个线程,流程应该是一致的。在引擎中存在一个资源预读列表,以及资源预读管理器,当预读一个资源时,通过资源数据库可以立即知道资源是否存在,如果不存在则先加载其引用的资源(在资源信息中已经记录)。预读可以是阻塞的,也可以使分时间片的方式,具体的方式允许外部在预读资源时设置。时间片方式目前只是实现到资源的粒度,如果一个资源超过了读取限制的时间,则继续读直到完成为止,如果没有超过则预估下一个预读的资源时间,如果够则读,不够则进入到下一帧。游戏层可以自己设定预读资源,真正需要资源时直接通过资源数据库获取,而且往往此时资源已经被创建好,如果没有,则同步创建。应用层也可以预读一些基础资源,比如玩家相关的模型、动画等等,和逻辑数据相关的最好能预读,无关的即使需要时再读都可以,而且是完全异步,比如纹理、声音等。这样的改进带来的好处就是游戏层不需要关心资源加载的实现细节,甚至不需要向引擎提供回调(需要时也可以设置回调),需要资源时直接通过资源数据库接口获取,简化程序的复杂性。而且游戏层可以根据自己的需要预读资源,资源的调度上也很灵活。

资源的多线程加载只是完成从磁盘到内存的过程,并不包含资源对象的创建过程,引擎规定所有资源对象的创建都是在主线程中完成的,不允许在其他线程创建资源对象,这样就完全消除了资源创建中线程同步的问题。实际上我们知道,最耗费时间的往往是磁盘 IO 读取操作,而资源数据一旦进入到内存,资源的解析和构建过程是很快的,即使放在逻辑线程中同步创建资源对象,开销也不大。而且在渲染线程独立的情况下,逻辑线程的负担就更少了,同步创建游戏对象已经不是主要的性能瓶颈。多线程只是 IO 读取的话,实现上就简单了许多,首先就不需要考虑资源对象管理上的线程同步问题,其次可以利用引擎已有的基于 Ring Buffer 生产者/消费者线程机制实现一个完全无锁的多线程 IO 读取框架。不过有种情况还是要特殊处理:当多线程 IO 读取某个资源数据时,主线程同步的需要创建这个资源对象,这种情况下需要同步IO读取。但实际上这种情况属于低概率事件,即使偶尔发生用户也能接受。

Advertisements
此条目发表在Uncategorized分类目录。将固定链接加入收藏夹。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  更改 )

Google+ photo

You are commenting using your Google+ account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

w

Connecting to %s