Как написать игру в стиле Tower Defense

Автор оригинала: Pablo Ruiz
Жанр Tower Defence один из самых популярных жанров игр на iOS. Причиной тому веселое времяпрепровождение, сдобренное постройкой башен для создания последней точки обороны против орд монстров, пытающихся эту самую оборону прорвать.
Представляю вашему вниманию перевод статьи, написанной Pablo Ruiz, и найденной мной на сайте raywenderlich.com.
В этом уроке будет показано, как создать игру Tower Defense с нуля, с помощью Cocos2D.
В процессе вы выучите следующее:
  • Как создавать волны врагов и настраивать время их появления.
  • Как заставить этих врагов двигаться по заданным точкам(waypoint'ам).
  • Как строить башни на специально отведенных местах на карте.
  • Как заставить башни стрелять во врагов.
  • Как визуально представить waypoint'ы и радиусы атак башен.

В конце урока, у вас будет свой фрэймворк для создания игр такого типа, который вы сможете расширять, добавляя новые типы врагов, башен и карт.
Для понимания этого урока, вам нужно базовое понимание Cocos2D. Если вы новичок, то можете взглянуть на более легкие уроки на сайте Рэя.

Вид с Башни из Слоновой Кости

Если вы не знакомы с жанром, Tower Defence представляет собой стратегическую игру, где игрок покупает и располагает вооруженные башни в стратегических точках, чтобы остановить волны врагов, которые пытаются достичь базы и уничтожить ее.
Каждая последующая волна врагов обычно сильнее предыдущей, достигается это за счет увеличения устойчивости к вашему оружию и способности быстрее двигаться. Игра заканчивается в случае, когда вы выстояли все волны врагов(Победа!), либо же в случае когда враги добрались до базы и уничтожили ее(Проигрыш!).
Вот скриншот игры, которую мы закончим писать в конце урока:

image

Как вы можете видеть на картинке, враги появляются из левой верхней части экрана и следуют вдоль зеленого маршрута к базе игрока.
Вдоль дороги множество платформ, где игрок может установить башню. Игрок может купить ровно столько башен, сколько позволяют запасы золота. Радиусы атаки башен, показаны белым кругом, если враги находятся внутри этого круга, башня будет стрелять по ним пока не уничтожит, либо враги не выйдут из радиуса атаки.

Подготовка ресурсов

Для вас уже подготовлен стартовый проект, содержащий в себе пустой шаблон Cocos2D и большинство ресурсов, которые мы будем использовать в ходе урока.
Стартовый проект содержит базовый шаблон Cocos2D 1.1, который предоставляет собой работающее приложение содержащее HelloWorldLayer и строку по середине экрана. Мы не будем использовать HelloWorldLayer, потому что создадим собственный интерфейс, и все же данный класс приводится для того, чтобы вы были уверены, что на данном этапе все работает.
Откройте проект в Xcode, скомпилируйте и запустите его. Текст «Hello World» удален из проекта, поэтому вы просто должны получить черный экран и не получить ошибок в процессе запуска.
Взгляните на структуру проекта. Внутри папки TowerDefense вы найдете:
  • Все классы использованные в игре
  • Папка с библиотекой Cocos2D
  • Папка ресурсов с необходимыми графикой и звуками

Теперь вы можете начать настраивать карту и создавать башни!

Установка башен

Вначале, добавьте фоновую картинку на сцену. Откройте класс HelloWorldLayer.m и добавьте следующие строчки кода внутрь условия «if» в методе «init»:

// 1 - инициализация
self.isTouchEnabled = YES;
CGSize wins = [CCDirector sharedDirector].winSize;
// 2 - назначение фона    
CCSprite * background = [CCSprite spriteWithFile:@"Bg.png"];
[self addChild:background];
[background setPosition:ccp(wins.width/2,wins.height/2)];

Первая строчка в разделе #1 позволяет слою принимать события касания. Остальная часть кода в разделе #2 добавляет фоновый спрайт на нашу сцену.
Фоновая картинка помогает увидеть, где игрок может ставить башни. Теперь вам нужно задать несколько точек на экране, касаясь которых игрок сможет строить башни.
Чтобы сохранить смысл наших действий, все точки будут находиться в файле ".plist", в котором их можно будет легко изменить. TowerPosition.plist находится в папке Resources и уже содержит несколько позиций для постройки башен внутри.
Рассмотрите этот файл, вы найдете массив словарей, которые содержат всего два ключа: «x» и «y». Каждый словарь представляет собой позицию башни по ее координатам на экране. Теперь вам нужно прописать этот файл и установить базы для башен на карте.
Откройте HelloWorldLayer.h и добавьте следующую переменную (внутрь фигурных скобок после строчки interface):

NSMutableArray * towerBases;

Внесите следующие изменения в HelloWorldLayer.m:
Код
//Добавьте новый метод над "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];
    }
 
}
 
