Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

从Demo到Game Engine(六) -- Run-Time Resource Manage

仅供个人学习使用,请勿转载,勿用于任何商业用途

作者:clayman

 

         资源可能包含所有类型的数据,比如mesh, texture, shader,音频文件,scriptlevel data等等。面对现代游戏动辄上G的资源,一个统一的模型管理所有资源必不可少。游戏开发中的资源管理是个非常广的范围,涉及整个游戏开发阶段,不单需要引擎还需要周边工具的配合,这里把资源管理分为offlinerun time阶段。

 

Offline: 这部分是content pipeline中重要的一个环节,主要由相关引擎工具完成,除了实现与一般程序开发版本管理类似的功能外,还包括:

资源编译:把来自DCC工具创建的文件转换为引擎可识别的格式;

创建资源数据库:允许使用某个key找到相应资源文件的位置以及资源之间的相互引用关系;

资源打包:把独立资源打包为一个数据包,被打包的数据可能需要压缩,加密;

 

Run-Time: 这部分属于引擎的核心功能,包括加载/卸载资源,管理资源的生命周期,保证资源唯一性。这篇文章主要讨论run time阶段的资源管理,先来看看一般引擎加载资源的流程:

 

create resource -----> parse resource ----> find resource file -----> compose resource

 

Create Resource: 发出创建资源的请求;

Parse Resource:检查是否支持所请求的资源类型,如果支持,则从资源数据库中找出相关的资源信息;

Find Resource:通过数据库中的信息,读出资源包或者来自网络的数据;

Compose Resource:组合复合资源(比如mesh可能包含顶点数据,材质,纹理….),完成资源加载;

 

为了实现资源的统一管理,首先要做的就是统一资源类型:

IResource
{
   name;
   id;
   type; 
//string
}

 

     所有游戏资源都必须继承或实现此接口。这里直接用string表示类型是为了方便将来扩展任意类型的资源。同样所有资源的创建也必须由一个类来完成:

ResourceManager
{
   IResource CreateResource(name, id, type, T resourceParam);
}

     

     要避免用户可以直接通过具体资源的构造函数创建对象,否则就有可能破坏整个资源管理机制,Ogre也是这么做的。你可能会说:嘿,我只写一个小程序,创建一张空白纹理/render target,何必非弄个ResourceManager,直接暴露构造函数吧。No,我是在讲从demoengine,你怎么还抱着老式的思想呢,不一致性只会迷惑引擎的用户。ResourceParam是一个泛型参数,传递关于特定资源的参数,比如创建一张空白纹理,resourceParam需要包含纹理大小,格式等信息:

CreateResoure(name, id, “texture”, null); //从资源文件加载纹理
CreateResource(name, id, “texture”, textureParam); //直接创建空白纹理

 

         CreateResource检查特定id的资源是否已经存在,如果是,则直接返回已加载资源。上面的伪代码中nameid看起来有些重复,这样写的原因是提醒大家name不一定等于id。直接把资源名作为id当然是可行的,很多引擎就这样做,因此必须保证美术在创建资源时没有重复名称。此外,应该避免用文件路径作为id,因为真正的资源文件路径很可能会变化,路径信息应该通过查询资源数据库得到。也可以考虑用自定义的GUID,不过这也需要在off-line阶段有工具为资源生成合适的GUID

 

         如果ResourceManager发现要创建的资源不存在,则先检查是否支持所需要创建的资源类型。有些引擎中,ResourceManager直接负责加载所有不同类型的资源,这种设计最大的缺点就是难以扩展,每次添加新类型就不得不修改整个ResourceManager。更好的设计是使用不同对象创建不同类型的资源,我把这些对象称为ResourceLoaderResourceLoader把自己注册到manager中,managerdictionary,以typekey保存注册过的loader

