基于组件的游戏编程 CBSE(Componnet Based Software Engineering)

转自:http://www.cnblogs.com/syncg/archive/2013/01/14/2859122.html


在传统OO编程中.区别于其他语言最大的亮点在于继承.这是一把双刃剑.

优点:

  1. 将数据与逻辑组织的更紧密.
  2. 更进一步的强化了代码与现实的对应关系.

缺点:

  1. 当继承树达到一定规模后.要改某个节点的功能将会很麻烦,我以游戏举例.因为游戏里的类创造性很大.变化也很大.
    看下面类图.这种设计很常见.GameNode里有最基本的属性.比如postion x, y. scale 等等,然后是Living,活物. Item物品.等等.
     

image

 

   现在我要加入一个血条.所有的子类都要加上.怎么办呢?你看了看图.把血条的相应方法加到了GameNode.这样所有子类都能用上了.

image

   看着挺美.然后,你又觉得每个子类都要有价格..哦..再加到GameNode 里去.

image

其实现在已经有问题了.你会发现当你子类公用的类越多.你的父类越来越大.会出现头重脚轻的现象.不过.这还不是关键问题.可能你就是想这样设计:).

继承最大的问题在于.如果我想给Hero 跟 Weapon 加上一个等级的属性怎么办呢?

1.加GameNode 里? Monster本不应该有的属性却有了..

2.再加一个类?让Hero跟Weapn继承? 像下图.

image 
(当出现这样的类结构时.你就要注意了.虽然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的架构会是什么样子呢.如下.

image

哦...下巴掉下来了...乱七八糟的类图...但这就是CBSE的主要思想.以组合模式将组件(IComponent)集中在一起形成实体(Entity).或者说将IS-A的关系重组成HAS-A的关系 . 到这里. 我们需要 围绕Componnet的职责范围,Component之间的通信机制做一些探索.

我们先拿出其中一个类来看.比如说Hero类

 

image

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之间的接口.形成如下类图

 

image

 

多继承.有点奇怪的样子..不用怕.这里继承的全是接口.安全无毒.通过上面类图.我们就可以在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想得到调用,就实现它.

 

image

写到这.CBSE在你脑里应该已经有了一个简单的雏形.

多个Component组成了Entity.多个Entity组成什么呢? 比如说hero身上有Package,而Package里面有weapon.这两者在划分上都应该属于entity.哪怎么组织它们呢?看下面的类图.你会发现Weapon1,Weapon2,Package,Hero,Health,Attack 冥冥中..以某种关联存在着..你努力试着想把这种结构表达给其他人.你需要一个术语------Composite pattern,也即组合模式.  白话文.就是A类中有B类的引用...A与B实现了同样的接口.

image

引用维基Composite pattern图片:

image

 

所以当你实现这一步的时候.又要想一个问题了.怎么能够精确的索引到我想到的每个节点呢?现在给你一个头节点Hero.怎么方便的得到每个孩子节点的索引呢?比如说拿到weapon1.因为这个索引问题.又引出了许多CBSE的变种.. 没错..只要你实现了这个功能.就是对的.没有唯一的方法.你可以给每个节点打上tag.你可以将这个树状结构放入hashmap.

基于接口的组件实现在项目规模不大时很不错.但如果规模一大.比如说一个entity类有20个组件...难道要继承20个接口..?当然.地球人阻止不了你.但第二部份我将介绍另外一种CBSE.请稍等几年.

基本的CBSE我已经全部讲完了.接下来的文章我将写一个比较酷的CBSE变种.

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值