钓龟岛保卫战-如何从零开始制作一款iOS塔防游戏(新)
原文在此:http://www.raywenderlich.com/15730/how-to-make-a-tower-defense-game#more-15730
本文由eseedo翻译
和之前的塔防游戏系列不同,该 教程来自Raywenderlich的iOS教程博文,是众网友投票选出的最渴求的教程之一,只是等得有点太久了。
大家可以将这篇教程和之前的塔防系列教程结合起来,制作一款真正属于自己的塔防游戏。
该教程的作者是InfinixSoft的联合创始人和COO Pablo Ruiz。如果想了解更多关于他个人的信息,可以查看他的博客:http://www.pabloruizmobiledev.com
他的twitter账号是:http://www.twitter.com/pabloruiz55
好了,言归正传,现在开始进入教程了~
在iOS的各类游戏中,塔防游戏是最受欢迎的一种,当然,你或许要说这也是被做烂了的一类,whatever。对于塔防狗来说,看着自己的炮塔逐渐变得威力无穷,把入侵的怪兽和太阳军干得毫无还手之力跪舔求饶之际,那种内心的暗爽是无法形容的。游戏本来就是要让玩家爽的吗!
在该教程中,我们将学习如何使用Cocos2D从零开始制作一款塔防游戏。至于为什么用Cocos2D,我想就毫无解释的必要了吧。如果还不太懂Cocos2D,建议可以看看博文中的cocos2d教程。
在该教程中,我们将学到以下内容:
1.如何在设定的时间内让一波波的太阳军在场景中。
2.如何让太阳军沿着特定的路点前进。
3.如何在地图的特定区域创建炮塔
4.如何让炮塔瞄准太阳军。
5.如何视觉化调整路点和炮塔的攻击范围
学完本教程后,相信大家对塔防游戏的制作就有了坚实的基础,我们可以在此基础上添加新的炮塔类型,太阳军和地图!
好了,众屌丝们是否已经迫不及待了呢?让我们开始做游戏吧!
塔防游戏扫盲
虽然说没听说过塔防游戏的玩家和开发者同样属于稀有动物,但本着我佛慈悲普度众生的大无畏精神,这里还是要对塔防游戏简单介绍下。
塔防游戏其实是策略游戏的一个子类(神马,连策略游戏这个词也没听说过?!你真的可以滚粗了~),玩家可以购买炮塔,并在特定的地点放置炮塔以拦截无穷无尽的太阳军。著名的塔防游戏包括《植物大战僵尸》,《防御编年史》等等。
每一波太阳军通常比上一波更强大,他们的移动速度更快,或者血更厚,让你的炮塔越来越难秒掉他们。当你成功的撑过所有波次的太阳军后,你就赢了。或者当太阳军穿越火线抵达你的基地并成功秒掉了它,你就败了。
下面是我们所制作游戏完成后的截图:

如你所见,太阳军军团会从屏幕的左上发起进攻,并试图沿着绿色的路径抵达玩家的基地。
在沿路上有很多平台,玩家可以在上面放置自己的炮塔。只要你有足够的金币,就可以买足够多的炮塔帮你守住基地。炮塔的攻击范围用白色的圆圈来标识。如果太阳军进入炮塔的攻击范围,炮塔就会朝太阳军发起攻击,直到被消灭或者跑出攻击范围之外。
整装待发
为了尽快开始我们的塔防之旅,不才已经提前制作了一个基本的项目,其中包含了空的Cocos2D项目,以及在教程中所需要用到的各种资源。
在这里下载这个起始项目吧:http://cdn2.raywenderlich.com/downloads/TowerDefenseStarter.zip
这个项目是使用Cocos2D1.1的基本模板所创建的。
在Xcode中打开项目,编译运行,确保一切正常。现在你看到的应该只是一个“空白”的黑色屏幕。
首先让我们大概看一下项目结构。在TowerDefense文件夹中,可以看到一下内容:
1.游戏中的所有类
2.包含了Cocos2D文件的libs文件夹
3.包含了所有美术和音效资源的Resources 文件夹
炮塔就位
首先,让我们添加场景的背景图片。打开HelloWorldLayer.m,然后在init方法的if语句中添加以下代码:
// 1 - Initialize self.isTouchEnabled = YES; CGSize wins = [CCDirector sharedDirector].winSize; // 2 - Set background CCSprite * background = [CCSprite spriteWithFile:@"Bg.png"]; [self addChild:background]; [background setPosition:ccp(wins.width/2,wins.height/2)];
其中第一部分代码的作用是允许层接收触摸事件。剩下的代码则是在场景中添加背景图片。
当我们把背景图片放在合适的位置后,就可以看出来玩家可以在哪些地方放置炮塔了。接下来我们需要沿着路径设置一些点,玩家可以在这些点上触摸并修建炮塔(没有建筑许可的话就等着城管大军来拆你吧)。
为了方便管理,我们将使用.plist文件来保存炮塔的安放地点。在Resources文件夹中已经提供了TowersPosition.plist文件,其中已经有了一些炮塔的位置。
仔细查看这个文件,你会看到一个字典数值,其中包含了两个键值:”x”和”y“.每一个字典项目都代表一个包含了x和y坐标的炮塔位置。现在我们需要读取plist文件,并在地图上放置炮塔。
打开HelloWorldLayer.h,并添加以下实例变量:
NSMutableArray *towerBases;
然后在HelloWorldLayer.m中添加以下方法:
-(void)loadPowerPositions{ NSString* plistPath = [[NSBundle mainBundle]pathForResource:@"TowersPosition" ofType:@"plist"]; NSArray *towerPositions = [NSArray arrayWithContentsOfFile:plistPath]; towerBases = [[NSMutableArray alloc]initWithCapacity:10]; for(NSDictionary *towerPos in towerPositions){ CCSprite *towerBase = [CCSprite spriteWithFile:@"open_spot.png"]; [self addChild:towerBase]; towerBase.position = ccp([[towerPos objectForKey:@"x"]intValue],[[towerPos objectForKey:@"y"]intValue]); [towerBases addObject:towerBases]; } }
然后在init方法中添加语句调用该方法:
[self loadTowerPositions];
最后记得在dealloc方法中清理内存:
[towerBases release];
编译运行游戏,你会看到在路径的边上摆放着方块,这些方块就是玩家用来 摆放炮塔的基座。

