游戏引擎的资源管理

(提示:此文为资源管理常识性知识综合,有经验的读者可以飘过。)

资源管理模块,也叫内容管理模块,是图形引擎的基础组件之一。当然,也是音效引擎,或者说游戏引擎的基本组件之一。这也带来了第一个选择题,是否要图形、音效等模块共享资源管理。

要说共享的话,理由似乎比较充分。既然都需要,而且需求差异不是特别大,与其重复代码,不如统一共享。但现实中,共享资源管理模块的游戏引擎并不多,理由也很简单,图形、音效等子系统的底层可能来自不同的第三方,或者受限于内容制作流程,各个子系统有自己的资源管理系统。要统一的话,就必须是各个部分都是自己从底层做起的,但这样往往是不经济的。另外,统一也有统一的坏处,虽然各个子系统对资源管理的需求接近,但仍然各有各的优化方向,而且难免日后某个子系统会有意料之外的特殊需求,导致统一资源管理模块过于复杂,或者特例代码过多。所以,保守的做法是各子系统用自己的资源管理。

资源管理的最基本功能就是资源标识和资源加载,就和访问文件系统类似,但要把后端封装隐藏掉,也就是常说的虚拟文件系统。不管资源的数据来自文件、内存、光盘、数据库、还是网络,都是底层实现细节。资源标识多采用类似文件系统路径的方式,当然也有用整数标识的,不过这种整数标识也是资源打包预处理时自动生成的。对制作人员来讲,有意义的名字是必须的。

资源的加载和卸载则可以比较复杂。最简单的情况是加载后就不卸载。稍进一步,则对每个场景有一个所需资源列表,根据前后场景差异来卸载不需要的资源、加载新资源。但有时场景的资源需求未必是静态确定的。所以,一个常用的方法是,给每个资源加一个类别标记,比如“主角”、“地等级怪”、“场景通用”、“场景A专用”等,这样可以指定加载或卸载某一类别,实现对资源装卸的主动控制。

当场景非常大时,则没必要所有该场景的资源都装入内存,也可能根本装不下,这时必然需要能够按照显示需求和内存来自动滚动装卸资源。但要能自动卸载,则程序必须能判断哪些资源当前不被使用可以卸载,常用的方法是对资源做引用计数。做引用计数的问题是,使用者必须正确的增减了计数。就像使用COM对象时蓄养防止内存泄露一样,对资源的引用也需要一点象CComPtr或shared_ptr这样的辅助工具。但对C#等不能重载->和.运算符的语言,是无法写出这种易用的辅助类的;不过,基于垃圾回收的内存管理有个更优雅的解决方案——弱引用(Weak Reference),当资源不被引用时可自动回收。但资源管理与COM等不同的是,资源引用数为0时不一定要立刻卸载资源,因为资源可能不久又被重新请求,所以,一般会根据加载新的资源的内存需求卸载一些不用的资源。那么,实现了这套复杂的系统就可以应付所有的情况了么?不能,需要时装载有时会有点太晚了,会导致游戏卡一下,需要更复杂的系统来解决,比如预测资源需求提前在背景线程装载,或者牺牲一点显示真实性,在资源装载完前先让一些物体不可见。用什么装载方案要根据游戏需求而定,简单的方法也可能更有效。

资源查询不是必要的,但也可能会用到。这个可参考文件系统的查询功能,根据需要决定支持多少。

资源预编译是一种提高资源加载速度的技术,也就是把通过一个预处理的程序,把资源全部转换成能被引擎最简单直接地利用的格式。如今游戏大多做得比较大,一个场景在启动时需要加载上G的数据,启动时间也就成了一个值得关注的东东。但这里还有一个设计上的理由,通过把一些复杂的操作提前到设计时的内容处理工具上,减小运行时引擎的规模和复杂度。运行时代码的减少,也有利于提高CPU cache的命中率,提高引擎性能。

资源打包也是游戏普遍要做的,主要理由有两个,一是访问单一文件的IO效率通常高于访问多个文件,二是保护游戏资源。打包可以自己设计虚拟文件系统,也可以利用现有压缩包文件格式。资源访问最基本的操作是将一个资源读进内存,一般同一时间只打开一个资源。但有些情况需要同时打开多个,比如背景音乐的音频流式访问,它不是一次读入内存的,而是播放到哪里就读到哪里,引擎会同时需要访问别的资源;大场景LOD数据读取也可能有类似的情况。这种情况需要打包模块支持,但有些压缩包的API不支持同时打开多个包内文件。这个也不难解决,其实这种比较大的、需要流式访问的资源文件,不放进包里也无所谓。

打了包就还需要考虑资源更新问题,上百M或上G的包,需要更新或添加一些资源文件怎么办?一般游戏只带压缩包解压组件,就算把压缩组件也带上,重打包也是一个费时的过程。所以,一般会考虑增量的模式,更新的文件放在一个新的压缩包里,和原来的压缩包放在一起。引擎找某一资源时,先到最新的资源包里找,找不到再看其他的资源包。这样也就大幅简化了更新过程。

你可能感兴趣的:(游戏,数据库,工具,图形,reference,引擎)