一 、原文
http://www.raywenderlich.com/37701/how-to-make-a-tower-defense-game-tutorial
二、
塔防类游戏是iOS中最流行的游戏之一。建造顶级的防御物和看着它消灭一群群的侵略者,让人有难于置信的乐趣。如植物大战僵尸,保卫萝卜等游戏。
从本文你可以学到:
*如何通过配置好的时间间隔创建一拨拨的敌人
*如何使这些敌人根据定义好的路线前进
*如何在地图上特定的位置创建炮塔
*如何使炮塔攻击敌人
*如何可视化地调试路线和炮塔的攻击范围
最后,你将有一个此类游戏的完整的框架,使你可以通过增加新的类型的炮塔,新的敌人和新的地图,来扩展游戏。
1.概述
塔防游戏是策略游戏,你可以购买炮塔,将它们放在战略位置来阻止一拨拨试图到达你的基地并破坏它的敌人。
每拨敌人通常都比上一拨敌人更厉害,移动速度更快,更强的防御能力。
当你干掉敌人通过了所有的关卡(胜利),或者当有足够多的敌人到达你的基地并摧毁了基地(失败),游戏结束。
下面是完成的游戏截图:
在路的两边,有许多可以放置炮塔的平台。玩家只可以购买和放置一定数量的炮塔,由金币数量的多少决定。 敌人从屏幕的左上角出现,沿着绿色的路线到达玩家的基地。
白色圆圈就是炮塔的攻击范围,如果有一个敌人出现在攻击范围内,炮塔就会向敌人开火,直到敌人被消灭或敌人走出攻击范围。
2.资源
我创建了个初始的项目工程,让你能很快地开始学习。download the starter project
这个工程是基于Cocos2D的模版创建的,这个工程有一个带文本的层,你不需要这个文本,你将创建自己的界面。
编译运行这个工程,你可以看到一个黑屏,因为“Hello World”已经被去掉了。只要运行成功,一切已经就绪。
看下工程的目录结构,你会发现:
*libs文件夹包含了所有的Cocos2D的文件
*Resources包含了所有的图片和声音文件
现在,你可以布置好地图,开始创建炮塔。
3.放置炮塔
a.设置背景图片
打开HelloWorldLayer.m,修改init函数,在 if 语句里增加代码
self.touchEnabled =TRUE;//接受触屏事件
CGSize size = [[CCDirectorsharedDirector]winSize];
CCSprite *backGround = [CCSpritespriteWithFile:@"bg.png"];
[self addChild:backGround];
[backGround setPosition:ccp(size.width/2,size.height/2)];
b.设置炮塔放置点
炮塔位置由文件TowersPosition.plist定义。
打开这个文件,你会发现一组包含字典的数组,每个字典代表一个炮塔位置的x,y坐标。
打开HelloWorldLayer.h,增加保存炮塔位置的数组变量。NSMutableArray * towerBases;
打开HelloWorldLayer.m,增加新的函数loadTowerPositions,从TowersPostion.plist读取位置信息,并增加位置图片到地图上。
- (void)loadTowerPositions
{
NSString *plistPath = [[NSBundlemainBundle]pathForResource:@"TowersPosition"ofType:@"plist"];
NSArray *towerPostions = [NSArrayarrayWithContentsOfFile:plistPath];
towerBases = [[NSMutableArrayalloc]initWithCapacity:10];
for(NSDictionary *towerPosin towerPostions)
{
CCSprite *towerBase = [CCSpritespriteWithFile:@"open_spot.png"];
[selfaddChild:towerBase];
[towerBasesetPosition:ccp([[towerPosobjectForKey:@"x"]intValue],
[[towerPosobjectForKey:@"y"]intValue])
];
[towerBasesaddObject:towerBase];
}
}
在init函数中增加代码[self loadTowerPositions];
c.增加炮塔类
打开HelloWorldLayer.h增加属性@property (nonatomic,strong)NSMutableArray* towers;
打开HelloWorldLayer.m实现属性读写函数@synthesize towers;
实现炮塔类Tower,父类为CCNode。
Tower.h
@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 theGame,mySprite;
+(id) nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location
{
return [[selfalloc]initWithTheGame:_gamelocation:location];
}
-(id) initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location
{
if( (self=[superinit])) {
theGame = _game;
attackRange =70;
damage =10;
fireRate =1;
mySprite = [CCSpritespriteWithFile:@"tower.png"];
[selfaddChild:mySprite];
[mySpritesetPosition:location];
[theGameaddChild:self];
[self scheduleUpdate];
}
return self;
}
-(void)update:(ccTime)dt
{
}
-(void)draw
{
// ccDrawColor4B(255, 255, 255, 255);
// ccDrawCircle(mySprite.position, attackRange, 360, 30, false);
[superdraw];
}
@end
打开HelloWorldLayer.m添加函数
-(BOOL) canBuyTower
{
return YES;
}
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for(UITouch *touchin touches)
{
CGPoint location = [touchlocationInView:[touchview]];
location = [[CCDirectorsharedDirector]convertToGL:location];
for(CCSprite *tbintowerBases)
{
if(CGRectContainsPoint([tbboundingBox],location) &&
[self canBuyTower] && !tb.userData)
{
Tower * tower = [TowernodeWithTheGame:selflocation:tb.position];
[towersaddObject:tower];
tb.userData = (__bridgevoid *)(tower); //关联炮塔
}
}
}
}
编译运行程序,可以放置炮塔了。但是若没有敌人,这些炮塔又有什么用呢?
4.路线,敌人,周期
a.路线,增加Waypoint类,路线上的点对象,父类是CCNode。
Waypoint类主要属性:
@property (nonatomic,readwrite)CGPoint myPosition;//位置
@property (nonatomic,assign)Waypoint *nextWaypoint;//下个点
b.给HelloWorldLayer增加属性waypoints,保存路线的关键点
@property (nonatomic,strong)NSMutableArray* waypoints;
增加函数addWaypoints,产生敌人行走路线的6个关键点,在init函数调用。
-(void)addWaypoints
{
waypoints = [[NSMutableArrayalloc]init];
Waypoint * waypoint1 = [WaypointnodeWithTheGame:selflocation:ccp(420,35)];//右下,终点
[waypointsaddObject:waypoint1];
Waypoint * waypoint2 = [WaypointnodeWithTheGame:selflocation:ccp(35,35)];
[waypointsaddObject:waypoint2];
waypoint2.nextWaypoint =waypoint1;
Waypoint * waypoint3 = [WaypointnodeWithTheGame:selflocation:ccp(35,130)];
[waypointsaddObject:waypoint3];
waypoint3.nextWaypoint =waypoint2;
Waypoint * waypoint4 = [WaypointnodeWithTheGame:selflocation:ccp(445,130)];
[waypointsaddObject:waypoint4];
waypoint4.nextWaypoint =waypoint3;
Waypoint * waypoint5 = [WaypointnodeWithTheGame:selflocation:ccp(445,220)];
[waypointsaddObject:waypoint5];
waypoint5.nextWaypoint =waypoint4;
Waypoint * waypoint6 = [WaypointnodeWithTheGame:selflocation:ccp(-40,220)];//左上,起点
[waypointsaddObject:waypoint6];
waypoint6.nextWaypoint =waypoint5;
}
@interface Enemy :CCNode {
int maxHp;//最大生命值
int currentHp;//当前生命值
BOOL active;//是否活动状态
float walkingSpeed;//行走速度
Waypoint *destinationWaypoint;//目标点
CGPoint myPosition;//当前位置
}
@property (nonatomic,assign)CCSprite *mySprite;//敌人精灵
d.给HelloWorldLayer增加属性enemies,保存敌人
@property (nonatomic,strong)NSMutableArray *enemies;
e.waves.plist定义了三拨敌人,第一拨有6个敌人,第二拨3个,第三拨5个。
spawnTime定义敌人被激活的时间。
增加函数loadWave,加载各拨敌人,在init函数调用。
-(BOOL)loadWave {
NSString* plistPath = [[NSBundlemainBundle]pathForResource:@"Waves"ofType:@"plist"];
NSArray * waveData = [NSArrayarrayWithContentsOfFile:plistPath];
if(wave >= [waveDatacount])
{
returnNO;
}
NSArray * currentWaveData =[NSArrayarrayWithArray:[waveDataobjectAtIndex:wave]];
for(NSDictionary * enemyDatain currentWaveData)
{
Enemy * enemy = [EnemynodeWithTheGame:self];
[enemiesaddObject:enemy];
[enemyschedule:@selector(doActivate)
interval:[[enemyDataobjectForKey:@"spawnTime"]floatValue]];
}
wave++;
return YES;
}
5.炮塔的攻击
每个炮塔检查看攻击范围有无敌人,如果有,炮塔会开始炮击敌人,直到敌人走出攻击范围或敌人被消灭,然后开始找其他敌人。
a.打开Tower.h
增加成员变量
BOOL attaching;
Enemy *chosenEnemy;
NSMutableArray *attackedBy;
-(void)getAttacked:(Tower*)attacker;
-(void)getLostSight:(Tower*)attacker;
-(void)getDamaged:(int)damage;
-(void)attackEnemy
{
[selfschedule:@selector(shootWeapon)interval:fireRate];
}
-(void)chosenEnemyForAttack:(Enemy *)enemy
{
chosenEnemy = nil;
chosenEnemy = enemy;
[selfattackEnemy];
[enemygetAttacked:self];
}
-(void)shootWeapon
{
CCSprite * bullet = [CCSpritespriteWithFile:@"bullet.png"];
[theGameaddChild:bullet];
[bulletsetPosition:mySprite.position];
[bulletrunAction:[CCSequenceactions:
[CCMoveToactionWithDuration:0.1position:chosenEnemy.mySprite.position],
[CCCallFuncactionWithTarget:selfselector:@selector(damageEnemy)],
[CCCallFuncNactionWithTarget:selfselector:@selector(removeBullet:)],nil]];
}
-(void)removeBullet:(CCSprite *)bullet
{
[bullet.parentremoveChild:bulletcleanup:YES];
}
-(void)damageEnemy
{
[chosenEnemy getDamaged:damage];
}
-(void)targetKilled
{
if(chosenEnemy)
chosenEnemy =nil;
[selfunschedule:@selector(shootWeapon)];
}
-(void)lostSightOfEnemy
{
[chosenEnemygetLostSight:self];
if(chosenEnemy)
chosenEnemy =nil;
[selfunschedule:@selector(shootWeapon)];
}
-(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(![theGamecircle:mySprite.positionwithRadius:attackRange
collisionWithCircle:chosenEnemy.mySprite.positioncollisionCircleRadius:1])
{
[self lostSightOfEnemy];//走出攻击范围
}
}else {
for(Enemy * enemyintheGame.enemies)
{
if([theGamecircle:mySprite.positionwithRadius:attackRange
collisionWithCircle:enemy.mySprite.positioncollisionCircleRadius:1])
{
[selfchosenEnemyForAttack:enemy];//锁定目标
break;
}
}
}
}
-(void)getRemoved
{
for(Tower * attackerinattackedBy)
{
[attackertargetKilled];//通知炮塔,已被消灭
}
[self.parentremoveChild:selfcleanup:YES];
[theGame.enemiesremoveObject:self];
//Notify the game that we killed an enemy so we can check if we can send another wave
[theGameenemyGotKilled];
}
-(void)getAttacked:(Tower *)attacker
{
[attackedByaddObject:attacker];
}
-(void)getLostSight:(Tower *)attacker
{
[attackedByremoveObject:attacker];
}
-(void)getDamaged:(int)damage
{
currentHp -=damage;
if(currentHp <=0)
{
[selfgetRemoved];
}
}
int playerHp;//命值
BOOL gameEnded;//标记游戏是否结束
CCLabelBMFont *ui_hp_lbl;//显示命值的文本
-( void ) doGameOver;//处理游戏结束的函数playerHp =5;
ui_hp_lbl = [CCLabelBMFontlabelWithString:[NSStringstringWithFormat:@"HP: %d",playerHp]fntFile:@"font_red.fnt"];
[selfaddChild:ui_hp_lblz:10];
[ui_hp_lblsetPosition:ccp(85,size.height-12)];
注意增加字体资源时,除了font_red.fnt,还有font_red_14.png也要加进去,否则文字无法显示。
增加函数
-(void)getHpDamage {
playerHp--;
[ui_hp_lbl setString:[NSStringstringWithFormat:@"HP: %d",playerHp]];
if (playerHp <=0) {
[selfdoGameOver];
}
}
-(void)doGameOver {
if (!gameEnded) {
gameEnded =YES;
[[CCDirectorsharedDirector]replaceScene:[CCTransitionRotoZoomtransitionWithDuration:1scene:[HelloWorldLayerscene]]];
}
}
[selfaddChild:ui_wave_lblz:10];
[ui_wave_lblsetPosition:ccp(400,size.height-12)];
[ui_wave_lblsetAnchorPoint:ccp(0,0.5)];
CCLabelBMFont *ui_gold_lbl;//显示金币数量的文本
-(void)awardGold:(int)gold{
playGold += gold;
[ui_gold_lblsetString:[NSStringstringWithFormat:@"Gold: %d",playGold]];
}
在init函数中增加playGold = 1000;
ui_gold_lbl = [CCLabelBMFontlabelWithString:[NSStringstringWithFormat:@"Gold: %d",playGold]fntFile:@"font_red.fnt"];
[selfaddChild:ui_gold_lblz:10];
[ui_gold_lblsetPosition:ccp(135,size.height-12)];
[ui_gold_lblsetAnchorPoint:ccp(0,0.5)];
修改canBuyTower函数, kTOWER_COST 在Tower.h中定义 #define kTOWER_COST 300-(BOOL) canBuyTower
{
if(playGold -kTOWER_COST>=0)
return YES;
else
return NO;
}
[ui_gold_lblsetString:[NSStringstringWithFormat:@"GOLD: %d",playGold]];
打开Enemy.m,修改函数 getDamaged,在if语句里面增加代码
[[SimpleAudioEnginesharedEngine] playEffect:@"tower_place.wave"];
在函数getHpDamage的开始添加音效
[[SimpleAudioEnginesharedEngine] playEffect:@"life_lose.wav"];
打开Enemy.m,添加 #import"SimpleAudioEngine.h"
在函数getDamaged的开始添加音效
[[SimpleAudioEnginesharedEngine] playEffect:@"laser_shoot.wave"];
7.结束
你可以做的事情还有:
*增加新的敌人类型
*增加新的炮塔类型
*增加多条敌人的行进路线
*拥有不同炮塔位置的不同关卡