以前写过一篇关于组合式实体的文章(这里),讨论了组合式实体的架构设计,以及和传统的继承式实体架构的区别和优势,最近在项目中,由于更多的做引擎方面的工作,所以有机会更多思考,设计和实现这样的架构,并在实践中总结了许多实际的使用经验,因此来和大家分享一下,不是很复杂,但相信会比较实用。组合式实体架构的英文是Component-based Entity System,我在下文会简略的称为CBES。这种架构可以看成是基于组件的软件工程(Compnent-based Software Engineer,CBSE)在游戏中的实践。另外,对于传统继承式实体架构,下文会简称为EHS(Entity Hierarchy System)。
一个游戏中的实体可以分为两个部分,一个是属性部分(Property),一个是行为部分(Behavior)。
属性部分是对这个实体的描述,比如考虑一个人的属性,会有姓名,年龄,身高,体重等等,对于每种实体来说,他的属性都是不同的,比如一个人和一个桌子肯定会使用不同的属性来描述,对于同一种实体来说,他们的属性应该是一致的,仅仅是属性的值会有不同,这就是同一种实体的个体差异。这也是很好理解的。当我们为实体设计数据的段的结构和接口时,这些特性是哦我们需要考虑的。基于上面的描述,使用黑板是不错的选择,用key-value对来方便表述。
行为部分的定义比较广义,它描述了这个实体可以有的一些行为,比如呈现外观(Rendering),控制动画(Animation),思考(AI)等等,我特别的为这些行为用一个动词描述,来强调行为和属性的区别,行为重视于控制和改变,属性重视于描述。实体上的所有行为应该可以直接访问实体的属性(读或者写),在CBES中,这些行为就被用一个个组件(Component)来表现。
下面的这个伪代码描述了一种可能的实体类定义,其中的update函数是这个实体的主循环,会依此更新它所有的组件
当定义一个实体的时候,我们需要为他调用addProperty添加属性和初始值(Any类型),并且用addComponent配置组件,这样的实体既可以作为单个实体的实例,也可以作为(借用Unity的叫法)预制件(Prefab),换句话说,就是作为一个实体模版(Template),以后可以用来生成更多的实例(Instance)。如下图,定义了描述一个人的实体,添加了名字,年龄属性,并且添加了渲染,动画和AI组件:
可以看到这是一个很机械化的过程,我们完全可以用数据驱动(Data -driven)的方式来配置这样的实体,比如用XML,或者其他的配置文件,甚至是自定义的脚本,如果更好的话可以用可视化的方式在定义,比如Unity那样,在界面上可以把组件拖拽到一个物体上。如果没时间做复杂的编辑器的话(就像我:S),用XML是我比较推荐的方式,简单,快速,解析起来也很方便,我在项目的引擎中就用了这种方式,如下图
有了这样的结构,我想大家都能知道如何去解析了吧,如果引擎能支持反射机制的话,那会更加的简单。
说完了实体的构建,下面来谈谈实体和组件,组件和组件,实体和实体间通信的问题,前面我说到,组件定义了这个实体上的行为,行为是用来影响和控制这个实体的属性的,所以我们需要考虑如何让组件和属性通信,另外组件与组件间,实体与实体间也存在通信的需求,这些接口应该如何来定义?这是我一直思考的问题,一方面要考虑到耦合性低,减少代码的混乱,一方面还要尽可能的保证接口的易用性,在项目中,我在这个地方重构了多次,并参考了一些其他引擎的设计以及对于CBES探讨的文章,总结下来,感觉下面这样的接口设计较为合理:
实体<->组件:实体和组件的通信的核心是实体的属性数据,属性可以看成是实体和组件的共享数据,所以在组件的类中,我会传入一个实体的引用,可以让组件直接访问(读或写)实体的属性。这样的访问是比较高效的,唯一需要注意的是,如果要复用某个组件,该组件的实现中就不能直接默认认为该实体一定存在某个属性,为此,我在实体的类中定义了两个函数,一个是hasProperty用来检查是否存在某个属性,另一个是在getProperty里多加了一个参数,当不存在该属性的时候,可以返回一个默认值。另外,为了访问的安全,还需要定义对于属性的只读访问器和可写访问器,这里就不再赘述。
一般情况下,实体不会去访问某个特定的组件,因为从设计上来说,实体是不应该默认假设它身上存在某个组件的。
组件<->组件:细心的同学可能注意到,在实体的类定义中,我并没有定义一个获取组件(getComponent)的接口,因为我并不希望暴露组件的内容给外部,虽然获取组件在某种情况下可能会很方便(特别是处理组件和组件间通信时),但会导致被滥用。所以,我在设计中把组件间访问全部通过事件的形式来处理,如果组件希望和别的组件有通信,就向实体派发事件,再由实体向它所有的组件派发该事件。所以每一个实体和组件都应有处理事件回调的能力,可以让实体类和组件类都嵌入引擎的事件系统中,在这里我让他们都继承自一个EventObject的类,如下面这个例子,在一个实体上绑定GirlComponent和BoyComponent,BoyComponent就可以通过向Entity发送消息,并由Entity转发,来和GirlCompnent通信。
实体<->实体:如果是访问实体的属性,可以直接通过getProperty的接口,另外因为我们有了上面的消息系统,所以更可以通过向实体发送消息的方式,来和其他实体交互,进而和实体的组件交互,通过这两个接口基本能满足所有的应用需求。我不太赞成在一个组件中直接去取得另一个实体的组件中的变量(通过罪恶的getComponent)接口,用Unity的同学,我也不建议这样做,虽然它并没有限制这样做,所以如果想更好的使用Unity的组件架构的话,我个人还是建议多看一些CBES相关的文章,来更好的组织组件和游戏逻辑架构,要不很容易就一团乱了。
好,这次先聊到这里,希望对大家有帮助,还有诸如CBES的性能和优化,以及如何去组织和设计实体的组件,留待下次再聊了。
PS:我对Unity的认识完全是基于它的文档和示例代码,如说的有不对的地方,欢迎指正~
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————
我最近也在设计类似代码,实体-组件形式来组织对象,和博主的解决方案比较相识,但是做到后面发现太过于依赖事件,如果一个组件被复用次数太多,通过对实体的引用来通信也比较麻烦,而且实体对组件还只能通过事件, 后来我增加了一配置和一个接口,接口只有一个map参数,接口默认是现在实体和组建的基类里面,当一个组件被加进来,在基类就会互相引用实体和组建的这个接口,然后通过map的key来进行功能路由
hi,通过你的描述,我不是很理解,能给再说的详细一点吗?
看到此,对组合式实体理解为:功能的模块化,但是有些疑问,比如 实体与组件的通信,是组件添加实体的引用,这样以来,组件的类型针对性变得很强,假如新建一个“女人”类,其中包含人类的所有属性,并增加“性别”属性,虽然仍然可使用“人类”的组件,但是针对“人类”的组件却无法操纵“女人”类中“性别”这个属性,难道还要在开发针对“女人”的组件吗?
如果实体用到了继承,某些组件的定义确实是会有特定性,不能做到通用,这个就是利弊和易用的权衡
你好,我是华如科技的陈敏杰,我公司经过5年的开发,有一款自主产品XSimStudio仿真平台,应用领域是军事方面,目前我们正在针对行为建模方面进行探索,如果你在北京的话,能否在AI方面与你进行一下沟通交流,可以是有偿的,方便的话可以联系我13910412182,谢谢了。
Helium、Nebula3等几个次世代开源引擎都对组件化提供良好的实践思路。最近在看unity中的PlayMaker,算是可视化状态机中很成熟的方案了,希望能和lz多交流,可惜unity中几个相对成熟的行为树方案都是闭源的,不敢过多用于项目
行为树并不难,完全可以自己实现,只是编辑器和调试器做起来比较麻烦,你说的这个两个引擎,我有时间去看看,谢谢提供的信息,以后多交流:)