//Внутри init, вызовите этот метод после раздела #2
// 3 - Загрузка позиции башен
[self loadTowerPositions];
 
//В dealloc, освободите память нового массива (перед вызовом super)
[towerBases release];

Скомпилируйте и запустите приложение, вы увидите квадраты по сторонам дороги, они будут служить базой для установки башен.

image

Теперь эти базы готовы, давайте построим несколько башен!
Вначале, откройте 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: определяет сколько времени нужно башни на перезарядку

С помощью этих трех переменных, вы можете создать огромное множество видов башен с разными типами атак. И, наконец, код содержит метод отрисовки радиуса атаки башни для удобства тестирования.
Настало время позволить игроку добавлять башни!
Откройте 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)
			{
				 //Мы потратим наше золото позже.
 
                		Tower * tower = [Tower nodeWithTheGame:self location:tb.position];
                		[towers addObject:tower];
               			 tb.userData = tower;
			}
		}
	}
}

ccTouchesBegan: фиксирует касания экрана. Затем код производит итерацию по массиву towerBases и проверяет, содержит ли какая-нибудь из баз башен точку касания.
Но до того, как башни могут быть сделаны, вы должны проверить две вещи:
  1. Может ли игрок позволить такое удовольствие? Метод canBuyTower проверит, имеет ли пользователь достаточно золота, чтобы купить башню. Хотя, на данный момент, у игрока все золото форта Нокс и метод будет возвращать всегда YES.
  2. Нарушает ли игрок правила постройки? Если tb.UserData задана, тогда на том месте уже стоит башня и вы не можете построить новую.

Если все условия выполнены, создается новая башня, устанавливается на базу и добавляется в массив башен.
Скомпилируйте и запустите игру. Коснитесь любой базы, и вы увидите, что добавляется башня с белым кругом вокруг нее, показывающим радиус ее атаки.

image
Но к чему все это вооружение без «плохих парней», давайте позовем их на вечеринку!

Политика игры: Враги, Волны и Вэйпоинты

До того как создадим врагов, давайте проложим дорогу для них. Враги будут следовать по маршруту из вэйпоинтов, которые по сути являются точками, соединенными между собой и определяющими путь движения врагов в вашем мире. Враги будут появляться на первом вэйпоинте, будут искать следующий в списке, двигаться к нему, и так будет повторяться, пока они не достигнут последнего — вашей базы! Если это произойдет — вы понесете урон.
Создайте список вэйпоинтов путем создания нового файла из 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

Вначале код создает объект вэйпоинта, ссылаясь на объект HelloWorldLayer и определяя CGPoint, который является позицией вэйпоинта.
Каждый вэйпоинт содержит ссылку на следующий, это создает связанный список вэйпоинтов. Каждый вэйпоинт «знает» следующего в списке.Таким образом вы можете вести врагов к их финальной цели, продвигаясь по цепочке вэйпоинтов.
В конце метод отрисовки показывает нам, где находятся вэйпоинты, и рисует линию соединения между ними, в целях тестирования.
Создайте список вэйпоинтов. Откройте HelloWorldLayer.h и добавьте следующую переменную:
Код
@property (nonatomic,retain) NSMutableArray *waypoints;

