四、创建炮塔

转载至:http://cn.cocos2d-x.org/tutorial/show?id=1108


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


下载本部分游戏Demo代码。


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

result1.png

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

  • 创建炮塔(包括它发射的子弹);

  • 触摸响应,实现炮塔的添加;

  • 碰撞检测敌人是否被子弹击中;

  • 完善敌人类,添加血条和死亡动画。


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


炮塔

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


创建炮塔基类

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


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

1
2
3
4
5
6
7
8
9
10
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方法用来检测离炮塔最近的敌人,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)子弹。如下图所示: 

搜狗截图14年07月11日1502_5.jpg

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


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

1
2
3
4
5
6
7
8
9
10
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个阶段:


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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存在,弓箭则会旋转,所以我们需要计算弓箭旋转的角度和旋转时间。 


关于旋转角度,可以利用三角正切函数来计算,如下图所示: 

搜狗截图14年07月11日1504_6.jpg

炮塔与敌人的之间的角度关系可以表示为: 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方法。


二、生成子弹,发动射击

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

1
2
3
4
5
6
7
8
Sprite* ArrowTower::ArrowTowerBullet()
{
     Sprite* bullet = Sprite::createWithSpriteFrameName( "arrowBullet.png" );
     bullet->setPosition(rotateArrow->getPosition());
     bullet->setRotation(rotateArrow->getRotation());
     addChild(bullet);    
     return  bullet;
}



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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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方法得到。超出该射程的子弹将被销毁。


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


三、销毁子弹

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

1
2
3
4
5
6
7
8
9
void  ArrowTower::removeBullet(Node* pSender)
{
     GameManager *instance = GameManager::getInstance();    
     auto bulletVector = instance->bulletVector;
 
     Sprite *sprite = (Sprite *)pSender;
     instance->bulletVector.eraseObject(sprite);
     sprite->removeFromParent();
}

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


多方向攻击塔

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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。

multiShoot1.png


multiShoot.png


小结

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


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


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值