CreateResource()
{
  
if( loadedResource.contains(id))
     retrun loadedResource[id];
  
else
  {
     loader 
= loaders[type];
     
if(loader != null)
     {
       newRes 
= loader.CreateResource();
       loadedResource.add(newRes);
       
return newRes;
     }
  }
}

 

         异步加载资源是任何现代游戏引擎必不可少的特性,资源加载通常涉及到IO,解压,资源组合等一系列费时的操作,如果直接在主线程中执行,那结果显然惨不忍睹。上面的模型很容易添加对异步加载的支持。为了让资源创建不阻塞主线程,并尽可能保证无论同步还是异步加载,接口代码都不需要做改动,我们把资源分为createinitialize2个阶段。在create阶段,我们只创建一个对象,并不加载实际资源数据,这一步和new普通对象并没有区别,非常快,因此不会引起阻塞;initialize阶段再加载实际资源数据。主线程调用CreateResource之后,马上得到一个新对象,继续往下执行。ResourceManager异步执行initialize,资源加载完之后,通过回调函数传递给resource。当然,需要添加一个标记,让上层系统知道资源是否可用

asyn load
IResource
{
   name;
   type;
   resourceState;
   LoadData();
   SetData(T data, U resourceParam);
}

//ver 1:
CreateResource(……, bool asyn)
{
   newRes 
= CreateResource();
   
if(asyn)
         add newRes to loadTaskQueue;
   
else
   {
     data 
= loader.Load();
     newRes.SetData(data);
   } 
   
return newRes;
}

//ver 2:
CreateResource()
{
   
return newRes = CreateResource();
}
ResourceManager.Load(resource, 
bool asyn)
{
   
if(asyn)
     add to loadTaskQueue;
   
else
   {
      data 
= loader.load();
      resource.SetData(data);
   }
}
Resource.Load(
bool asyn)
{
   ResourceManager.Load(
this,asyn);
}

 

         上面列出了两种稍微有些区别的异步加载方式, 可以看到,无论异步还是同步,用户编写代码的方式都不需要改变。可以用一个简单的bool变量来表示resourceState,但资源状态并非只有2种,ogre中定义了4种不同状态,我也定义了4种,稍微有些不同:

unload : 默认值,资源已经创建,但还没有初始化;

loading:调用Load()之后,SetData()之前的状态,避免多次重复调用load();

Loaded: SetData()之后,并接通过验证;

LoadFailedSetData()之后,但数据没有通过验证或者发生异常;

 

         资源加载之后,相关的问题就是卸载。与创建相同,资源的卸载也必须通过ResourceManager完成,卸载方式也分为unloaddestroy两种,前者仅卸载资源数据,后者则彻底删除整个对象。这里关键的问题并不是如何卸载,而是何时卸载资源。依赖用户手动卸载对象显然是困难的,传统的卸载方式有2中,一种是通过引用计数,判断对象是否有效;另一种则是把资源做为逻辑group,通过显示调用,卸载整个group数据。两种方法各有优缺,前者允许单独卸载某个资源,后者实现起来则比较容易。对group方式,只需为IResource添加一个groupId,在resourcemanager中保存group集合即可,这种方法非常适合管理静态场景数据。引用计数方式则是传统的重载操作符,实现addRefremoveRef这里不仔细介绍。

 

         值得一提的是对于xna来说,因为C#无法重载=操作符,很难实现引用计数模式,但我们有更好的选择GC!你可能会想到,由于ResourceManager中保存了对resource的引用,除非显示删除,否则永远不会被GC回收。对,但我们可以让ResourceManager只保存resourceweak reference(详见<<clr via c#>>GC部分),这样就可以完全利用GC来进行管理了。还有一点需要注意的是,由于CG只记录托管对象的大小,对texturevertex buffer来说,有可能native资源已经消耗的差不多,但GC只看到xna中相对较小的wrapper数据,不触发回收,因此,每次加载包含native资源的对象时,应该调用GC.AddMemoryPressure告诉GC native资源的存在。

 

         本文仅介绍简单了资源管理run-time部分的资源加载和卸载,至于off-line阶段和rum-time下的资源定位,组合,通常与具体引擎的content pipeline,开发时所用的工具相关,内容涉及方方面面,过于庞大,就不继续讨论了。

 

 

posted on 2010-05-23 03:18  clayman  阅读(2522)  评论(2编辑  收藏  举报