Добавьте следующий код в HelloWorldLayer.m:

//В самом верху файла:
#import "Waypoint.h"
 
// Добавьте 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;
 
}
 
// В конце метода init:
// 4 - Add waypoints
[self addWaypoints];
 
//Внутри dealloc
[waypoints release];

Скомпилируйте и запустите игру:

image

На карте шесть вэйпоинтов, по этому пути будут следовать враги. Перед тем как вы дадите друзьям проиграть в игре, вам нужно добавить несколько вспомогательных методов.
Для начала добавьте описание методов в заголовочный файл, чтобы другие классы могли использовать эти методы без предупреждений от компилятора.
Откройте 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 ) {
    // Обычные состояния GL: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
    // Необходимые состояния: GL_VERTEX_ARRAY,
    // Ненужные состояния: 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
@property (nonatomic,retain) NSMutableArray *enemies;

Внесите следующие изменения в HelloWorldLayer.m:

// Synthesize enemies
@synthesize enemies;
 
// В 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
        {
            //Достигнут конец пути. Нанесение повреждений игроку
            [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];
 
    //Сообщите игре что мы убили врага и можно проверить если можно запустить новую волну
    [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, для удобства работы(нормализованная переменная).
  • Нормализованный вектор умножается на скорость передвижения, для вычисления количества пути для преодоления в данном кадре. Ему присваивается нынешняя позиция для получения следующей.

Наконец, метод отрисовки создает полоску здоровья над спрайтом. Вначале он рисует красный фон, затем покрывает его зеленым, согласно нынешнему количеству HP врага.
Класс врагов готов, вы можете показать их на экране!
Откройте 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 - добавляем врагов
enemies = [[NSMutableArray alloc] init];
[self loadWave];
// 6 - создаем надпись про номер волны
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, и вы заметите, что он содержит 3 массива. Каждый из них представляет собой волну, которая является группой врагов, появляющихся вместе. Первый массив содержит шесть словарей. Каждый словарь представляет собой врага. В этом уроке, словарь хранит только время появления врага, но может содержать в себе тип врага, его способности, здоровье и т.д.
Метод loadWave проверяет, должна ли появиться следующая волна, создает соответствующих врагов, основываясь на информации о волне, и расписывает их время появления соответственно.
Метод enemyGotKilled проверяет количество врагов на экране, если их нет, вызывает следующую волну. Позже этот же метод будет использован для проверки победы игрока.
Скомпилируйте и запустите игру сейчас. Враги двигаются к вашей базе!

image

Атака башен

Башни на месте? Проверьте. Враги двигаются? Перепроверьте! Пришло время завалить этих наглецов.
Каждая башня проверяет есть ли враг внутри радиуса ее поражения. Если да, башня начинает стрелять по нему, пока не произойдет одно из двух: враг выйдет за пределы досягаемости, или враг будет уничтожен. Тогда башня начнет искать новую жертву.
Начнем защищать базу!
Откройте Tower.h и внесите следующие изменения:
Код
// добавьте несколько переменных
BOOL attacking;
Enemy *chosenEnemy;
 
// добавьте описание методов
-(void)targetKilled;

Теперь в Tower.m:

// импортируйте хэдер Enemy класса
#import "Enemy.h"
 
// Добавьте  следующие методы над init:
-(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){
 
        //Мы заставляем ее повернуться к выбранному врагу
        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;
            }
        }
    }
}

Кода много- это так. Плюс, вы наверно заметили увеличивающиеся количество предупреждений от компилятора по мере добавления кода. Во-первых, разберемся с предупреждениями, добавив несколько недостающих бит.
Откройте Enemy.h и внесите следующие изменения:
Код
//добавьте переменную
NSMutableArray *attackedBy;
 
