“组合式”实体(Entity)的实现


最近在做项目前期的一些调研的工作,研究并参考了几个引擎和框架的设计,包括内部引擎,商业引擎,和开源引擎,通过比较和学习后,觉得对于游戏中的实体实现,用“组合”的设计模式会比用“继承”的更为便利,想到我早些时候的一个项目里用到的一个引擎,也是实现了组合式的实体,而且对于AI程序员来说也是和“游戏实体”打交道最多的,一个好的设计可以大大的提高代码的质量和可维护性。所以,我觉得很有必要在这里记录一下,也和大家一起分享一下我的一些心得。

在学习面向对象编程的时候,一个很重要的概念就是“万物皆对象”,我们可以把现实世界的物体抽象成一个个的Object,并且通过继承的方式实现多样化的对象集。这个是面向对象编程的一些很重要的概念。我想学过的同学都应该对此非常的熟悉。

游戏中也是一样的,一般游戏内都会抽象出一个称之为“游戏实体”(Entity)的类,当然也有直接叫Object的,不管名字怎么样,概念上是相通的,总之都是对于游戏中可能的任何物体一种提炼。在引擎中用继承的方式扩展Entity是一种比较常见和直观的方式,可以充分利用到面向对象编程中的多态的优势,虽然可能大家都比较熟悉这种方式,但我想还是用一个比较简单的例子来帮助理解,假设我们仅仅做三个类层次的话,可以从Entity这个基类中先继承出,静态实体(Static Entity)和动态实体(Dynamic Entity),静态实体表示没有运动信息的实体,比如一些静态的建筑,地图的网格等等,动态实体表示带运动信息的实体,比如人物,机械,怪物等等,这个作为第二个层次,最下面的一个层次可以是具体的物件,就像我上面所具的那些例子。

继承的好处是直观,而且分工合作也不错,大家的协作可以互不干扰,但“继承式”的实体结构有一个问题,我想用过此类模式的同学应该也有所体会,就是随着开发的进行,基类的“体积”会日益庞大,因为当我们发现子类间有一些共通的地方时,我们可以选择的办法之一,就是把这个共通的地方写到他们共有的基类里面去,我们用上面的例子举例的话,当“人物”和“怪物”有一些属性是共通的时候,我们就不得不把这些代码移到“动态实体”这个类中,当然这还不是最糟,当“人物”和“建筑”有共通的东西时,我们就只能把代码写到最高层的“Entity”中了,而且这些共通的代码可能“怪物”这个类并不需要,但因为它也继承自Entity,所以也“被迫”的包含了这些代码。所以基类代码的可维护性就变的很差,虽然,好的继承关系的设计可以一定程度上缓解这样的问题,但并不能从根本上解决。

为了解决这样的问题,所以,现在很多的引擎架构里,都提出了“组合式”实体的概念,“组合式”的概念就类似与小孩搭积木,通过用不同部件来“组合”出不同的物体。

比如,对于“静态的建筑”这个实体来说,它可以由“空间属性模块”,“显示模块”组成,“空间属性模块”包括了位置,朝向等信息,“显示模块”包括了这个建筑的模型,贴图等和渲染相关的信息。那对于“人物”这个实体来说,他的模块就更多了,除了“空间属性模块”,“显示模块”,还会包括,“人物AI模块”,“动画模块”,“物理模块”等等。对于“怪物”来说,它可能包含了和“人物”差不多的信息,但我们会给它一个“怪物AI的模块,而不是“人物AI模块”。但这时,也许游戏设计师需要一个“高级的人形怪物”,它更聪明,需要有像人一样的AI,如果按照原来“继承式”的设计方式,我们可能需要从“怪物”这个类里继承,然后再从“人物”这个类里拷贝AI部分的代码,或者将这部分代码移到“动态实体”类中,以便于在两个类中共用,但在“组合式”的设计中,我们要做的,仅仅是将那些模块重新组合,用“人物的AI模块”来替代原来的“怪物AI模块”,这样我们就得到了一个全新的实体。

这就是“组合式”实体概念,这些用来“组合”的元件,称之为“组件(Component)”,或者“轨道(Track)”,这些组件是可继承的,所以从本质上来说,它将基于实体的继承,转移到了“组件”的继承上,将实体和属性分开,达到了简化实体类的作用。明白了它的概念,那从实现上来说就更简单了,我们可以在实体类中,包含一个存有组件基类指针的数组,然后在构建实体的时候,将相关的组件添加进来就可以了。然后在更新实体的时候,将所有在这个实体上的组件一并更新即可,代码我就不在这里写了。

“组件式”实体的概念和实现并不是很复杂,但效果却是惊人的,它用一个简单的设计模式,使得代码更易维护,而且,不同的程序员可以更合理的分工实现各自的组件部分。像AI程序员,就可以专注与对于AI组件的开发了,比如,可以做一系列的AI组件,对应不同的难度的AI,只要将这些组件和实体相连,就可以实现虽然怪物的外观相同(因为“显示组件”是一样的),但AI截然不同的效果了。对于3D程序员也是这样,比如,我可以先做一个真实效果的渲染组件,但可能我还想试试卡通渲染效果,那只要再实现一个这样的组件,将原来那个替换掉就可以了。

当然,“组合式”实体的开发中,也有些问题值得大家思考,最重要的一个问题就是,“如何在组件间传递数据?”,最实际的一个例子就是,“空间组件”中存有实体的位置信息,那其他的组件开发者肯定是需要得到这些信息的,所以在组件间传递数据的功能是必不可少的,我不推荐直接开放诸如GetComponent这种直接得到组件指针的接口,那如何安全而有效的传递数据呢,这个仁者见仁,智者见智问题就留给大家讨论了。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————



(已被阅读1,864次)

8 评论

  1. 博主好超前啊,11年我才入行,博主已经思考“组合”式引擎与“继承”式引擎的差别了。现在在用unity3d开发游戏,深刻的理解了“万物即对象”。

  2. 楼主好厉害!我最近也在思考引擎组件化的设计,感觉这个使用组合的设计方式比使用继承方式来实现用户自定义逻辑的方式好。

  3. 使用Components来组合GameObject,的确很方便和灵活,不知道博主对unity3d有没研究,他的引擎结构很不错,因为GameObject是通过组件拼成的,所以可以实现重用功能,例如Unity 中的Prefab系统,这里有篇好文章介绍 http://altdevblogaday.com/2011/05/23/the-top-6-misconceptions-i-had-about-unity/
    ,关于组件间传递数据,我引用UNITY3D的一些方法,可以使用在某些特定的物体,组成管理器,使用单件类型,通过管理器间接调用组件指针,以及使用类似C#中event delegate的方式,个个组件注册,通过消息管理器来触发Components内的方法。例如:http://www.unifycommunity.com/wiki/index.php?title=Advanced_CSharp_Messenger

发表评论

电子邮件地址不会被公开。

Copyright © 2011-2017 AI分享站    登录