Cocos2d-x塔防游戏_贼来了4——创建炮塔

原创: 任珊


Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据
Cocos2d-x塔防游戏_贼来了10——选择关卡

上一章我们向程序中添加了一个沿着地图固定路径行走的小偷。但到目前为止,我们的塔防游戏是还不能被称之为一个游戏的,它的基本逻辑部分都还没有成形,所以本节我们将紧接着上节的内容,继续介绍如何制作一款塔防游戏。

本部分游戏demo代码下载地址:https://github.com/iTyran/thiefTD/tree/part2

运行该部分demo,你将看到如下所示的效果图:

在本部分的代码中,涉及的内容如下:

  • 创建炮塔(包括它发射的子弹);
  • 触摸响应,实现炮塔的添加;
  • 碰撞检测敌人是否被子弹击中;
  • 完善敌人类,添加血条和死亡动画。

本章我们会先来创建炮塔。

炮塔

同创建敌人类似,在一个塔防游戏中会有各种不同类型的炮塔,所以,我们同样先来创建一个炮塔的基类,再在这个基类上扩展其他不同种类的子炮塔。

创建炮塔基类

塔防游戏中,炮塔的种类莫过于以下几种:魔法塔,攻击塔(又细分为箭塔,大炮等),减速塔,以及具有和都敏俊兮同样功能的时间冻结塔。这些炮塔属性大多都不同,但也有一些相同的属性,如:作用范围,杀伤力,发弹速率(时间间隔)等。粗劣了解了炮塔的这些特征以后,现在我们就可以开始来创建炮塔的基类了。

我们先来创建了一个叫做TowerBase的基类,下面是其定义:

class TowerBase: public Sprite
{
public:
    TowerBase();    
    virtual bool init();
    CREATE_FUNC(TowerBase);    
    void checkNearestEnemy();

    CC_SYNTHESIZE(int, scope, Scope);  // 塔的视线范围
    CC_SYNTHESIZE(int, lethality, Lethality);   // 杀伤力
    CC_SYNTHESIZE(float, rate, Rate);    
protected:
    EnemyBase* nearestEnemy;    // 塔子视野内最近的敌人
};

对于一个炮塔而言,它会不停的搜索视野范围内离自己最近的敌人,然后对其发动攻击。所以我们只要把敌人存储在向量Vector中并让炮塔遍历这个向量就能检测到离它最近的敌人。

除此之外,游戏中敌人的数量是不停变换的,所以在炮塔检测视野内最近的敌人时,我们需要时刻获得最新的敌人列表,这样才能确保准确的检测到最近的敌人。不用多说,这个敌人Vector应该是一个全局变量。

checkNearestEnemy方法用来检测离炮塔最近的敌人,其代码如下:

void TowerBase::checkNearestEnemy()
{
    // 1
    GameManager *instance = GameManager::getInstance();
    auto enemyVector = instance->enemyVector;
    // 2
    auto currMinDistant = this->scope;
    // 3
    EnemyBase *enemyTemp = NULL;
    for(int i = 0; i < enemyVector.size(); i++)
    {
        auto enemy = enemyVector.at(i);
        auto distance = this->getPosition().getDistance(enemy->sprite->getPosition());
        if (distance < currMinDistant) {
            currMinDistant = distance;
            enemyTemp = enemy;
        }
    }
    // 4
    nearestEnemy = enemyTemp;
}
  1. 获得敌人的向量列表,这里GameManager就是获得最新的敌人向量列表的关键所在,这将在后续章节中详细介绍。这里你只要记住它是一个单例模式的类,enemyVector值全局唯一就可以了。
  2. 初始化当前射击的最近距离。因为要求敌人在炮塔的视线范围内才发动攻击,所以初始化currMinDistant为炮塔的视线范围(scope)。
  3. 遍历敌人向量,更新当前距离炮塔最近敌人的这段距离,并记录下该敌人。
  4. 遍历完整个向量后,得到最近的敌人。

创建箭塔

完成基类建设以后,接下来我们来创建一个最普通的炮塔——ArrowTower 箭塔。

一个箭塔最起码应该由以下的三部分组成,1)箭塔底座,2)弓箭,3)子弹。如下图所示:

这里底座是要求不动的;弓箭安放于底座靠上的位置处,它会根据敌人的方向旋转;子弹在弓箭处产生且它的初始方向应与弓箭保持一致。根据这些,我们就可以开始创建箭塔了。

在init方法中添加如下代码初始化箭塔:

    setScope(90);
    setLethality(1);
    setRate(2);

    auto baseplate = Sprite::createWithSpriteFrameName("baseplate.png");
    addChild(baseplate);

    rotateArrow = Sprite::createWithSpriteFrameName("arrow.png");
    rotateArrow->setPosition(0, baseplate->getContentSize().height /4  );
    addChild(rotateArrow);

初始化它之后,我们现在来看看箭塔怎样旋转射击,其逻辑行为可分为以下3个阶段:

一、旋转弓箭,等待射击。

当敌人进入箭塔的视线范围内时,弓箭会开始围绕距离它最近的敌人旋转。敌人跑到哪边,弓箭就指向哪边,一直瞄准它。下面是该方法的实现:

void ArrowTower::rotateAndShoot(float dt)
{
    // 1
    checkNearestEnemy();
    if (nearestEnemy != NULL)
    {   // 2
        Point rotateVector = nearestEnemy->sprite->getPosition() - this->getPosition();
        float rotateRadians = rotateVector.getAngle();
        float rotateDegrees = CC_RADIANS_TO_DEGREES(-1 * rotateRadians);
        // 3
        float speed = 0.5 / M_PI;
        float rotateDuration = fabs(rotateRadians * speed);
        // 4
        rotateArrow->runAction( Sequence::create(RotateTo::create(rotateDuration, rotateDegrees),
                                                 CallFunc::create(CC_CALLBACK_0(ArrowTower::shoot, this)),
                                                 NULL));
    }
}
  1. 检测炮塔视线范围内距离它最近的敌人。
  2. 如果最近的敌人nearestEnemy存在,弓箭则会旋转,所以我们需要计算弓箭旋转的角度和旋转时间。
    关于旋转角度,可以利用三角正切函数来计算,如下图所示:

    炮塔与敌人的之间的角度关系可以表示为: tan a = offY/offX,而rotateVector =(offX,offY)。
    getAngle方法将返回rotateVector向量与X轴之间的弧度数。但旋转弓箭我们需要的是角度,所以这就需要把弧度rotateRadians转化为角度。不过还好,Cocos2d-x中提供了能把弧度转化为角度的宏CC_RADIANS_TO_DEGREES,这样我们就可以很方便的转化了。
    另外,Cocos2d-x中规定顺时针方向为正,这显然与我们计算出的角度方向相反,所以转化的时候需要把角度a变为-a。
  3. 计算旋转时间。
    speed表示炮塔旋转的速度,0.5 / M_PI其实就是 1 / 2PI,它表示1秒钟旋转1个圆。
    rotateDuration表示旋转特定的角度需要的时间,计算它用弧度乘以速度。
  4. 让弓箭顺序执行旋转动作和shoot方法。为了让旋转和射击保持同步,所以这里我们需要先让弓箭旋转,再允许执行射击shoot方法。

二、生成子弹,发动射击

子弹的起始位置和角度要求与弓箭保持一致,创建子弹的代码如下;

Sprite* ArrowTower::ArrowTowerBullet()
{
    Sprite* bullet = Sprite::createWithSpriteFrameName("arrowBullet.png");
    bullet->setPosition(rotateArrow->getPosition());
    bullet->setRotation(rotateArrow->getRotation());
    addChild(bullet);    
    return bullet;
}

当弓箭瞄准敌人后,在弓箭处生成一颗子弹,发动射击,代码如下:

