(译)如何做一个塔防游戏(cocos2d 2012-8-17)

IOS 专栏收录该内容
40 篇文章 1 订阅

PS:一直关注http://www.raywenderlich.com/这个网站,前几天看了他们8月17发的一个塔防游戏教程,试了一下感觉不错,搜了一下没发现没有译成中文的(不知道现在有没有),就自己尝试翻译了一下,由于本人英语水平比较烂,翻译水平肯定不怎地,各位就凑合看吧,大体能看懂就行,呵呵。

原文链接地址:http://www.raywenderlich.com/15730/how-to-make-a-tower-defense-game

截图:

这是由IOS教程团队成员Pablo Ruiz(iOS游戏开发者,InfinixSoft的共同创始人和首席运营官)投递的一个教程贴子,你可以留意他的博客,或者在Twitter上关注他。

有很好理由证明塔防类型游戏是iOS上最流行的游戏类型之一,它让玩家体验到令人难以置信的乐趣,包括终极防御以及抹杀成群的侵略者。

在本教程中,您将使用cocos2d从头开始建立自己的一个塔防游戏!

在接下来的学习中,你会了解以下内容:
    .如何根据配置的间隔时间创建一波敌人。
    .如何使这些敌人沿着自定义的航点行走。
    .如何在地图上特定位置创建塔。
    .如何使塔射击敌人。
    .如何在视觉上调试敌人的航点以及塔的攻击范围。

在本教程结束时,你就会获得一个这种塔防类型游戏的坚实框架,,你将能够进一步拓展,添加新的塔型,敌人和地图!

在本教程前,您需要对cocos2d有一个基本的了解。如果cocos2d对于你来说是一个完全新的东西,那么你应该先在本网站检索一些其他的cocos2d教程看看先。

事不宜迟,让我们开始建立我们的防守!

A View from the Ivory Tower

如果你不熟悉的塔防类型,其实塔防游戏是战略游戏,你需要购买和武装塔定安放在战略要点上,以阻止一波又一波的敌人试图到达你的基地并摧毁它的。

每波敌人通常是比上一波更难,它们具有更快的移动速度以及更好的防护能力。当你在一波波敌人中能幸免于难(胜利),又或者有足够的敌人到达你的基地并摧毁它(你已经被打败了!),那么游戏结束。

这是游戏做好后的一个截图:

正如你所看到的,敌人从顶部左侧的屏幕出现,然后沿着绿色的路径到达玩家的基地。

在路的两旁,有很多的平台,玩家可以在每个平台上放置一个塔。玩家根据自己的金币要尽可能购买和放置尽可能多的塔。塔的攻击范围被画成一个白色的圆,如果敌人是塔的攻击范围内,那么塔就会射击敌人,直到敌人被摧毁,或他们离开塔的攻击范围。

Towers ‘R’ Us: Assembling the Resources

为了使你上手更快,我为你已经创建了一个启动项目。它包含一个空的cocos2d的项目,以及大部分在本教程中您将使用的素材资源。

因此,你可以到这下载启动的项目,并将其解压缩到您所选择的项目位置。

注:该项目的美术设计来自一个免费的美术设计包by Vicki,而这又是建立在另一个由花生壳工作室提供的免费的美术设计包基础上的。如果您喜欢这个美术风格,在花生壳工作室的这个家伙可供雇佣!

这个开始的项目是基于cocos2d 1.1模板建立的一个新的项目,这个新项目为您提供一个工作项目,有一个HelloWorldLayer,上面有一个标签在屏幕中间。你不会使用HelloWorldLayer在上面创建自己的UI,但是这给你一个基本cocos2d的启动项目,这样你就知道它正常工作。

在Xcode打开项目,然后编译并运行它,以确保一切工作正常。启动项目已经把“Hello World”文本从HelloWorldLayer移除,所以如果项目正常运行,你只看到一个黑色的屏幕。如果项目编译和运行,那么你的所有设置就准备好了!

在项目结构看一看。在里面的TowerDefense文件夹中,你会发现:
    .所有在游戏中使用的类
    .包含了所有cocos2d文件的libs文件夹
    .资源文件夹包含所有的图像和声音

现在,您可以设置地图,并开始建立塔!

Someone to Tower Over Me: Placement

首先,给场景添加背景图片。打开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)];

#1中的第一行,将允许该层接收到触摸事件。#2中的代码给我们的场景增加了一个的背景精灵。

在背景的地方,你可以直观地找出其中被允许放置塔的地方。现在,你需要沿路设置一些点上,在这些点上,玩家将能够触摸并建立一个塔。 (嘿 - 你得到建筑许可证了吗,老兄?)

为了方便管理,我们将用一个plist文件来存储塔的安置点,这样就可以很容易地改变它们。 TowersPosition.plist已包含在资源文件夹,其中已经有一些塔的安放位置。

查看这个文件,你会发现一个字典型的数组,字典只包含两个键“X”和“Y”。每个字典条目代表一个塔位置的x和y坐标。现在,你需要读取这个文件,并把塔基安置在地图上!

打开HelloWorldLayer.h,并添加下面的实例变量(在@interface行后面大括号里):

NSMutableArray * towerBases;

现在在HelloWorldLayer.m进行以下更改:

//Add a new method above "init"
-(void)loadTowerPositions
{
    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 setPosition:ccp([[towerPos objectForKey:@"x"] intValue],[[towerPos objectForKey:@"y"] intValue])];
        [towerBases addObject:towerBase];
    }
 
}
 
//In init, call this new method after section #2
// 3 - Load tower positions
[self loadTowerPositions];
 
//In dealloc, release the new array (before the call to super)
[towerBases release];

编译并运行应用程序,你会看到道路两侧的正方形方块,这些作为玩家的塔的基地。

现在,塔基都准备好了,下面开始建造,建立一些塔!

首先,打开HelloWorldLayer.h,添加一个属性(在右大括号后):

@property (nonatomic,retain) NSMutableArray *towers;

在HelloWorldLayer.m中@implementation行后实现塔的属性:

@synthesize towers;

现在,创建一个新类来表示塔。用iOS\Cocoa Touch\Objective-C类模板添加一个新文件,命名为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的内容替换为以下内容:

#import "Tower.h"
 
@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

塔类包含几个属性:一个精灵,是塔的可视化表示,一个父层的引用,方便访问父层;三个变量:
    .attackRange:代表塔可以攻击敌人的距离
    .damage:代表塔攻击敌人对敌人造成的伤害
    .fireRate:代表塔再次攻击敌人的时间间隔(冷却时间)。

仅这三个变量,你可以创建各种不同的塔,不同的攻击属性,如远程重击,需要很长的时间来重新加载,或者快枪手,射击很快,但攻击范围有限。最后,代码中包含一个draw方法,它在塔的周围绘制一个圆,显示出塔的攻击范围,这是方便我们调试的。

是时候让玩家添加一些塔了!

打开HelloWorldLayer.m,并进行以下更改:

//At the top of the file:
#import "Tower.h"
 
//Inside dealloc: 
[towers release];
 
//After the dealloc method, add the following methods:
-(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函数用于检测玩家是否有足够的金币来购买塔。但对于目前,假设你的玩家有很多金币,canBuyTower函数总是返回YES。
2、玩家违反建筑法规吗?如果设置了tb.UserData,那么这个塔基上已经有一个塔了,你不能再添加一个新的塔!

如果一切都检查通过,创建一个新的塔,将其放置在塔基上,然后将它添加到塔数组towers里。

编译并运行游戏。触摸任何塔基,你会看到一塔被添加上去,并且它的周围有一个白色的圆圈,显示出塔的攻击范围! Muahahaha,你现在武装上了!

但是,这一切的火力如果没有坏家伙的话就体现不出它的好处了,让我们邀请了一些敌人来参加party!

Tower Politics: Enemies, Waves and Waypoints

创建的敌人之前,让我们“铺平了道路”。敌人将遵循了一系列的航点,通过这些简单地相互连接点可以定义一个路径,敌人将按照你定义的路径在你的世界中行走。敌人从第一个航点出现,搜索列表中的下一个航点,移动到这个下一个航点,如此重复,直到他们到达列表中的最后一个航点—你的基地!如果敌人到达你的基地,你会受到损害。

用iOS\Cocoa Touch\Objective-C类模板添加一个新文件代表航点列表。将该类命名为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

首先,代码初始化一个waypoint对象,参数包含HelloWorldLayer对象的引用和代表航点位置的CGPoint。

每个航点包含了下一个航点,这将创建一个链接列表(你注意到数据结构类了吗?)。每个导航点“知道”列表中的下一个航点。通过这种方式,你可以引导敌人到达他们的最终目的地。在这个世界上从来没有退却的敌人,他们是小神风特攻队战士。


最后,draw函数用于显示航点位置,绘制一条直线与下一个航点连接,这方便用于调试。游戏的发行版本不能绘制敌人行走的路径—这会使游戏变得很简单!

创建航点的列表。打开HelloWorldLayer.h,并添加以下属性:

@property (nonatomic,retain) NSMutableArray *waypoints;

接下来,添加下面的代码到HelloWorldLayer.m中:

//At the top of the file:
#import "Waypoint.h"
 
// Add synthesise
@synthesize waypoints;
 
//Add this method above 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,在@end前,添加下面定义的方法:

-(BOOL)circle:(CGPoint)circlePoint withRadius:(float)radius collisionWithCircle:(CGPoint)circlePointTwo collisionCircleRadius:(float)radiusTwo;
void ccFillPoly(CGPoint *poli, int points, BOOL closePolygon);

接下来,打开HelloWorldLayer.m,并添加以下内容到文件末尾(@end前):

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,并添加下面的代码:

// Add these instance variables
int wave;
CCLabelBMFont *ui_wave_lbl;
 
// Add the following property to the properties section
@property (nonatomic,retain) NSMutableArray *enemies;

对HelloWorldLayer.m进行一下的更改:

// Synthesize enemies
@synthesize enemies;
 
// Release it in dealloc
[enemies release];

是时候创建一个类了,这个类将包含敌人的信息以及管理他们如何在屏幕上移动。用iOS\Cocoa Touch\Objective-C类模板添加一个新文件。命名为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引用作为参数初始化敌人对象。在里面的init方法中,有几个重要的变量设置:
    .MAXHP:定义敌人的血量。 (硬汉,是吗?)
    .walkingSpeed??:定义敌人的移动速度。
    .mySprite:存储的可视化表示的敌人图像。
    .destinationWaypoint:存储到下一个航点的引用。

update方法是奇迹发生的地方。它每一帧都会被调用,它首先调用你早先写的collisionWithCircle方法检查看它是否到达了目的地航点,如果是的话,则就前进到下一个航点——除非敌人被玩家的塔打死。

然后,它根据移动速度沿一条直线移动精灵一直到目的地航点,它通过以下算法实现这个过程:

找出从当前位置到目标位置的一个向量,然后为了方便计算使这个向量长度为1(归一化变量)。

把移动速度乘以归一化的向量以获得这一帧的移动量。将它添加到当前位置以移动到新的位置

最后,draw方法在精灵上面绘制实现一个简单血条。它首先绘制一个红色的背景,然后根据当前敌人的HP绘制绿色覆盖红色背景。

现在Enemy类完成后,您可以把它们显示在屏幕上了!

打开HelloWorldLayer.h,并添加一个方法定义:

-(void)enemyGotKilled;

切换到HelloWorldLayer.m,并进行以下更改:

/At the top of the file:
#import "Enemy.h"
 
//Add the following methods before the init method:
-(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]]];
        }
    }
}
 
// Add the following to the end of the init method:
// 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方法用于检查出现在屏幕上的敌人的数量,如果没有的话,就加载下一波敌人。在后面,这个方法还将被用来确定玩家是否已经赢得游戏。

编译并运行现在的游戏。啊哈!坏蛋正向您宝贵的基地前进!(Betcha that old “All your base” meme popped into your head! Don’t feel too bad — it popped into our heads, too.)

Tower Wars: The Attack of the Towers

塔安放了吗?检查一下,敌人前进吗?仔细检查——它们看起来很有意思!看起来是时候修理一下那些坏蛋了!是编写智能塔代码的时候了。

每座塔会进行检查,看看是否有敌人在攻击范围内。如果有,塔将开始对敌人开火,直到以下两件事情其中有一件发生:要么是敌人走出了射击范围,要么是敌人被消灭。之后塔会重新开始寻找另一名受害者。

把塔建起来,新兵!你有一个基地需要捍卫!

首先,打开Tower.h,并进行以下更改:

// Add some instance variables
BOOL attacking;
Enemy *chosenEnemy;
 
// Add method definitions
-(void)targetKilled;

对Tower.m进行以下更改:

// Import Enemy header at the top of the file:
#import "Enemy.h"
 
// Add the following methods above the update method
-(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,改变下面的代码:

// Add instance variable
NSMutableArray *attackedBy;
 
// Add method definitions
-(void)getAttacked:(Tower *)attacker;
-(void)gotLostSight:(Tower *)attacker;
-(void)getDamaged:(int)damage;

在Enemy.m中更改下面的代码:

// Add the following at the beginning of initWithTheGame: (within the "if" condition)
attackedBy = [[NSMutableArray alloc] initWithCapacity:5];
 
// Replace the contents of  getRemoved method with the following:
-(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];
}
 
// Add the following methods to the end of the file
-(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];
    }
}

在代码中最重要的部分是在塔的update方法。塔将不断地检查,看看敌人是否在射程范围内。如果在,那么,我们的塔将旋转,并开始向敌人开火。

一个敌人一旦被标记为被攻击,一个方法将根据塔的攻击频率初始化子弹并按照射速发射子弹。反过来,每一个敌人有一系列的能攻击它,那么如果敌人被杀死,塔可以标志并停止攻击敌人。

编译并运行您的应用程序!将几个塔在地图上。您将看到一旦敌人进入塔的攻击范围内塔就会向敌人射击,敌人的血量将减少,血条表示他们能承受多少的伤害,直到最后它们都被消灭!胜利就在眼前!


唷!好了,距离你拥有一个全功能的塔防游戏,只剩下一些小细节了!音效将是一个良好的体验。尽管不可战胜以及财富无限是一件美妙的事情,但你的基地如果被敌人攻击的话还是应该受到伤害的——并且你需要限制玩家的金钱供应量。

The Shining Tower: Gotta Polish It All!

开始显示玩家的剩余血量——当玩家的生命都没有了,会发生什么!

打开HelloWorldLayer.h,并添加下面的三个实例变量:

int playerHp;
CCLabelBMFont *ui_hp_lbl;
BOOL gameEnded;

playerHp表示玩家有多少的生命,CCLabelBMFont是一个标签,将显示生命数。gameEnded代表游戏结束状态!此外,还添加下面的方法的定义:

-(void)getHpDamage;
-(void)doGameOver;

现在,打开HelloWorldLayer.m,并进行以下更改:

// At the end of init, add the following lines of code:
// 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中检查当敌人走完所有路径时将会发生什么。幸运的是,你已经在前面的代码块实现了这个,所以接下来很容易!

编译并运行游戏,但这个时候,尝试手下留情让敌人到达道路的终点。

您将看到玩家的生命在减少,直到输掉比赛。

好了,胖猫,是时候限制金币的供应量了。

大多数游戏都实现了“零和”功能,建每座塔需要花费一定资源,并给玩家有限的资源分配。你的应用程序将执行一个类似的模型,但是一个非常简单的方式。

打开HelloWorldLayer.h,并添加下面的实例变量:

int playerGold;
CCLabelBMFont *ui_gold_lbl;

像显示生命数一样,添加一个变量保存金币数,还有一个标签来显示金币数。同时,添加一个新的方法定义:

-(void)awardGold:(int)gold;

现在,打开HelloWorldLayer.m做以下几点:

//Add the following method before init
-(void)awardGold:(int)gold {
    playerGold += gold;
    [ui_gold_lbl setString:[NSString stringWithFormat:@"GOLD: %d",playerGold]];
}
 
// Add at the end of 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)];
 
//Replace canBuyTower method with the following:
-(BOOL)canBuyTower {
    if (playerGold - kTOWER_COST >=0)
        return YES;
    return NO;
}
 
// In ccTouchesBegan, add the following lines inside the if statement, where you commented that we would spend our gold later:
playerGold -= kTOWER_COST;
[ui_gold_lbl setString:[NSString stringWithFormat:@"GOLD: %d",playerGold]];

上述新的代码中,当玩家尝试建塔时会检查是否有足够的金币。如果有,那么塔就能建并且会从玩家金币中扣去塔的成本。玩家应该从他们的枪法中获得奖励——每次杀死一个敌人会奖励玩家一些金币。

在Enemy.m的getDamaged方法(在“if”条件里)添加下面一行:

[theGame awardGold:200];

运行现在的游戏,你会发现,你不能像以前那样把尽情建塔了,因为每一个塔需要花费一些金币。当然,杀死敌人获得金币,让您可以继续购买更多的塔!这是一个很好的系统,不是吗?

到现在,最后,为做得更好,让你的游戏多几分乐趣,添加一些很酷的背景音乐( created by Kevin MacLeod )和一些声音效果(made with cxfr)!

打开HelloWorldLayer.m,并进行以下更改:

//At the top of the file:
#import "SimpleAudioEngine.h"
 
//At the beginning of init: (inside the "if" condition)
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"8bitDungeonLevel.mp3" loop:YES];
 
//Inside ccTouchesBegan, before instantiating a new Tower object:
[[SimpleAudioEngine sharedEngine] playEffect:@"tower_place.wav"];
 
//At the beginning of getHpDamage
[[SimpleAudioEngine sharedEngine] playEffect:@"life_lose.wav"];

现在,打开Enemy.m,并添加以下几行:

//At the top of the file:
#import "SimpleAudioEngine.h"
 
//At the beginning of getDamaged:
[[SimpleAudioEngine sharedEngine] playEffect:@"laser_shoot.wav"];

就这么多,你完全做到了!编译并运行游戏,并和周围的人一起玩。难道你不喜欢那些复古的声音吗?

Where To Go From Here?

这是一个示例项目,包含了上面教程中的所有代码。

如果你想继续深入这个项目,天空无极限!有很多的事情可以做,以改善这个游戏,并使其成为一个成功的游戏。这里只是一些想法:

    .新的敌人类型,有不同的速度,血量等
    .新型的塔,具有独特的攻击模式和成本
    .设置多种导航点实现多种敌人行走路线模式
    .根据不同的塔基配置有不同的关卡

如果你拓展了游戏,添加任何很酷的新功能,或有任何意见或疑问,请加入论坛讨论!


  • 1
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值