3D引擎中的异步资源调入
前几天又和一个朋友谈到连续世界(Continuous World)的实现,他讲“这很简单,后台使用一个线程读磁盘不就行了”。这使我想起F3D中的异步资源调入机制,这是F3D中我最不满意的一个部分(请见之前的博文:)。F3D中的异步资源调入就是基于这种直觉去设计的,设计目标是:不单场景可以异步调入,道具模型,角色模型等都需要异步调入,这主要是为了避免游戏逻辑被卡在读盘上,造成某些操作的时候会卡一下。所以,F3D的异步资源调入机制是建立在Resource基类上的,这样所有的Resource都可以使用异步调入机制。实现也很简单:
1.启动一个线程(且叫它IO线程),它开始等待一个event;
2.主线程将需要调入的资源加入一个队列中,并激活这个event;
3.IO线程被激活,它开始循环读取请求队列,完成所有IO请求,然后回到1的状态。 这个方案缺乏设计,主要体现在两个方面,首先是异步资源调入机制对上层逻辑的影响考虑不足,其次是没有考虑资源自身也有内在联系。
先从简单的说起,看看后者:资源自身的内在联系。例如一个角色模型--在F3D中是一个Resource的派生类--ResSkin,当这个资源被载入成功之后,现有的机制是,当渲染系统引用材质的时候,会发现贴图资源(是另外一个Resource的派生类ResTexture)还没有创建,则会将贴图读取请求发送给IO线程,而当前帧则只能使用一个默认材质,这样造成的结果就是角色模型会显示成白色,最坏的情况是:如果IO请求队列中已经有了很多其他请求,那么这个“空白材质”的状态会十分显眼。另外一个资源相关的例子在游戏层,例如一个主角,因为要换装,可能需要几个skin来组成,理想的情况下是需要这些skin全都加载完成之后,才显示出来。
再来看一下对上层逻辑的影响。为了举例子,先交代一下F3D的Entity,Entity包括一个Resource对象引用和一个AnimCtrl对象,前者指向对象资源,后者用来存储动画播放的动态,以及控制逻辑,这是一个典型的FlyWeight模式。如果没有引入异步IO的情况,那么很简单:
Entity* myChar = new Entity;
myChar->LoadResource("asdf.xxx");
myChar->PlayAnim("walk");
引入了异步IO之后,有一个问题就要处理了,LoadResource()调用之后,资源并没有立即可用,AnimCtrl无法知道资源中是否有walk动画,也不知道这个动画有多少关键帧,无法立即执行PlayAnim()的操作!如果把这个问题封装在AnimCtrl这一层,即把PlayAnim()命令存储起来或者开始累计动画时间,并每帧检测资源是否创建完成,一旦完成则将动画状态计算好。这显然是一种很傻的办法,而且上层相当于一个busy loop的模式,效率很低!
要解决这两个问题,方法之一就是,所有有关上层逻辑的资源对象都不使用异步IO机制,即只有World Geometry才使用异步IO。这样大大简化了问题。
我一直在思索如何真正解决这两个问题...。对于资源之间的联系,我想还是比较好解决的,只要加入一个“IO请求分组”的概念即可,对于模型引用贴图这种情况,可以为Resource增加一个方法用来获得某资源内部引用了那些其他资源,这样在IO线程中,某资源调入完成之后,递归的调入其引用的所有资源;至于主角换装系统那种游戏层的需要,就只能提供接口,由逻辑层来完成“提交一组IO请求”的功能。
“IO请求分组”这一个概念无法孤立的工作,必须配合“完成通知机制”。如果想要避免busy loop模式(上层每帧都去问,资源调入了没有?),那就只有引入“完成通知机制”了,这也就意味着异步IO模型带来的异步编程的复杂性,无法完全封装在底层,必须由上层承担。上层在添加一个IO请求的时候,可以指定一个事件信息的结构体,当IO线程完成该IO请求的时候,通知主线程,并同时携带此事件信息,其面Entity的例子就变成了:
Event initAnim(...);
Entity* myChar = new Entity;
myChar->LoadResource("asdf.xxx",initAnim);
....
void onInitAnim()
{
myChar->PlayAnim("walk");
}
这种编程方法给上层增加了很大的复杂度,也不是一个很理想的方法。如果您想到什么更好的思路,请一定告诉我啊。:)
页:
[1]