转载至:http://cn.cocos2d-x.org/tutorial/show?id=1108
上一章我们学习了基础知识,讲解了地图的创建加载,并向程序中添加了一个沿着地图固定路径行走的小偷。但到目前为止,我们的塔防游戏是还不能被称之为一个游戏的,它的基本逻辑部分都还没有成形,所以本节我们将紧接着上节的内容,继续介绍如何制作一款塔防游戏。
下载本部分游戏Demo代码。
运行该部分demo,你将看到如下所示的效果图:
在本部分的代码中,涉及的内容如下:
-
创建炮塔(包括它发射的子弹);
-
触摸响应,实现炮塔的添加;
-
碰撞检测敌人是否被子弹击中;
-
完善敌人类,添加血条和死亡动画。
本章我们会先来创建炮塔。
炮塔
同创建敌人类似,在一个塔防游戏中会有各种不同类型的炮塔,所以,我们同样先来创建一个炮塔的基类,再在这个基类上扩展其他不同种类的子炮塔。
创建炮塔基类
塔防游戏中,炮塔的种类莫过于以下几种:魔法塔,攻击塔(又细分为箭塔,大炮等),减速塔,以及具有和都敏俊兮同样功能的时间冻结塔。这些炮塔属性大多都不同,但也有一些相同的属性,如:作用范围,杀伤力,发弹速率(时间间隔)等。粗劣了解了炮塔的这些特征以后,现在我们就可以开始来创建炮塔的基类了。
我们先来创建了一个叫做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; } |
-
获得敌人的向量列表,这里GameManager就是获得最新的敌人向量列表的关键所在,这将在后续章节中详细介绍。这里你只要记住它是一个单例模式的类,enemyVector值全局唯一就可以了。
-
初始化当前射击的最近距离。因为要求敌人在炮塔的视线范围内才发动攻击,所以初始化currMinDistant为炮塔的视线范围(scope)。
-
遍历敌人向量,更新当前距离炮塔最近敌人的这段距离,并记录下该敌人。
-
遍历完整个向量后,得到最近的敌人。
创建箭塔
完成基类建设以后,接下来我们来创建一个最普通的炮塔——ArrowTower 箭塔。
一个箭塔最起码应该由以下的三部分组成,1)箭塔底座,2)弓箭,3)子弹。如下图所示:
这里底座是要求不动的;弓箭安放于底座靠上的位置处,它会根据敌人的方向旋转;子弹在弓箭处产生且它的初始方向应与弓箭保持一致。根据这些,我们就可以开始创建箭塔了。
在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存在,弓箭则会旋转,所以我们需要计算弓箭旋转的角度和旋转时间。
关于旋转角度,可以利用三角正切函数来计算,如下图所示:
炮塔与敌人的之间的角度关系可以表示为: 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。
小结
在本部分程序中一共创建了3中类型的炮塔,实现方法大同小异,就不过多讲解了。总的来说,炮塔类的设计仰仗于其基类的设计,对于该游戏中的各种炮塔来说,它们在功能上有所区别,各有其特点,这样的设计使得也使得游戏层次更加丰富,同时也可以增强玩家排兵布阵的趣味性。
下一章中我们将把创建好的炮塔添加到场景中去,通过触摸屏幕实现添加,敬请期待。