zoukankan      html  css  js  c++  java
  • 从Demo到Game Engine(六) Run Time Resource Manage

    从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,开发时所用的工具相关,内容涉及方方面面,过于庞大,就不继续讨论了。

  • 相关阅读:
    ERROR Invalid input of type: 'bool'. Convert to a bytes, string, int or float first.
    python2中正则通过全部中文字符
    查询正在执行的SQL
    dataguard 备库删除已应用过的过期归档
    linux lrzsz上传下载
    /u01/app/11.2.0.4/grid/perl/bin/perl -I/u01/app/11.2.0.4/grid/perl/lib -I/u01/app/11.2.0.4/grid/crs/install /u01/app/11.2.0.4/grid/crs/install/rootcrs.pl execution failed
    重启网卡提示Bringing up interface eth0 Device eth0 does not seem to be present,delaying initialization [FAILED]
    rhel 7 RAC安装执行root.sh遭遇ohasd failed to start,Failed to start the Clusterware. Last 20 lines of the alert log follow
    Error in invoking target 'agent nmhs' of makefile...MK_EMAGENT_NMECTL修正
    oracle 11g RAC由非归档修改成归档
  • 原文地址:https://www.cnblogs.com/clayman/p/1741882.html
Copyright © 2011-2022 走看看