Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
原创: 任珊
Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据
Cocos2d-x塔防游戏_贼来了10——选择关卡
前面的章节中我们已经提到过如何实现碰撞检测,本着认真负责的态度,这章我们就将落实到具体的行动上。不过在实现碰撞检测之前,我们会先看看怎样存储敌人,子弹等检测需要的数据。最后会完善一下游戏的功能。总而言之,本章是一篇大杂烩教程,擦屁股的教程,之前教程中漏讲的在这里都会一一讲到。
管理游戏数据
本游戏使用了向量Vector和数组来存储并管理场景中的敌人、子弹、炮塔,以及后面会讲到的一整批敌人信息等等数据。这些变量全局唯一,且时刻变换。例如敌人:此刻场景中有1个敌人,但下一秒可能这个敌人就被射死了或者突破重围闯入了玩家阵地,也有可能下一秒起点处又出来了一个新的敌人,这些情况都是随时可能发生的。
又因为这些数据会应用于多个类中,所以这里我们设计了一个管理游戏数据的单例模式类,意图是保证其他类可以同时访问这个类的数据。
单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。它保证了一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。以下就是我们管理游戏数据的单例类的定义:
class GameManager { public: Vector enemyVector; Vector bulletVector; Vector towerVector; static GameManager* getInstance(); private: static GameManager * instance; };
《设计模式》一书中给出了一种比较认可的单例模式的实现方法:用一个public的静态方法来获取类的唯一实例,这个实例则是一个private的静态指针变量。在GameManager类中,instance就是这一唯一的实例,通过getInstance()方法可以获取到它。方法如下:
GameManager* GameManager::instance; GameManager* GameManager::getInstance() { if (instance == NULL) instance = new GameManager(); return instance; }
了解更多关于单例模式的讲解,请阅读Cocos2d-x设计模式发掘之一:单例模式一文。
小结:GameManager类的实现决定了整个游戏框架拥有良好的结构,我们将通过GameManager类获取游戏数据信息和关卡信息,为全局访问提供保障。
碰撞检测
本游戏的碰撞检测使用了最传统的方法——通过遍历子弹向量和敌人向量,检测两个对象是否相交。在碰撞检测时,会根据检测情况添加特效并销毁相应对象。
这里的检测碰撞并不难,关键点在于检测区域的获取和碰撞后的数据处理。对于不同层次上的节点来说,碰撞检测时必须把它们的坐标位置映射到同一个层中,否则可能无法精准的检测。正如在本游戏中,我们是把子弹作为炮塔的一部分添加到了炮塔精灵上的,所以它的坐标位置是相对于炮塔的,这样子弹与敌人就不在同一个父节点上,检测时如果不处理就会出问题。
说了这么多废话后,我们还是回到正题,来看看下面具体的实现方法吧。
void PlayLayer::CollisionDetection() { GameManager *instance = GameManager::getInstance(); auto bulletVector = instance->bulletVector; auto enemyVector = instance->enemyVector; if(bulletVector.empty() || enemyVector.empty() ){ return; } // 1 Vector enemyNeedToDelete; Vector bulletNeedToDelete; // 2 for (int i = 0; i < bulletVector.size(); i++) { auto bullet = bulletVector.at(i); auto bulletRect = Rect(bullet->getPositionX()+bullet->getParent()->getPositionX()-bullet->getContentSize().width/2, bullet->getPositionY()+bullet->getParent()->getPositionY()-bullet->getContentSize().height/2, bullet->getContentSize().width, bullet->getContentSize().height ); // 3 for (int j = 0; j < enemyVector.size(); j++) { auto enemy = enemyVector.at(j); auto enemyRect = enemy->sprite->getBoundingBox(); // 4 if (bulletRect.intersectsRect(enemyRect)) { enemyNeedToDelete.pushBack(enemy); bulletNeedToDelete.pushBack( bullet); // 5 break; } } // 6 for (EnemyBase* enemyTemp : enemyNeedToDelete) { enemyTemp->enemyExpload(); instance->enemyVector.eraseObject(enemyTemp); } enemyNeedToDelete.clear(); } // 7 for (const auto& bulletTemp : bulletNeedToDelete) { instance->bulletVector.eraseObject(bulletTemp); bulletTemp->removeFromParent(); } bulletNeedToDelete.clear(); }
- 定义待删除子弹和敌人的临时向量变量,当有子弹或敌人需要被删除时,就会把它们插入到这些向量中。
- 遍历子弹向量,计算子弹的占地范围,其中子弹的Rect时要把它的坐标值映射到场景层(PlayerLayer)中,保证能与敌人在同一子节点上。
- 遍历敌人向量,计算敌人的范围。getBoundingBox函数用于获得经过缩放和旋转之后的外框盒大小。
- 判断子弹与敌人是否有交集。如果相交,则把该敌人和子弹添加到待删除的列表中。
- 只要击中敌人,就跳出循环,这也意味着一个bullet只能射击一个敌人。
- 销毁待删除列表中的敌人,并把该敌人从enemyVector中移除,最后清理enemyNeedToDelete。enemyExpload方法将在播放了爆炸效果后销毁敌人,这章后半部分会讲解。
- 从bulletVector中移除待删除列表中的子弹并销毁最后清理bulletNeedToDelete。
完善敌人功能
目前为止,我们的游戏已经添加了敌人、炮塔,并且已经能射杀敌人了。抛开只有一个敌人的问题来看,这里还有一个缺陷就是这里的敌人一枪毙命,死的也太容易了。所以,我们还需要完善一下敌人的功能,让它更经打一些。并且,我们还将为敌人添加代表血量的进度条和爆炸效果。
为了实现以上所说的功能,需要在EnemyBase类中添加以下属性:
CC_SYNTHESIZE(int, maxHp, MaxHp); CC_SYNTHESIZE(int, currHp, CurrHp); CC_SYNTHESIZE(float, hpPercentage, HpPercentage); CC_SYNTHESIZE_READONLY(ProgressTimer*, hpBar, HpBar);
添加血条
添加敌人的血条我们用进度条实现。游戏开发中难免会用到进度条,Cocos2d-x也为我们封装了进度条,虽然不太理想,但要实现基本的一些功能还是很方便的。下面就来向大家介绍下在Cocos2d-x中如何使用进度条ProgressTimer。
void EnemyBase::createAndSetHpBar() { // 1 hpBgSprite = Sprite::createWithSpriteFrameName("hpBg1.png"); hpBgSprite->setPosition(Point(sprite->getContentSize().width / 2, sprite->getContentSize().height )); sprite->addChild(hpBgSprite); // 2 hpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName("hp1.png")); hpBar->setType(ProgressTimer::Type::BAR); hpBar->setMidpoint(Point(0, 0.5f)); hpBar->setBarChangeRate(Point(1, 0)); hpBar->setPercentage(hpPercentage); hpBar->setPosition(Point(hpBgSprite->getContentSize().width / 2, hpBgSprite->getContentSize().height / 3 * 2 )); hpBgSprite->addChild(hpBar); }
- 添加血条背景图片。
- 添加血条进度条。
进度条ProgressTimer有两种类型:一种是环形,一种是条形(包括vertical 和 horizontal),所以使用进度条时需要指明它是哪种类型。setMidpoint方法设置进度条的起始点,(0,y)表示最左边,(1,y)表示最右边,(x,1)表示最上面,(x,0)表示最下面。
setBarChangeRate方法用来设置进度条变化方向的,如果不用变化的方向,则设置该方向为0,否则设置为1。所以(1,0)表示横方向,(0,1)表示纵方向。
ProgressTimer有一个很最要的percentage属性。它代表了当前进度条的进度值。如果要让一个进度条正常的显示出来,那么percentage的值必须大于0。setPercentage方法能设置ProgressTimer的percentage值。
我们会在子弹与敌人的碰撞检测中不断的更新当前敌人血量的进度值和生命值,当敌人的生命值小于等于0时,再移除敌人。如下所示修改CollisionDetection方法。
if (bulletRect.intersectsRect(enemyRect)) { auto currHp = enemy->getCurrHp(); currHp--; enemy->setCurrHp( currHp ); auto currHpPercentage = enemy->getHpPercentage(); auto offHp = 100 / enemy->getMaxHp(); currHpPercentage -= offHp; if(currHpPercentage < 0){ currHpPercentage = 0; } enemy->setHpPercentage(currHpPercentage); enemy->getHpBar()->setPercentage(currHpPercentage); if(currHp <= 0) { enemyNeedToDelete.pushBack(enemy); } bulletNeedToDelete.pushBack( bullet); break; }
爆炸效果
在游戏中,移除死亡角色之前,一般都会伴有牛逼的爆炸效果。本游戏也不例外,我们将在移除敌人之前,播放一则爆炸动画,方法如下:
void Thief::enemyExpload() { hpBgSprite->setVisible(false); sprite->stopAllActions(); unschedule(schedule_selector(Thief::changeDirection)); // 修整爆炸动画的位置,因为它比其他状态都要大 sprite->setAnchorPoint(Point(0.5f, 0.25f)); sprite->runAction(Sequence::create(Animate::create(AnimationCache::getInstance()->getAnimation("explode1")) ,CallFuncN::create(CC_CALLBACK_0(EnemyBase::removeFromParent, this)) , NULL)); }
在该方法中,我们会先暂停敌人所有的动作,并停止方向检测。然后再让敌人播放爆炸动画,播放完后再将它移除。
小结
为了让敌人看起来不那么单一,在第二部分的Demo中我们还添加了另外两种类型的敌人,分别是Pirate和Bandit。它们同Thief类似,只有轻微的差别,所以教程中就不再做过多的描述。
下一章教程我们将围绕第三部分Demo一一讲解,下图是该部分的效果图,敬请关注。