还记得我们的第一个小游戏–忍者来袭吗?
那个有点太Easy了,敌人对主角一点威胁都没有。那我们今天就改造一下,让敌人也会发子弹,而且一旦敌人的子弹或者敌人碰到主角的话,就Game Over.
首先,我们想一下如何设计,让敌人也会发飞镖?我想要的效果是敌人在移动的过程中,随机位置发射一枚飞镖。
我最开始的想法是,在addMonster()中,做一个敌人的飞镖精灵,它的起始点是敌人随机移动的位置,终点当然是player的位置。但是,Moster是一直在运动的,如何设定飞镖的起始点让我犯了愁。后来我意识到,飞镖是敌人发出来了,那么可以把发射飞镖作为monster的一个动作,加到动作序列(Sequence)里面啊。于是,用CallFunc写个回调函数shootStar,在shootStar里面写发射飞镖,这样运行起来符合预期而且代码也很容易理解。
那么修改代码思路如下:原来monster只有一个MoveTo的Action,那我们改成MoveTo -> shootStar -> MoveTo这样的动作序列不就结了。而且前半段MoveTo的距离是随机产生的。于是代码如下:
// 2 Let monster run
int maxX = selfContentSize.width;
int randomX = maxX - (rand() % (int)(maxX / 2));
float monsterSpeed = 160;
float randomDuration2 = (float)randomX / monsterSpeed;
float randomDuration1 = (float)(maxX - randomX) / monsterSpeed;
auto move1 = MoveTo::create(randomDuration1, Vec2(randomX, randomY));
auto move2 = MoveTo::create(randomDuration2, Vec2(-monsterContentSize.width / 2, randomY));
// 定义消除的Object。怪物移出屏幕后被消除,释放资源。
auto actionRemove = RemoveSelf::create();
// Shoot Enemy's Projectile
auto shootStar = CallFunc::create([=]() {
// 创建敌人飞镖
Sprite* eProjectile = Sprite::create("Projectile_enemy.png");
eProjectile->setPosition(monster->getPosition());
// Add Enemy's Projectile's physicsBody
auto physicsBody = PhysicsBody::createBox(eProjectile->getContentSize(), PhysicsMaterial(0.0f, 0.0f, 0.0f));
physicsBody->setDynamic(false);
physicsBody->setCategoryBitmask(3);
physicsBody->setContactTestBitmask(4);
eProjectile->setPhysicsBody(physicsBody);
eProjectile->setTag(ENEMY_PROJECTILE);
this->addChild(eProjectile);
// 发射敌人飞镖
float starSpeed = 120;
float starDuration = (float)randomX / starSpeed;
auto projectileMove = MoveTo::create(starDuration, _player->getPosition());
auto projectileRemove = RemoveSelf::create();
eProjectile->runAction(Sequence::create(projectileMove, projectileRemove, nullptr));
});
// 敌人发射飞镖时,停顿一下
DelayTime* delay = cocos2d::DelayTime::create(0.05);
monster->runAction(Sequence::create(move1, delay, shootStar, move2, actionRemove, nullptr));
为了使动作更逼真一点,我在发射飞镖的时候让敌人停顿了一下。
注意,代码里的CallFunc::create([=]
,中括号里一定得是=号,不能用&哦,因为它里面用到了monster这个局部变量。关于这里捕获模式的知识点可以参考这篇博文。好了,看一下效果:
敌人的飞镖也开始发射了,接下来,我们要设计一个相对复杂的碰撞检测(严格来说应该是接触检测)。也就是:
怎么还会有第3条?你认为是理所当然的,程序可不是那么认为。你要知道,player和它的飞镖是两个不同的精灵,而player发射飞镖的瞬间它们是相互接触的。如果设计不恰当,飞镖就会把自己杀死。(我在做的过程中就经历了这场悲剧)
关于如何判断接触检测产生消息回调,请参考我的上一篇博文。
根据我们需求,我设计的setCategoryBitmask和setContactTestBitmask掩码如下:(这个有N多种设计,大家可以多想想,还是比较练脑子的)
&操作之后,若都不为0,产生回调消息;否则不产生回调消息
player: setCategoryBitmask(5); // 0101
setContactTestBitmask(1); // 0001
player's projectile: setCategoryBitmask(5); // 0101
setContactTestBitmask(2); // 0010
monster: setCategoryBitmask(3); // 0011
setContactTestBitmask(1); // 0001
monster's projectile: setCategoryBitmask(3); // 0011
setContactTestBitmask(4); // 0100
好了,有了这个宝典,代码实现就比较容易了。还需要注意的是,我们不需要用真正的物理碰撞,所以需要每个精灵的动态属性设为false,physicsBody->setDynamic(false);
这个在上一篇博文也有介绍。
以player举例就是这样子的:
// Add _player's physicsBody
auto physicsBody = PhysicsBody::createBox(_player->getContentSize(), PhysicsMaterial(0.0f, 0.0f, 0.0f));
physicsBody->setDynamic(false);
physicsBody->setCategoryBitmask(5);
physicsBody->setContactTestBitmask(1);
_player->setPhysicsBody(physicsBody);
_player->setTag(ME);
this->addChild(_player);
最后,当然是修改一下接触消息的处理。其实也比较简单,之前player飞镖的判断不用修改,可以继续使用。再加上player接触的处理就可以了。那我们需要给player也加一个标签。为了调试方便,我给4个精灵都分别加上了标签。
#define MY_PROJECTILE 10
#define ENEMY_PROJECTILE 20
#define ME 100
#define ENEMY 200
GameOver的处理,我是做了一个GameOver的Scene,里面只有一张图片。然后做一下场景切换,就OK了。
bool NA_Level1::onContactBegin(cocos2d::PhysicsContact& contact)
{
auto nodeA = contact.getShapeA()->getBody()->getNode();
auto nodeB = contact.getShapeB()->getBody()->getNode();
if (nodeA && nodeB)
{
int tagA = nodeA->getTag();
int tagB = nodeB->getTag();
log("onContact!! tagA = %d, tagB = %d", tagA, tagB);
// 当player的飞镖碰到敌人或者敌人的飞镖,将它们杀死
if (tagA == MY_PROJECTILE) {
nodeB->removeFromParentAndCleanup(true);
}
if (tagB == MY_PROJECTILE) {
nodeA->removeFromParentAndCleanup(true);
}
// 当player碰到碰到敌人或者敌人的飞镖,GameOver处理
if (tagA == ME || tagB == ME){
log("Game over");
Director::getInstance()->replaceScene(TransitionSlideInT::create(0.2f, GameOver::createScene()));
}
}
return true;
}