一个基于组件的动态对象系统

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

一、静态的痛苦作为一个项目经验丰富的程序员,你经常会遇到游戏开发过程中的“反复”(iterations):今天美术将一个静态的模型改为骨骼模型并添加了动画;明天企划会议上决定把所有未拾取武器由原先的闪光效果改为原地旋转;后天你的老板告诉你:配合投资方的要求,需要提升AI的质量,这使得AI需要响应特定的碰撞检测、可破坏的路径变化,甚至彼此的交互。哦,修改设计,按照教科书上的做法我们必须对现有代码进行重构,你回答道。但你的老板显然不这么认为。尽管全体程序员一致地、强烈地反对,项目经理还是决定要在一周内把这些改动全部付诸实施。这是一场噩梦不是吗?于是工程上的禁忌、代码层面的犯罪……各种各样丑陋不堪的东西写进了游戏程序,除此之外,你还搭上了周末和女朋友约会的时间。更糟糕的是,当你周一凌晨提交代码后,发现原本“健壮”的游戏程序,经常莫名奇妙地崩溃,这让你的老板在投资方那里出尽了洋相……后果可想而知。当然这不能完全责怪你的项目经理和老板,毕竟游戏不是一道纯软件大餐。而我相信,你的游戏只要还被当作一件艺术品来制做,就永远无法避免反复。既然它至榛完美的必经之路就是设计上的反复,那么我们总有办法将它的冲击降至最小。这里我要讨论的是一个基于组件的对象系统:在游戏层中,它可以使对象行为的改变变得异常简单,甚至可以在无需程序员介入的情况下,由企划或设计师来动态组合成新类型的对象,而作为应用该系统的一个副产品,它还能为你的游戏层代码降低耦合度。下面让我们来看看,传统情况下我们是如何设计游戏层的:所有的物体都是一个Object。它作为游戏中所有类型的基类,由许多子类来继承,诸如Renderable、Movable、Collideable等等。顾名思义是为可渲染对象、可移动对象、和计算碰撞的对象准备的基类。继承自Renderable又有一个名为Animatable的类,显然有经验的你也能猜到它具有赋予类型以动画的功能。在Collider之下有一个Inventory类,它定义了可拾取物件的一些规则。在此之下就是一些具体的类,例如会进行动画的、可移动的Character人物类,以及只能渲染静态物件的、可拾取的Weapon类、Item类、Armor类。这样一个简单的类继承体系可以由图1来表示。图1一个传统的、典型的、看上去不错的继承体系嗯,这个继承体系看上去合理且干净,绝对可以做教科书中的范例,而且对于这个简单系统来说能工作得很好,直到有一天企划的设计发生了修改。就像之前提到的,企划们从测试员或内测玩家中获得了反馈:武器或者道具掉落在地上,如果没有一点显眼的表示,玩家很难注意到,甚至会让整个游戏显得死气沉沉。于是他们告诉你武器掉落在地上需要原地旋转,就像Quake那样,而道具掉落在地上,每隔2秒要闪烁一下。你对照着类继承图比划了一下,觉得可以把Inventory类的继承关系从Collideable下转移为多重继承Collideable和Animatable。于是你开始修改类继承结构,尽管Armor不需要播放动画,一个空函数就可以打发它了。那么这个问题目前算是被解决了。可是好景不长,关卡企划觉得目前刚体物理的效果还不错,决定广泛应用这一特性,而他失望地发现很多物件都没有刚体物理的效果,只有RigidBody才拥有这项功能,而它的实现只有一些简单的盒子一类的物体,用于做关卡设计。于是他告诉你需要把屏幕上能看到的物体,尽量都赋予刚体特性。你同他争执了一段时间,最后你妥协了,把Renderable整个拉到RigidBody继承体系下。这样尽管Tree和Character并不能按照一个简单刚体来运动,但至少Weapon、Item、Armor可以了。在折腾完关于刚体物理对象的改动之后,你再度审视这个继承体系时,发现它已经不像原先那般优雅了:大量定义接口的基类被放在继承树的上方,而下方都是零散的各个具体类。这很让人倒胃口,你这么想着,打算着手真正重构目前的代码。但时间不等人,第二天企划又告诉你,他需要用脚本来控制这些刚体对象的位置,这下连Movable都无法幸免,你必须把它移动到RigidBody之上,让所有的具体类都能继承它。这样一个头重脚轻(top-heavy)的继承树简直是一个教科书式的反面教材(如图2所示)!坚持原则的你实在看不下去了,向项目经理提出了质疑,要求砍掉这个功能,或者开辟额外的时间让你重构代码。但是很不幸,很多情况下,项目经理是不会理睬这种要求的。图2在许多“合理”的设计改动后,继承树往往变成了这种头重脚轻的样子如此这般的设计,为什么无法满足游戏的快速反复的开发需要呢?我想主要原因有二:一是C++和其他强类型语言在继承上的强制性;二是我们恰恰让继承做了它所不擅长的事情。继承在很多强类型语言中,是一个静态的语言行为,是在编译期决定的,而且对一个较大的继承体系的修改,不但面临重重困难,而且将会对之后的系统产生深远影响。继承的这种特性决定了它不适合类型行为经常变更的场合,或者说在类型行为经常变更的场合中,仅仅使用继承很难解决矛盾。那除了继承,语言的其他特性是否能满足我们对对象类型这种近乎变态的反复要求呢?答案之一就是组合,或者聚合,直观一点就叫“has-a”的关系。倍受推崇的《设计模式》一书中,也建议尽量使用对象组合而非类继承。该书开宗明义写道:“1、对接口编程,而不是对实现编程;2、优先考虑使用对象组合,而不是使用类继承”[GoF94]。至于原因,在书中也有很精辟的论述:“我们的经验显示,架构师经常过分强调将继承作为重用技术,而事实上,如果着重以对象组合作为重用技术,则可以获得更多的可重用性以及简单的设计”[注1]。二、动态的优雅1、组合既然大师们是这样说的,我们不妨回头看看游戏层的系统。假设我们要设计这样一个“武器”的类,类似上面的那个例子,它需要能渲染、能播放动画、能移动位置,甚至在掉落在场景中时,它还具备刚体物理的特性。于是可以整理出如图3这样的类:图3一个典型的由组件组合而成的对象。可以看到,一个Weapon对象就是简单地由IRenderable、IAnimatable、IMovable以及IRigidBody这些具体的组件组合而成的。在下文里,我就把组合成对象的这些功能性的类,称为组件。哦,功能倒是都组合在一起了,但我怎么使用这些组件呢?它没有任何可供调用的方式!经常使用基类接口的你开始注意到这个问题。在传统设计中,我们通常需要一个统一的基类接口来操作多个对象,而这些接口被声明为虚拟的,以便我们在类层次中实现多态(若接口不是虚拟的,则其调用的实现函数就是虚拟的),而在客户端,我们一旦能获得这个接口就能以统一的方式来处理所有从这个类继承的类型。这种做法对于有经验的你早就像吃饭睡觉一般熟悉了,如果不能通过统一接口来处理多种对象,恐怕很多人要难过到死。我们的组件,也是由一个通用的组件基类接口定义的,权且称他为IComponent,实现各自功能的组件,需要各自扩展这套接口。例如渲染组件IRenderable,可能需要扩展一个Render()方法;而动画组件IAnimatable,可能要做的是扩展一个Update(float)方法用以更新动画;IMovable组件就需要SetPosition()/GetPosition()之类的接口等等。有了这些组件接口之后,我们的组件类就直接实现这些接口。例如Renderable就实现IRenderable::Render()。定义接口的优点在于,你可以通过一个对象的句柄,查询这个对象是否实现了某一组件的接口。如果回答是肯定的,则可以返回一个指向该组件的指针,而指针的类型是接口类,这样客户端代码就可以调用这些组件的实际功能了。如果回答是否定的,则返回空指针,意味着该对象并未实现指定的接口,查询失败。图4比较好的说明了这个问题。图4需要使用组件接口时,要对ObjectManager查询组件接口。2、无中生有既然对象都是由组件组成的,那么对象本身就可以非常精简,甚至连一个组件的指针都不用储存,而将组件管理的工作可以交给对象管理器去做,我们暂且叫它ObjectManager。这样,世界上就不存在名叫Car的对象,也不存在叫Dog的对象,它们不过是一些组件的组合而已,只是在ObjectManager一侧的记录中,有着Dog所拥有的组件,以及Car所拥有的组件。当对象需要行为的时候,客户端代码就向ObjectManager索取对应的组件接口,比如:代码1IRenderable*renderable=static_castIRenderable*(objectManager-QueryInterface(object,TYPE_IRenderable));或者写一道宏指令以减少笔误:代码2IRenderable*renderable=QUERY_INTERFACE(objectManager,object,IRenderable);之后你就像平常一样操作这个组件:代码3renderable-PreRender();...renderable-Render();...renderable-PostRender();所有不同类型的ObjectHandle看起来都是差不多的,他们的区别只在于记录在ObjectManager里的组件不同而已。所以,忘掉类型的概念吧!在这个世界中,只有组件的组合,没有死板的类型。3、即插即用那么如果企划再对我的Weapon提出什么非份的要求,怎么办?担惊受怕的你继续问道。很简单,如果Weapon类还需要其他的功能,只要这个功能已经以组件形式实现了,那么你完全可以让他自己搞定!因为基于组件的对象系统中,一个具体的对象已经没有静态的“类型”概念了,只要我愿意,我可以对某个对象添加任意的组件功能,即使它看上去多么荒谬:代码4ObjectHandle*weapon=objectManager-CreateObject();//创建了一个赤身裸体的对象,它还没有任何功能。objectManager-AddComponent(weapon,TYPE_IRenderable);//对象拥有了渲染的组件,及其功能。objectManager-AddComponent(weapon,TYPE_IProceduralAnimatable);//武器也需要过程动画吗?无论怎样的组件都能添加。对象的能力不再“静态”地由继承关系决定,而是由一组扁平组织起来的组件“动态”地、自由地组合而成。只要组件实现得足够健壮,我们可以放心地生成任意“类型”(或称组合)的对象,而不用担心设计上的修改和对象臃肿的问题。嗯,这样的对象足够灵活,但还不够!我们可以解析描述对象的XML文件,从其中读取的信息里,决定我们要生成什么样的对象,以及添加怎样的组件。代码5voidCreateObjectFromXml(XmlNode*pNode){ObjectHandle*object=NULL;if(pNode-GetName()==TEMPLATE_WEAPON){object=objectManager-CreateObject();objectManager-AddComponent(object,TYPE_IRenderable);objectManager-AddComponent(object,TYPE_IProceduralAnimatable);IProceduralAnimtable*procAnim=static_castIProceduralAnimtable*(objectManager-QueryInterface(object,TYPE_IProceduralAnimtable));ASSERt(procAnim);procAnim-SetSeed(Rand());procAnim-SetIteration(pNode-GetAttribute(Num_Iteration).ToNumber());}else....}哈哈!这样我们可以把这些添加功能的工作,扔给企划写XML去了。而且我

1 / 16
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功