void ArrowTower::shoot()
{
    GameManager *instance = GameManager::getInstance();
    auto bulletVector = instance->bulletVector;

    if(nearestEnemy!=NULL && nearestEnemy->getCurrHp() > 0 )
    {
        auto currBullet = ArrowTowerBullet();
        instance->bulletVector.pushBack(currBullet);

        auto moveDuration = getRate();
        Point shootVector = nearestEnemy->sprite->getPosition() - this->getPosition();
        Point normalizedShootVector = -shootVector.normalize();

        auto farthestDistance = Director::getInstance()->getWinSize().width;
        Point overshotVector = normalizedShootVector * farthestDistance;
        Point offscreenPoint = (rotateArrow->getPosition() - overshotVector);

        currBullet->runAction(Sequence::create(MoveTo::create(moveDuration, offscreenPoint),
                                               CallFuncN::create(CC_CALLBACK_1(ArrowTower::removeBullet, this)),
                                               NULL));
        currBullet = NULL;
    }
}

在后面的碰撞检测中,我们需要检测子弹是否射中了敌人,所以同敌人一样,这里我们要把创建的子弹添加到一个子弹向量中,方便下一步的碰撞遍历。依旧使用单例模式GameManager来得到这个子弹列表bulletVector。

如果有敌人在箭塔的视线范围内,且它的生命值不为0,则创建子弹,射向最近的敌人。换句话说,这里我们的重点是要计算子弹的执行MoveTo动作的两个参数。
子弹的最大射程长度我们定为屏幕的宽,移动这段距离的时间(可理解为子弹发弹速率)通过getRate方法得到。超出该射程的子弹将被销毁。

最终位置 = 起始位置 - 单位向量 * 射程长度 。

三、销毁子弹

最后一阶段是销毁超出射程的子弹,释放内存。代码如下:

void ArrowTower::removeBullet(Node* pSender)
{
    GameManager *instance = GameManager::getInstance();    
    auto bulletVector = instance->bulletVector;

    Sprite *sprite = (Sprite *)pSender;
    instance->bulletVector.eraseObject(sprite);
    sprite->removeFromParent();
}

这样一来我们的箭塔就创建好了,接下来再来看一看另一种牛逼的多方向攻击塔。

多方向攻击塔

其原理和箭塔类似,就不做赘述。不同的是该塔会同时朝六个方向发射子弹,其逻辑行为也只有2个阶段,第一阶段会创建子弹,朝六个方向射击,第二阶段就是销毁子弹。下面来看朝六个方向射击的方法。

void MultiDirTower::createBullet6(float dt)
{
    GameManager *instance = GameManager::getInstance();
    auto bulletVector = instance->bulletVector;
    int dirTotal = 6;
    this->checkNearestEnemy();
    if(nearestEnemy != NULL && nearestEnemy->getCurrHp() > 0 )
    {
        for(int i = 0; i < dirTotal; i++)
        {
            auto currBullet = MultiDirTowerBullet();
            instance->bulletVector.pushBack(currBullet);
            auto moveDuration = getRate();

            Point shootVector;
            shootVector.x = 1;
            shootVector.y = tan( i * 2 * M_PI / dirTotal );
            Point normalizedShootVector;
            if( i >= dirTotal / 2 )
            {
                normalizedShootVector = shootVector.normalize();
            }else{
                normalizedShootVector = -shootVector.normalize();
            }       
            auto farthestDistance = Director::getInstance()->getWinSize().width;
            Point overshotVector = normalizedShootVector * farthestDistance;
            Point offscreenPoint = (currBullet->getPosition() - overshotVector);

            currBullet->runAction(Sequence::create(MoveTo::create(moveDuration, offscreenPoint),
                                                   CallFuncN::create(CC_CALLBACK_1(MultiDirTower::removeBullet, this)),
                                                   NULL));
            currBullet = NULL;
        }
    }
}

该方法主要通过计算不同方位子弹的单位向量来求得它的运动轨迹。它也也适合朝4个方向,8个方向等偶数位方向开火的炮塔,只要把dirTotal参数改了就OK。

小结

在本部分程序中一共创建了3中类型的炮塔,实现方法大同小异,就不过多讲解了。总的来说,炮塔类的设计仰仗于其基类的设计,对于该游戏中的各种炮塔来说,它们在功能上有所区别,各有其特点,这样的设计使得也使得游戏层次更加丰富,同时也可以增强玩家排兵布阵的趣味性。

下一章中我们将把创建好的炮塔添加到场景中去,通过触摸屏幕实现添加,敬请期待。

标签: cocos2d-x教程

?>