// добавьте описание методов
-(void)getAttacked:(Tower *)attacker;
-(void)gotLostSight:(Tower *)attacker;
-(void)getDamaged:(int)damage;

Теперь в Enemy.m:
Код
//Добавьте следующее в начало метода initWithTheGame: (внутри условия "if")
attackedBy = [[NSMutableArray alloc] initWithCapacity:5];
 
// Замените содержимое  getRemoved:
-(void)getRemoved
{
    for(Tower * attacker in attackedBy)
    {
        [attacker targetKilled];
    }
 
    [self.parent removeChild:self cleanup:YES];
    [theGame.enemies removeObject:self];
 
    //Сообщите игре, что мы убили врага и можно проверить, нужно ли выслать новую волну
    [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];
    }
}

Самая важная часть в коде- метод update в классе Tower. Башня постоянно будет проверять на наличие врага внутри радиуса атаки. Если таковой имеется, башня поворачивается к нему и начинает стрелять.
Как только враг помечен, как цель, метод задает расписание стрельбы башни согласно ее скорострельности. Каждый враг содержит список башен, стреляющих по нему, если враг умрет, они все будут оповещены об этом факте и перестанут стрелять.
Скомпилируйте и запустите ваше приложение. Расставьте несколько башен на карте. Вы увидите, как башни начнут стрелять во врагов, проходящих внутри их радиуса, как меняются полоски их здоровья до тех пор, пока они не умирают.

image

Осталось добавить всего несколько деталей, чтобы получить готовую версию tower defense. Неплохо бы добавить звуковые эффекты, научить базу получать урон от врагов и ограничить количество золота игрока.

Финальные штрихи

Начнем с отображения количества жизней, оставшихся у игрока, и тем, что будет, когда они закончатся!
Откройте HelloWorldLayer.h и добавьте следующие переменные:
Код
int playerHp;
CCLabelBMFont *ui_hp_lbl;
BOOL gameEnded;

playerHp показывает сколько жизней у игрока, а CCLabelBMFont это надпись, которая демонстрирует это количество. gameEnded задается, когда игра закончена! Также добавьте следующие описания методов:

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

Теперь откройте HelloWorldLayer.m и внесите следующие изменения:

// В конце метода init:
// 7 - жизни игрока
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)];
 
// Добавьте следующие методы в конец файла:
-(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 вызывается, когда враг достигает базы. Вы ранее уже добавили его в update метод в Enemy.m.
Скомпилируйте и запустите игру. Позвольте врагам дойти до вашей базы. Вы должны увидеть, как количество ваших жизней уменьшается, пока игра не оканчивается.

image

Пришло время урезать бюджет!
Множество игр внедряют опцию “zero-sum”, предоставляя игроку ограниченное количество ресурсов на постройку в начале игры. Ваша игра будет иметь нечто похожее, но в упрощенном виде.
Откройте HelloWorldLayer.h и добавьте следующие переменные:

int playerGold;
CCLabelBMFont *ui_gold_lbl;

Также как с жизнями, была добавлена переменная количества золота(playerGold) и надпись для его отображения(ui_gold_lbl). Также добавьте описание нового метода:

-(void)awardGold:(int)gold;

Теперь откройте HelloWorldLayer.m и сделайте следующее:
Код
//Добавьте метод перед init:
-(void)awardGold:(int)gold {
    playerGold += gold;
    [ui_gold_lbl setString:[NSString stringWithFormat:@"GOLD: %d",playerGold]];
}
 
// В конце init:
// 8 - Золото
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]];

Новый код вверху проверяет, хватает ли золота каждый раз, как игрок строит башню. Если хватает, башня строится, цена башни вычитается из количества золота игрока. Нужно также награждать игрока за убийство врагов.
Добавьте следующую строчку в метод getDamaged:(внутри условия “if”) в Enemy.m:

[theGame awardGold:200];

