推荐看2篇讲述飞机游戏的绝好文章:
地球人04 http://www.cocoachina.com/gamedev/gameengine/2012/0618/4367.html
笨木头04 http://blog.csdn.net/musicvs/article/details/8135705#comments
类图基础知识:http://blog.csdn.net/qxbailv15/article/details/9113283
上面2篇文章的组合设计替代继承的思想非常精彩,虽然刚开始实现会麻烦点,但是后期,绝对越来越简单,需要什么,组装什么。麻雀虽小,五脏齐全,千万别小看了这些设计。对面向对象能力是个很好的考验。好的设计不会让自己走着走着就再也走不动了。花了刚好一星期,写了个MySpaceWar1.0,同样是飞行游戏,这是写的第一个android游戏,平常上课下课的时候就可以自己玩自己的游戏了,真棒,哈哈。
我的类图基本上是这样的:
在C++ 中没有接口类,可以用抽象类模拟,即被画成紫色的类。 采用PowerDesigner 画类图。(其实我把关联和复合混淆了,把它当成了一回事。A类关联B类,是指A类里有B类的指针,而复合是不仅A类里有B类的指针,而且他们是同时产生的,比如小鸟和它的翅膀,翅膀指针需要在小鸟的构造函数里去赋值,但是玩家和控制器就不同了,它们是关联的关系,控制器不是玩家本身的一部分,不能在玩家构造函数的里面进行初始化,而应当在玩家的init函数里调用额外的函数setControlller()
)
我的MySpaceWar1.0版 的刚开始的设计也是基于上面2篇文章的思想,多次企图修改,但最好不得不改回去,因为我考虑的不够多,最后发现基本改不了。只能一步步和作者走向重合。但是还是觉得他们的关于子弹管理器的设计过于复杂了,互相设置监听类的方法,难于为所有怪物加上子弹管理器,非常复杂。既要考虑到数据的封装,又要考虑接口的合理性,不乱设计方法的参数的情况下为怪物装上子弹管理器,让怪物有发射子弹的能力是很难的。 于是,必须稍微放弃点作者原有的设计,才能打破僵局, 也就是说需要重构。之所以局部 重构的理由是我要为每只怪物都能复合(组合)一个子弹管理器,让怪物也能有发射子弹功能。用原有的监听模式,过于复杂,只能一时的想清楚整个逻辑,但是过一会儿,又把这些设计逻辑忘了。所以必须有一个简单易懂的设计,无用的抽象类删掉。
于是我试着设计了个碰撞管理器类(CollisionManager),现在有3个管理类了,怪物管理器类,子弹管理器类。 当然我们应该让 itemsLayer 去 复合 碰撞管理器类(CollisionManager)。
删掉了他们原先的Shooter抽象类,CollisionListener抽象类,BulletListener抽象类。其实这些设计监听者的核心思想是,被监听者复合监听者的指针,然后在被监听者的update()函数里,通过复合的监听者指针不断去调用监听者的函数,通过这样模拟一种“回调函数”的效果。很值得学习,但是却不能滥用。
同时让Collidable类,多一个方法CollisionDectected().
class Collidable { public: virtual bool isCollidedWith(Collidable* target) = 0; // 这里是判断是否和其他可碰撞的对象碰撞 virtual CCPoint GetPosition() = 0; // 此函数放在这似乎不太合适,任何entity按道理自己应该有这2个函数,但是isCollidedWith的函数实现会告诉你你不得不这么做,不然就得用dynamic_cast 强制转换为子类指针后调用GetPosition()和CollisionDetected(),2种选其一啦。 virtual const CCSize GetContainSize() = 0; virtual void CollisionDetected() = 0; // 2013年12月24日12:37:54 add };
只需要传进player*和 monsterManager* 指针,同时让CollisonManager成为Player,MonsterManager,BulletManager它们的友元类。 让CollisonManager要风得风,要雨得雨,取上将的头颅如探囊取物尔。不然CollisionManager 无法获取他们的数据,就没办法统一管理了。
或许我们会想到让MonsterManager实现一个方法GetMonsters()获取其怪物数组,但是这样的话,monsters数组就和没封装一样的,暴露给了所有其他类。 所以我能想到的最好的办法是只暴露给CollisionManager ,毕竟它为你承担了碰撞检测的艰巨的负担,让人家成为你的朋友——友元类,不过分吧。同样让CollisionManager成为BulletManager,Player的朋友.
void CollisionManager::update(float dt) // 做3种情况的碰撞检测,会不会导致还没检测到,子弹就已经穿过怪物了,尤其是手机比较卡的时候 { /* 1.检测player 和 monster是否碰撞了 */ CCObject* obj = NULL; CCObject* obj2 = NULL; CCARRAY_FOREACH(mMonsterManager->mMonsterArray, obj ) { Monster* monster = (Monster*)obj; if (monster && monster->getIsAlive() && monster->isCollidedWith(mPlayer) ) { monster->CollisionDetected(); mPlayer->CollisionDetected(); } } CCARRAY_FOREACH( mMonsterManager->mMonsterArray,obj) { Monster* monster = (Monster*)obj; CCARRAY_FOREACH( monster->mBulletManager->mBulletArray,obj2) { Bullet* bullet = (Bullet*)obj2; if (bullet && bullet->getIsAlive() && mPlayer && mPlayer->isCollidedWith(bullet)) { mPlayer->CollisionDetected(); bullet->CollisionDetected(); } } } /* 3.monster 和 player 的子弹碰撞 */ CCARRAY_FOREACH(mPlayer->mBulletManager->mBulletArray, obj) { Bullet* bullet = (Bullet*)obj; if (bullet && bullet->getIsAlive()) // 用if稍微过滤下,减少调用次数 { CCARRAY_FOREACH(mMonsterManager->mMonsterArray, obj2) { Monster* monster = (Monster*)obj2; if (monster->getIsAlive() && monster->isCollidedWith(bullet)) { monster->CollisionDetected(); mPlayer->mScore ++ ; // 让玩家的积分 + 1 mPlayer->refreshUiValue(); // 刷新得分 bullet->CollisionDetected(); } } } }
因为子弹的精灵图片有多种,比如player和monster的子弹就不一样,如何 让一个bullet类既能承担player的子弹的显示的精灵,又能显示monster的子弹的精灵呢。或许很容易想到用2个子类继承bullet,不就行了。这样当然不行,假如有10种子弹呢,岂不是要10个继承自bullet子类了。我想的办法就是通过在bullet类里传入枚举类型,构造时先判断这是谁的子弹,来响应地用哪种图片。总之利用枚举,可以达到一个类适合多种情况的效果。如下:
class Bullet : public Entity { public: enum BULLET_TYPE { PLAYER_BULLET = 1, MONSTER_BULLET }; bool init(CCSpriteBatchNode* batchNode, BULLET_TYPE type); static Bullet* createWithBatchNode(CCSpriteBatchNode* batchNode, BULLET_TYPE type); virtual void CollisionDetected(); };
bool Bullet::init(CCSpriteBatchNode* batchNode, BULLET_TYPE type) { bool bRet = false; do { char fileName[60] = {0}; sprintf(fileName, "bullet_%02d.png", type); // 没有写死,传个枚举打破僵局,也避免了if else 的可维护性差 // CCSprite* bulletSprite = CCSprite::createWithSpriteFrameName("bullet_01.png"); 写死了 CCSprite* bulletSprite = CCSprite::createWithSpriteFrameName(fileName); CC_BREAK_IF(! bulletSprite); batchNode->addChild(bulletSprite); this->setVisual(bulletSprite); /* 为子弹加上一个简单移动控制器,让它能飞起来 */ SimpleMoveController* moveController = SimpleMoveController::create(); if ( type == PLAYER_BULLET ) { moveController->setYSpeed(400); } else if (type == MONSTER_BULLET) { moveController->setYSpeed(-200); } else { ; } this->setController(moveController); bRet = true; } while (0); return bRet; }
更多知识和设计上的体会以后有时间再多写点吧。下面是游戏部分截图:
菜单场景
游戏场景