现在炮塔基座已经准备就需,接下来就可以正式修建炮塔了!
首先还是打开HelloWorldLayer.h,并添加一个属性:
@property (nonatomic,retain) NSMutableArray *towers;
然后在HelloWorldLayer.m中添加属性变量的合成语句:
@synthesize towers;
接下来创建一个代表炮塔的新类。在Xcode中使用iOS\Cocoa Touch\Objective-C class模板创建一个新类,将其命名为Tower,并选择其父类为CCNode。
替换Tower.h中的内容如下:
#import "cocos2d.h" #import "HelloWorldLayer.h" #define kTOWER_COST 300 @class HelloWorldLayer, Enemy; @interface Tower: CCNode { int attackRange; int damage; float fireRate; } @property (nonatomic,assign) HelloWorldLayer *theGame; @property (nonatomic,assign) CCSprite *mySprite; +(id)nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location; -(id)initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location; @end
接下来使用以下代码替代Tower.m中的代码:
@implementation Tower @synthesize mySprite,theGame; +(id) nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location { return [[[self alloc] initWithTheGame:_game location:location] autorelease]; } -(id) initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location { if( (self=[super init])) { theGame = _game; attackRange = 70; damage = 10; fireRate = 1; mySprite = [CCSprite spriteWithFile:@"tower.png"]; [self addChild:mySprite]; [mySprite setPosition:location]; [theGame addChild:self]; [self scheduleUpdate]; } return self; } -(void)update:(ccTime)dt { } -(void)draw { glColor4f(255, 255, 255, 255); ccDrawCircle(mySprite.position, attackRange, 360, 30, false); [super draw]; } -(void)dealloc { [super dealloc]; } @end
Tower类包含以下属性:一个精灵对象,代表炮塔的视觉呈现;到父层的引用,以及三个变量:
attackRange:决定炮塔的攻击范围
damage:炮塔可以对太阳军造成的伤害
fireRate:炮塔装载炮弹并再次开火的间隔时间
有了以上三个变量,就可以创建具有不同攻击属性的炮塔,比如需要较长加载时间的重炮,以及快速开火的狙击枪。最后,代码中还有一个draw方法,用于在炮塔周围绘制一个圆以显示其攻击范围(以方便测试)。
接下来就可以让玩家来添加一些炮塔了。
打开HelloWorldLayer.m,并对代码做出以下调整:
在文件顶部添加以下代码:
#import "Tower.h"
在dealloc方法中添加以下代码:
[towers release];
在dealloc方法后添加以下方法:
-(BOOL)canBuyTower { return YES; } - (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { for( UITouch *touch in touches ) { CGPoint location = [touch locationInView: [touch view]]; location = [[CCDirector sharedDirector] convertToGL: location]; for(CCSprite * tb in towerBases) { if([self canBuyTower] && CGRectContainsPoint([tb boundingBox],location) && !tb.userData) { //We will spend our gold later. Tower * tower = [Tower nodeWithTheGame:self location:tb.position]; [towers addObject:tower]; tb.userData = tower; } } } }
ccTouchesBegan:方法用于检测玩家在屏幕中的触摸事件。以上代码将遍历towerBases数组,并检查是否有触摸点落在炮塔的基座上。
不过在创建炮塔前还要确认两件事情:
1.玩家是否有能力购买炮塔?canBuyTower方法用于检查玩家是否有足够的钱来购买炮塔。为了简便起见,这里假定玩家是Facebook的早期投资人,所以-不差钱!
2.玩家是否在违章修建?如果tb.UserData中已有数据,那么基座上已经有了炮塔,显然此时你不能在上面修一个新的炮塔!
如果一切检查都通过,那么就可以创建一个新的炮塔,将其放置在基座上,并添加到炮塔数组中。
编译运行游戏。触摸任何一个基座,当炮塔放置上去后,就可以看到周围有个白圆圈显示它的攻击范围。蛤蛤,现在老子也是有枪的人了,放马过来吧!
且慢,你这炮塔看起来很NB了。不过如果没有几个小太阳军过来骚扰一下,岂不是寂寞难耐?!
太阳军进村
在让太阳军进村之前,首先要“为他们铺路”,尼玛也太坑爹了吧。进村的太阳军们会会沿着一系列路点前进。太阳军会在第一个路点出现,然后搜寻列表中的下一个路点,然后从当前路点移动到下一个路点,再重复上面的过程,直到抵挡最后一个路点-钓龟岛基地!如果太阳军们成功抵达钓龟岛,你就成了我飞龙帝国的头号汉奸啊啊啊!
首先要创建一个新的类Waypoint,选择iOS\Cocoa Touch\Objective-c class,将其命名为Waypoint,并选择其为CCNode的子节点。
使用以下代码替代Waypoint.h中的内容:
#import "cocos2d.h" #import "HelloWorldLayer.h" @interface Waypoint: CCNode { HelloWorldLayer *theGame; } @property (nonatomic,readwrite) CGPoint myPosition; @property (nonatomic,assign) Waypoint *nextWaypoint; +(id)nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location; -(id)initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location; @end
然后使用以下代码替代Waypoint.m中的内容:
#import "Waypoint.h" @implementation Waypoint @synthesize myPosition, nextWaypoint; +(id)nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location { return [[[self alloc] initWithTheGame:_game location:location] autorelease]; } -(id)initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location { if( (self=[super init])) { theGame = _game; [self setPosition:CGPointZero]; myPosition = location; [theGame addChild:self]; } return self; } -(void)draw { glColor4f(0, 255, 0, 255); ccDrawCircle(myPosition, 6, 360, 30, false); ccDrawCircle(myPosition, 2, 360, 30, false); if(nextWaypoint) ccDrawLine(myPosition, nextWaypoint.myPosition); [super draw]; } -(void)dealloc { [super dealloc]; } @end
在以上代码中,通过传入一个HelloWorldLayer对象和CGPoint(路点的位置)来初始化一个waypoint对象。
每个路点都包含到下一个路点的引用(属性),这样将创建一个有关联的路点清单。每个路点都“知道”清单中的下一个路点。通过这种方式,就可以沿着路点链将太阳军引导到最终的目的地(这么看,哥到底是汉奸还是卧底?!!)显然,崇尚暴菊和剖腹的小太阳军永远不会撤退。该出手时就出手吧!
最后,draw方法显示了路点的摆放位置,并在路点之间绘制一条线将其连接(当然,仅仅是测试时使用)。在实际的游戏场景中玩家不会看到这条线,否则岂不是太容易了些!
接下来需要创建路点清单。打开HelloWorldLayer.h,并添加以下属性:
@property (nonatomic,retain) NSMutableArray *waypoints;
接下来在HelloWorldLayer.m中添加以下代码:
//在文件顶部: #import "Waypoint.h" // Add synthesise @synthesize waypoints; //在init方法前添加以下方法 -(void)addWaypoints { waypoints = [[NSMutableArray alloc] init]; Waypoint * waypoint1 = [Waypoint nodeWithTheGame:self location:ccp(420,35)]; [waypoints addObject:waypoint1]; Waypoint * waypoint2 = [Waypoint nodeWithTheGame:self location:ccp(35,35)]; [waypoints addObject:waypoint2]; waypoint2.nextWaypoint =waypoint1; Waypoint * waypoint3 = [Waypoint nodeWithTheGame:self location:ccp(35,130)]; [waypoints addObject:waypoint3]; waypoint3.nextWaypoint =waypoint2; Waypoint * waypoint4 = [Waypoint nodeWithTheGame:self location:ccp(445,130)]; [waypoints addObject:waypoint4]; waypoint4.nextWaypoint =waypoint3; Waypoint * waypoint5 = [Waypoint nodeWithTheGame:self location:ccp(445,220)]; [waypoints addObject:waypoint5]; waypoint5.nextWaypoint =waypoint4; Waypoint * waypoint6 = [Waypoint nodeWithTheGame:self location:ccp(-40,220)]; [waypoints addObject:waypoint6]; waypoint6.nextWaypoint =waypoint5; } // At the end of init: // 4 - Add waypoints [self addWaypoints]; //Finally, release the waypoints array in dealloc [waypoints release];
编译运行游戏,将看到下面的画面:
在地图中有6个路点,而小太阳军将会沿着这条路径前进。当然,在你诱敌深入之前,需要添加两个辅助方法。
首先需要添加新的方法定义。
打开HelloWorldLayer.h,并添加以下方法定义:
-(BOOL)circle:(CGPoint)circlePoint withRadius:(float)radius collisionWithCircle:(CGPoint)circlePointTwo collisionCircleRadius:(float)radiusTwo; void ccFillPoly(CGPoint *poli, int points, BOOL closePolygon);
接下来打开HelloWorldLayer.m,并添加以下几个方法:
void ccFillPoly( CGPoint *poli, int points, BOOL closePolygon ) { // Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY // Needed states: GL_VERTEX_ARRAY, // Unneeded states: GL_TEXTURE_2D, GL_TEXTURE_COORD_ARRAY, GL_COLOR_ARRAY glDisable(GL_TEXTURE_2D); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisableClientState(GL_COLOR_ARRAY); glVertexPointer(2, GL_FLOAT, 0, poli); if( closePolygon ) glDrawArrays(GL_TRIANGLE_FAN, 0, points); else glDrawArrays(GL_LINE_STRIP, 0, points); // restore default state glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glEnable(GL_TEXTURE_2D); } -(BOOL)circle:(CGPoint) circlePoint withRadius:(float) radius collisionWithCircle:(CGPoint) circlePointTwo collisionCircleRadius:(float) radiusTwo { float xdif = circlePoint.x - circlePointTwo.x; float ydif = circlePoint.y - circlePointTwo.y; float distance = sqrt(xdif*xdif+ydif*ydif); if(distance <= radius+radiusTwo) return YES; return NO; }
在以上代码中,collisionWithCircle方法用于判断两个圆圈是否会碰撞或相交。这将用于判断太阳军是否抵达了某个路点,同时还可以检测太阳军是否在炮塔的攻击范围内。
ccFillPoly方法则使用OpenGL来绘制一个填充的多边形,因为在Cocos2d中只能绘制非填充的多边形。ccFillPoly方法将用于绘制太阳军的血条。
现在可以让太阳军隆重登场了!
打开HelloWorldLayer.h,并添加以下代码:
// 添加以下变量 int wave; CCLabelBMFont *ui_wave_lbl; // 添加以下属性 @property (nonatomic,retain) NSMutableArray *enemies;
接下来切换到HelloWorldLayer.m,并做出以下修改:
// Synthesize enemies @synthesize enemies; // Release it in dealloc [enemies release];
现在可以为帝国一衣带水的“朋友”们创建一个单独的类了。使用iOS\Cocoa Touch\Objective-C class模板来创建一个新的类,将其命名为Enemy,并确定其为CCNode的子类。
使用以下代码替换Enemy.h中的内容:
#import "cocos2d.h" #import "HelloWorldLayer.h" #import "GameConfig.h" @class HelloWorldLayer, Waypoint, Tower; @interface Enemy: CCNode { CGPoint myPosition; int maxHp; int currentHp; float walkingSpeed; Waypoint *destinationWaypoint; BOOL active; } @property (nonatomic,assign) HelloWorldLayer *theGame; @property (nonatomic,assign) CCSprite *mySprite; +(id)nodeWithTheGame:(HelloWorldLayer*)_game; -(id)initWithTheGame:(HelloWorldLayer *)_game; -(void)doActivate; -(void)getRemoved; @end
然后使用以下代码替换Enemy.m中的内容:
#import "Enemy.h" #import "Tower.h" #import "Waypoint.h" #import "SimpleAudioEngine.h" #define HEALTH_BAR_WIDTH 20 #define HEALTH_BAR_ORIGIN -10 @implementation Enemy @synthesize mySprite, theGame; +(id)nodeWithTheGame:(HelloWorldLayer*)_game { return [[[self alloc] initWithTheGame:_game] autorelease]; } -(id)initWithTheGame:(HelloWorldLayer *)_game { if ((self=[super init])) { theGame = _game; maxHp = 40; currentHp = maxHp; active = NO; walkingSpeed = 0.5; mySprite = [CCSprite spriteWithFile:@"enemy.png"]; [self addChild:mySprite]; Waypoint * waypoint = (Waypoint *)[theGame.waypoints objectAtIndex:([theGame.waypoints count]-1)]; destinationWaypoint = waypoint.nextWaypoint; CGPoint pos = waypoint.myPosition; myPosition = pos; [mySprite setPosition:pos]; [theGame addChild:self]; [self scheduleUpdate]; } return self; } -(void)doActivate { active = YES; } -(void)update:(ccTime)dt { if(!active)return; if([theGame circle:myPosition withRadius:1 collisionWithCircle:destinationWaypoint.myPosition collisionCircleRadius:1]) { if(destinationWaypoint.nextWaypoint) { destinationWaypoint = destinationWaypoint.nextWaypoint; }else { //Reached the end of the road. Damage the player [theGame getHpDamage]; [self getRemoved]; } } CGPoint targetPoint = destinationWaypoint.myPosition; float movementSpeed = walkingSpeed; CGPoint normalized = ccpNormalize(ccp(targetPoint.x-myPosition.x,targetPoint.y-myPosition.y)); mySprite.rotation = CC_RADIANS_TO_DEGREES(atan2(normalized.y,-normalized.x)); myPosition = ccp(myPosition.x+normalized.x * movementSpeed,myPosition.y+normalized.y * movementSpeed); [mySprite setPosition:myPosition]; } -(void)getRemoved { [self.parent removeChild:self cleanup:YES]; [theGame.enemies removeObject:self]; //Notify the game that we killed an enemy so we can check if we can send another wave [theGame enemyGotKilled]; } -(void)draw { glColor4f(255, 0, 0, 255); CGPoint healthBarBack[] = {ccp(mySprite.position.x -10,mySprite.position.y+16),ccp(mySprite.position.x+10,mySprite.position.y+16),ccp(mySprite.position.x+10,mySprite.position.y+14),ccp(mySprite.position.x-10,mySprite.position.y+14)}; ccFillPoly(healthBarBack, 4, YES); glColor4f(0, 255, 0, 255); CGPoint healthBar[] = {ccp(mySprite.position.x + HEALTH_BAR_ORIGIN,mySprite.position.y+16),ccp(mySprite.position.x+HEALTH_BAR_ORIGIN+(float)(currentHp * HEALTH_BAR_WIDTH) / maxHp,mySprite.position.y+16),ccp(mySprite.position.x+HEALTH_BAR_ORIGIN+(float)(currentHp * HEALTH_BAR_WIDTH) / maxHp,mySprite.position.y+14),ccp(mySprite.position.x+HEALTH_BAR_ORIGIN,mySprite.position.y+14)}; ccFillPoly(healthBar, 4, YES); } -(void)dealloc { [super dealloc]; } @end
在以上代码中,首先,通过向HelloWorldLayer对象传递一个参数来初始化Enemy。在init方法中设置了以下重要的参数:
1.maxHp:太阳军的最大生命值,能抗多少刀?
2.walkingSpeed: 定义太阳军的移动速度
3.mySprite: 保存太阳军的视觉形象
4.destinationWaypoint:保存到下一个路点的引用
至于update方法,则是真正见证奇迹发生的地方。程序会每帧调用该方法,首先调用collisionWithCircle方法来判断太阳军是否已经到达目标路点。如果是,则前进到下一个路点,直到太阳军抵达最终目的地-西方极乐世界。
接下来的代码将会让代表太阳军的精灵图片沿着直线移动到目标路点,同时还限定其速度。为了实现这一目的采用了以下算法:
1.计算出从当前位置到目标位置的向量,然后将其长度设置为1(所谓的向量标准化)。
2.将移动速度乘以标准化向量以获取精灵的移动距离。用当前位置坐标加上移动距离从而获得新的位置坐标。
最后,draw方法在精灵图片的上方实现了血条。语句中首先绘制了一个红色背景,然后根据太阳军的当前HP生命值来设置绿色的覆盖条。
好了,Enemey类已然完工,我们已经可以在屏幕中看到太阳军团的出现了。
打开HelloWorldLayer.h,并添加以下方法定义:
-(void)enemyGotKilled;
切换到HelloWorldLayer.m,并对其中的代码做出以下修改:
在文件的顶部添加以下代码:
#import "Enemy.h"
在init方法前添加以下方法:
-(BOOL)loadWave { NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"Waves" ofType:@"plist"]; NSArray * waveData = [NSArray arrayWithContentsOfFile:plistPath]; if(wave >= [waveData count]) { return NO; } NSArray * currentWaveData =[NSArray arrayWithArray:[waveData objectAtIndex:wave]]; for(NSDictionary * enemyData in currentWaveData) { Enemy * enemy = [Enemy nodeWithTheGame:self]; [enemies addObject:enemy]; [enemy schedule:@selector(doActivate) interval:[[enemyData objectForKey:@"spawnTime"]floatValue]]; } wave++; [ui_wave_lbl setString:[NSString stringWithFormat:@"WAVE: %d",wave]]; return YES; } -(void)enemyGotKilled { if ([enemies count]<=0) //If there are no more enemies. { if(![self loadWave]) { NSLog(@"You win!"); [[CCDirector sharedDirector] replaceScene:[CCTransitionSplitCols transitionWithDuration:1 scene:[HelloWorldLayer scene]]]; } } }
在init方法的最后添加以下语句:
// 5 - Add enemies enemies = [[NSMutableArray alloc] init]; [self loadWave]; // 6 - Create wave label ui_wave_lbl = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"WAVE: %d",wave] fntFile:@"font_red_14.fnt"]; [self addChild:ui_wave_lbl z:10]; [ui_wave_lbl setPosition:ccp(400,wins.height-12)]; [ui_wave_lbl setAnchorPoint:ccp(0,0.5)];
以上代码中最重要的是loadWave方法,它的作用是从Waves.plist中读取数据。
现在来看看Waves.plist,你会发现其中包含三个数组。其中每个数组中都包含一个波次,其实就是要一起杀过来的一群太阳军。第一个数组里面包含六个字典,其中每个字典都定义了一个太阳军。在这篇教程中,字典中仅包含太阳军应出现的时间,但该字典同时也可以用来定义太阳军的类型,或其它任何可以区分太阳军的特殊属性。
loadWave方法会检查下一个应出现的波次,并根据波次信息创建对应的太阳军,并让它们在合适的时候出现在屏幕中。
enemyGotKilled方法会检查出现在屏幕中的太阳军数量,如果已经没有了,就让下一波上。到后面的时候,我们将使用同样的方法来判断玩家是否赢得了游戏。
编译运行游戏,你会看到太阳军团正蚂蚁般的向钓龟岛基地前进。肿么办?!

塔防战争:炮塔的力量
炮塔就位了吗?赶紧检查一下。太阳军已经来了吗?赶紧再确认下。难道我会告诉你是小马哥坐在直升机上亲自来检查吗?好吧,现在该是让炮塔发威的时候了。
首先,每个炮塔都会检查确认太阳军是否已在攻击范围内。如果是,炮塔将向太阳军发起攻击,直到:太阳军跑出攻击范围外,或者去了西方极乐世界。此时炮塔会去寻找新的牺牲品。
光荣与梦想的时刻到了,为了钓龟岛,努力吧骚年!
首先打开Tower.h,并对其中的代码做以下修正:
//添加以下实例变量: BOOL attacking; Enemy *chosenEnemy; //添加方法定义: -(void)targetKilled;
对Tower.m中的代码做以下修正:
//在文件顶部引用Enemy类的头文件 #import "Enemy.h"
在update方法的上方添加以下方法:
-(void)attackEnemy { [self schedule:@selector(shootWeapon) interval:fireRate]; } -(void)chosenEnemyForAttack:(Enemy *)enemy { chosenEnemy = nil; chosenEnemy = enemy; [self attackEnemy]; [enemy getAttacked:self]; } -(void)shootWeapon { CCSprite * bullet = [CCSprite spriteWithFile:@"bullet.png"]; [theGame addChild:bullet]; [bullet setPosition:mySprite.position]; [bullet runAction:[CCSequence actions:[CCMoveTo actionWithDuration:0.1 position:chosenEnemy.mySprite.position],[CCCallFunc actionWithTarget:self selector:@selector(damageEnemy)],[CCCallFuncN actionWithTarget:self selector:@selector(removeBullet:)], nil]]; } -(void)removeBullet:(CCSprite *)bullet { [bullet.parent removeChild:bullet cleanup:YES]; } -(void)damageEnemy { [chosenEnemy getDamaged:damage]; } -(void)targetKilled { if(chosenEnemy) chosenEnemy =nil; [self unschedule:@selector(shootWeapon)]; } -(void)lostSightOfEnemy { [chosenEnemy gotLostSight:self]; if(chosenEnemy) chosenEnemy =nil; [self unschedule:@selector(shootWeapon)]; }
最后,使用以下内容替代update方法中的内容:
-(void)update:(ccTime)dt { if (chosenEnemy){ //We make it turn to target the enemy chosen CGPoint normalized = ccpNormalize(ccp(chosenEnemy.mySprite.position.x-mySprite.position.x,chosenEnemy.mySprite.position.y-mySprite.position.y)); mySprite.rotation = CC_RADIANS_TO_DEGREES(atan2(normalized.y,-normalized.x))+90; if(![theGame circle:mySprite.position withRadius:attackRange collisionWithCircle:chosenEnemy.mySprite.position collisionCircleRadius:1]) { [self lostSightOfEnemy]; } } else { for(Enemy * enemy in theGame.enemies) { if([theGame circle:mySprite.position withRadius:attackRange collisionWithCircle:enemy.mySprite.position collisionCircleRadius:1]) { [self chosenEnemyForAttack:enemy]; break; } } } }
这下子可添加了不少代码。当然,在你添加代码的时候,还可以看到Xcode中不断发出各种警告。显然,有些工作做的不完美。
打开Enemy.h,并对代码做以下修正:
//添加以下的实例变量: NSMutableArray *attackedBy; //添加以下方法定义: -(void)getAttacked:(Tower *)attacker; -(void)gotLostSight:(Tower *)attacker; -(void)getDamaged:(int)damage;
对Enemy.m中 的代码做出以下修正:
//在initWithTheGame:方法的顶部添加以下语句: attackedBy = [[NSMutableArray alloc] initWithCapacity:5];
使用以下代码替代getMoved方法中的内容:
-(void)getRemoved { for(Tower * attacker in attackedBy) { [attacker targetKilled]; } [self.parent removeChild:self cleanup:YES]; [theGame.enemies removeObject:self]; //Notify the game that we killed an enemy so we can check if we can send another wave [theGame enemyGotKilled]; }
在文件的底部添加以下方法:
-(void)getAttacked:(Tower *)attacker { [attackedBy addObject:attacker]; } -(void)gotLostSight:(Tower *)attacker { [attackedBy removeObject:attacker]; } -(void)getDamaged:(int)damage { currentHp -=damage; if(currentHp <=0) { [self getRemoved]; } }
其中最重要的代码在Tower类的update方法中。炮塔会定期检查太阳军是否在火力范围内。如果是,炮口会自动旋转对准敌军开火。
一旦某个太阳军被标记为正在受到攻击,会调用方法以炮塔的开火初始间隔向敌军发射炮弹。这样一来,每个太阳军都会保持一个向其发起攻击的炮塔列表。当太阳军不堪重负往生西方之时,所有在攻击该敌军的炮塔就会停下来凉凉炮筒。
编译运行应用!在地图上放置一些炮塔。你会看到当太阳军进入炮塔的火力范围时,炮塔会迫不及待的向它们开火,而太阳军的血条也会逐渐减少,直到最终归天。开炮吧,你还在等什么!

轰轰轰!!!好吧,现在还有一些小细节工作要做,让这个游戏显得更真实一点。首先你得添加音效吧,不然哪儿能体现出我大宋帝国借我城管三千,扫灭太阳三岛的王八之气!另外,虽然我大钓龟岛壳够硬,但也经不住自慰队的轮番攻击啊。而且,大宋的黄金储备也是有限的,不足以添加足够的炮塔支撑到小马哥的援兵。
锦上添花
首先来显示钓龟岛基地的剩余血量吧,还有当钓龟岛基地被太阳军攻击的时候,会发生些什么。
打开HelloWorldLayer.h,添加以下三个实例变量:
int playerHp; CCLabelBMFont *ui_hp_lbl; BOOL gameEnded;
其中playerHp代表钓龟岛基地的血量,而CCLabelBMFont类型的变量则是一个标签,用于显示血量的数值。gameEnded是个布尔变量,用来表示游戏是否结束。
接下来添加以下方法定义:
-(void)getHpDamage; -(void)doGameOver;
打开HelloWorldLayer.m,并做出以下代码修改:
//在init方法的最后,添加以下代码: // 7 - Player lives playerHp = 5; ui_hp_lbl = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"HP: %d",playerHp] fntFile:@"font_red_14.fnt"]; [self addChild:ui_hp_lbl z:10]; [ui_hp_lbl setPosition:ccp(35,wins.height-12)]; // Add the following methods to the end of the file -(void)getHpDamage { playerHp--; [ui_hp_lbl setString:[NSString stringWithFormat:@"HP: %d",playerHp]]; if (playerHp <=0) { [self doGameOver]; } } -(void)doGameOver { if (!gameEnded) { gameEnded = YES; [[CCDirector sharedDirector] replaceScene:[CCTransitionRotoZoom transitionWithDuration:1 scene:[HelloWorldLayer scene]]]; } }
以上代码添加了一个方法,用于减少基地的血量,更新标签,并检查基地是否被爆头了。如果是,恭喜你,千古罪人的称号非你莫属了!
每当有太阳军抵达基地时,都会调用getHpDamage方法。你需要将其添加到Enemy.m的update:方法中,以检查当敌军已经走完所有路点后该发生点什么。
幸运的是,这个任务我们在之前的代码中已然完成。如此就可以踹口气了。
编译运行游戏,为了看看效果,管好你的手指,让太阳军顺利抵达基地试试看。忍住,一定要忍住!

此时,你会看到基地的血量不断减少,直到。。。游戏失败,钓龟岛失陷?!
好吧,接下来要限制下大宋的黄金储备(还需要限制吗?)
大多数的塔防游戏都会采取“零和”博弈策略。这样每个炮塔都需要花费一定的成本来购买,同时限制玩家的黄金储备。
打开HelloWorldLayer.h,并添加以下实例变量:
int playerGold; CCLabelBMFont *ui_gold_lbl;
如同之前一样,这里添加的变量用于保存黄金储备,并使用标签来显示它。同时,添加一个新的方法定义:
-(void)awardGold:(int)gold;
现在,打开HelloWorldLayer.m,并对代码进行以下修改:
-(void)awardGold:(int)gold { playerGold += gold; [ui_gold_lbl setString:[NSString stringWithFormat:@"GOLD: %d",playerGold]]; } // 在init方法的最后添加以下语句: // 8 - Gold playerGold = 1000; ui_gold_lbl = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"GOLD: %d",playerGold] fntFile:@"font_red_14.fnt"]; [self addChild:ui_gold_lbl z:10]; [ui_gold_lbl setPosition:ccp(135,wins.height-12)]; [ui_gold_lbl setAnchorPoint:ccp(0,0.5)]; //使用以下代码替换 canBuyTower 方法中的内容: -(BOOL)canBuyTower { if (playerGold - kTOWER_COST >=0) return YES; return NO; } // 在ccTouchesBegan方法中,在if语句中添加以下语句,也就是之前注释说将要花费黄金储备的地方: playerGold -= kTOWER_COST; [ui_gold_lbl setString:[NSString stringWithFormat:@"GOLD: %d",playerGold]];
以上代码将会在玩家放置炮塔时检查是否有足够的黄金储备(神马?黄金都在大美丽坚那里?)如果是,就会放置炮塔,并从黄金储备中减少炮塔的成本。当然,在每次击溃某只太阳军的时候,也需要小小的奖励一些黄金吧。
在Enemy.m的getDamaged:方法中添加以下语句(在if条件从句中):
[theGame awardGold:200];
编译运行游戏,你会发现现在自己不能随心所欲的安放炮塔了(哎,黄金储备都存在瑞士银行和花在三母消费上了,我北洋舰队和附属炮台就这样沦为太阳军的炮灰?!)
幸运的是,杀灭太阳军会有一定的黄金奖励,这样还能多撑一会儿,待得小马哥拍马杀来救援。

好了,最后的最后,我们该加点激动人心的背景音乐和音效了。感谢Kevin MacLeod提供的背景音乐和使用cxfr制作的一些音效。
打开HelloWorldLayer.m,并对其中的代码做以下修正:
//在文件的顶部添加引用: #import "SimpleAudioEngine.h" //在init方法的开始处(在if条件语句中)添加以下语句: [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"8bitDungeonLevel.mp3" loop:YES]; //在ccTouchesBegan方法中,在初始化新的炮塔对象前添加以下语句: [[SimpleAudioEngine sharedEngine] playEffect:@"tower_place.wav"]; //在getHpDamage方法的开始处添加以下代码: [[SimpleAudioEngine sharedEngine] playEffect:@"life_lose.wav"]; 然后打开Enemy.m,并添加以下代码: //在文件的顶部添加以下代码: #import "SimpleAudioEngine.h" //在getDamaged:方法的开始处添加以下代码: [[SimpleAudioEngine sharedEngine] playEffect:@"laser_shoot.wav"];
噢了!马上进入钓龟岛保卫战吧,小马哥在直升机上等你呢!
后续的后续
这里是以上教程的示例代码:http://cdn4.raywenderlich.com/downloads/TowerDefenseFinished.zip
其实钓龟岛保卫战才刚刚开始,接下来还有很多可以完善的地方:
1.添加不同类型的太阳军,不同的速度,生命值,等等。
2.不同类型的炮塔,带有不同的攻击模式,当然价格也不一样。
3.不同的行进路线,不同的路点摆放模式。
4.不同的关卡。。。