Запустите игру и вы увидите, что вы можете строить ровно столько башен, сколько позволяет запас золота. За убийство врагов золото добавляется. Восхитительная система, неправда ли?

image

Наконец добавим немного музыки!
Откройте HelloWorldLayer.m:
Код
//В верхушке файла:
#import "SimpleAudioEngine.h"
 
//В начале метода init: (внутри условия 'if')
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"8bitDungeonLevel.mp3" loop:YES];
 
//Внутри метода ccTouchesBegan, перед созданием нового объекта Tower:
[[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"];

Все! Вы закончили! Запустите игру, как вам ретро звуки?!
Сэмпл проект со всем что мы сделали выше: TowerDefenseFinished.zip
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 24

    +3
    Огромное спасибо! Побольше бы таких статей, где весь процесс разработки описан. Новичкам очень помогает.
      +38
      Откройте рот. Пошире. Теперь ложкой зачерпните немного еды. Для вас уже подготовлен стартовый проект, содержащий в себе пустой шаблон еды. Положите ложку в рот таким образом, что еда окажется во рту, причем выпуклость ложки будет снизу (независимо от положения рта). Я лично рекомендовал бы принять такую позу, когда нёбо будет сверху, а подбородок — снизу, это наиболее удобная поза для принятия еды. Сомкните губы. Выньте ложку. Скорость вынимания ложки подберите так, дабы пища не вылетала за пределы губ. Продолжайте занятия и в конце у вас будет удобный фреймверк для засовывания в рот и других предметов, к примеру вилки и даже ножа, расширяя функционал! Ну как, вкусно? При этом мы совершенно ничего не рассказали ни про еду, ни про ложки. Вива современное образование, событийное и беспощадное.

      Ну и да, Товердефенс — отличная игра, только достало, что других игр то в общем-то и нет.
        +1
        Я специально с нуля начал, чтобы разобраться и в волну попасть. Крестики-нолики на CSS и JS делаю ><
          +10
          Ждём статей «Как снять девушку»
            +2
            Нетехническая статья, меня же зобанят
              +3
              Пишите про девушку-робота с оговоркой, что это работает и на обычных ;)
            0
            На чужую реализацию всегда интересно посмотреть, тем более не очевидного потомка С. Атомарные шаги действительно стоило пропустить или оставить в комментариях к коду.
          0
          Раз уж тема такая, вот скажите: на PS3 есть замечательная игра Comet Crash из этого жанра. Ничего даже близкого к ней не на PS я не видел, а хотелось бы. Даже сам написать порывался неоднократно. Может, кто что подскажет?
            0
            Ну я подскажу таки прорваться и написать!
            +3
            Хмм… тоже что-ли перевести один из туториалов с этого сайта? Например… про платформер?
              0
              Если есть такая возможность, конечно переводите!
              0
              о, про платформеры там вообще отличный материал
              0
              www.ant-karlov.ru — тут тоже уроки по tower defence, если кто собирается писать подобные игры сча, можно почитать…
                0
                Да, если быть точнее — тут находится серия статей по созданию TD. Неплохой такой гайд.
                  0
                  Обожаю этот блог.
                  +14
                  Извините, но чтобы переводить на русский, нужно знать русский.
                  ПроИгрыш (как и воИн, АндроИд и т.д.) Специально пишу не в личку, потому что это не ошибка, а уже болезнь целого поколения.
                    +3
                    Точно, бойлезнь какая-то!
                    +1
                    Как фанат TD, хочу сказать, что то, что вы описали — это самый «стаб», примитивная реализация. Эпоха первых TD времён xenotactics давно закончилась. Общественность жаждет игр класса gemcraft, kingdom rush, on slaughth. Другими словами, нетривиальной механики.
                      0
                      Так а что вам мешает? Тривиальный процесс описан, дальше вы уже сами!
                        +2
                        Объём временных затрат. Я хорошо себе представляю процесс написания подобных вещей, чтобы точно знать, что у меня не хватит ни терпения, ни времени, ни энтузиазма.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое