转自:http://www.cnblogs.com/syncg/archive/2013/01/14/2859122.html
在传统OO编程中.区别于其他语言最大的亮点在于继承.这是一把双刃剑.
优点:
缺点:
现在我要加入一个血条.所有的子类都要加上.怎么办呢?你看了看图.把血条的相应方法加到了GameNode.这样所有子类都能用上了.
看着挺美.然后,你又觉得每个子类都要有价格..哦..再加到GameNode 里去.
其实现在已经有问题了.你会发现当你子类公用的类越多.你的父类越来越大.会出现头重脚轻的现象.不过.这还不是关键问题.可能你就是想这样设计:).
继承最大的问题在于.如果我想给Hero 跟 Weapon 加上一个等级的属性怎么办呢?
1.加GameNode 里? Monster本不应该有的属性却有了..
2.再加一个类?让Hero跟Weapn继承? 像下图.
(当出现这样的类结构时.你就要注意了.虽然c++允许多继承.但请记住.如果此类不为子类.一定要是抽象类.具体请看<<Effectvie C++ 2nd>>, 不然.会给你的设计带来无尽的麻烦.)
如果抛开多继承的缺点不谈.还有什么问题?Level适合做父类吗?它明显不满足 IS-A的关系.Hero不是Level. Weapon也不是Level.非要说通.只能以private 去继承.这样:
class Hero :private Level {
//...
};
private 继承的主义是根据某类实现.你还得在Hero与Weapon里封装调用Level的方法才算完.绕了一圈才把一个简单的功能加完.
或者你直接在Hero. Weapon分别写Level的功能.写完后.copy. 这是程序员的禁区.技术含量为0的copy导致日后维护的出错率加倍.
以上问题在开发游戏时尤其明显.角色的属性,物品的属性,场景的属性改来改去.你永远不能一次性写对类之间的继承关系.所以CBSE(Componnet Based Software Engineering)出现了.它要解决的就是这个问题.频繁的需求变动带来的类的不确定性.将其影响降到最低的解决方案.如果你的游戏类基本不变.用继承当然就非常科学.
因为CBSE带来了设计灵活也带来了代码的复杂.
那我们来看看如果,将上面的类图改成CBSE的架构会是什么样子呢.如下.
哦...下巴掉下来了...乱七八糟的类图...但这就是CBSE的主要思想.以组合模式将组件(IComponent)集中在一起形成实体(Entity).或者说将IS-A的关系重组成HAS-A的关系 . 到这里. 我们需要 围绕Componnet的职责范围,Component之间的通信机制做一些探索.
我们先拿出其中一个类来看.比如说Hero类
Hero类组合了4个Component.要以代码表示可能是这样
class Hero:public Entity
{
public :
Move * _move;
Health* _health;
Attack* _attack;
Level* _level;
};
要达到跟继承一样的效果.我们要解决2个问题:
1.怎么通过Hero表现出组件的功能?
比如说,在直接用继承的时候,你可能直接这样调用 hero->attack(hero2);就完成了攻打英雄的动作.在组件里怎么去模拟呢?
2.组件之间怎么互相通信?
比如hero被attack,费了血.怎么通知_health组件呢?
由以上两个问题产生了很多种实现CBSE的分支.我们先以最简单的思路去解决
第一个问题:怎么通过Hero表现出组件的功能?
拿Attack组件举例,直觉的想法就是统一 Attack与Hero之间的接口.形成如下类图
多继承.有点奇怪的样子..不用怕.这里继承的全是接口.安全无毒.通过上面类图.我们就可以在Hero 里实现Attack方法,类似如下
Hero::Attack(Entity* entity_)
{
_attack->Attack(entity_);
}
Attack::Attack(Entity* entity)
{
entity->ConsumeHP(100);
}
第一个问题也算是解决了.但随之而来的就是第二个问题.我怎么通知entity_所包含的Health组件?
理当在我费血那时,所以在Attack()函数里会调用entity_类里的组件_health的ConsumeHP函数.这里又引出另一个问题.我怎么知道它有Health组件呢?
改Attack参数?将Entity改成Hero?明显不行.Component不能依赖于实体.不然就不能发挥组件的功用了.
所以.这个地方也是CBSE变种最多的的地方.因为有n种方法知道这个Entity具有Health组件.(这里最酷的方法当然是通过配置文件)
但我们用一个简单的代码方法.
IHealth* health=dynamic_cast<IHealth*>(entity_);
if(NULL!=health)
{
health->ConsumeHP(100);
}
Health::ConsumeHP(int health_cost_)
{
//这里扣掉血
//...
if(holder)
_holder->OnConsumeHP(cost_); //_holder代表Component 的持有者.这里OnConsumeHP方法就通知到了entity血量的变化.做它相应的操作.
}
OnConsumeHP这种调用是一种监听机制.理所当然也是一个接口.它跟IHealth的关系很紧密.所以放在IHealth里就很好.只不过.它不是纯虚,而是有空实现的虚方法.如果hero想得到调用,就实现它.
写到这.CBSE在你脑里应该已经有了一个简单的雏形.
多个Component组成了Entity.多个Entity组成什么呢? 比如说hero身上有Package,而Package里面有weapon.这两者在划分上都应该属于entity.哪怎么组织它们呢?看下面的类图.你会发现Weapon1,Weapon2,Package,Hero,Health,Attack 冥冥中..以某种关联存在着..你努力试着想把这种结构表达给其他人.你需要一个术语------Composite pattern,也即组合模式. 白话文.就是A类中有B类的引用...A与B实现了同样的接口.
引用维基Composite pattern图片:
所以当你实现这一步的时候.又要想一个问题了.怎么能够精确的索引到我想到的每个节点呢?现在给你一个头节点Hero.怎么方便的得到每个孩子节点的索引呢?比如说拿到weapon1.因为这个索引问题.又引出了许多CBSE的变种.. 没错..只要你实现了这个功能.就是对的.没有唯一的方法.你可以给每个节点打上tag.你可以将这个树状结构放入hashmap.
基于接口的组件实现在项目规模不大时很不错.但如果规模一大.比如说一个entity类有20个组件...难道要继承20个接口..?当然.地球人阻止不了你.但第二部份我将介绍另外一种CBSE.请稍等几年.
基本的CBSE我已经全部讲完了.接下来的文章我将写一个比较酷的CBSE变种.