- PVSM.RU - https://www.pvsm.ru -

Как сделать простую игру для iPhone с помощью Cocos2D

Cocos2D – это мощная библиотека, которая позволяет значительно ускорить процесс разработки игр под iPhone. Она включает в себя поддержку спрайтов, классные графические эффекты, анимации, физические библиотеки, звуковые движки и многое другое.

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

Эта серия туториалов покажет вам весь процесс создания простой игры для iPhone с помощью Cocos2D, от начала до конца. Можете читать учебники от корки до корки, а можете сразу обратиться к проекту – образцу игры в конце статьи. И вот что ещё. Будут ниндзя.

Hello, Cocos2D!

Давайте начнём с того, что создадим простой Hello World проект и запустим его с помощью шаблона Сосоs2d, который мы только что установили. Перейдите к созданию нового проекта в Xcode, выберите cocos2d Application и назовите проект “Cocos2DSimpleGame”.

Как сделать простую игру для iPhone с помощью Cocos2D
Сразу после создания запустите проект на выполнение. Если никаких ошибок нет, вы должны увидеть следующее:
Как сделать простую игру для iPhone с помощью Cocos2D
Сocos2D построен на использовании коцепции “сцен” (scenes), которые являются чем-то вроде “уровней” или “экранов” для игры. Например у вас может быть сцена для первоначального меню игры, ещё одна для основного действия и ещё одна для окончания игры. Внутри сцены вы можете иметь некоторое количество слоёв (layers) (вроде как в Фотошопе), а слои могут содержать в себе так называемые “узлы” (nodes), такие как спрайты, метки, меню и так далее. Узлы могут также содержать в себе другие узлы (например спрайт может иметь внутри себя другой спрайт – “потомок”).

Если вы заглянете внутрь нашего проекта, то увидите, что там пока что только один слой – HelloWorldLayer – и мы будем реализовывать наш игровой процесс именно в нём. Давайте откроем его и вы увидите, что на данный момент в его init методе к нему добавляется метка с надписью “Hello World”. Мы её уберем, а на её место поставим спрайт.

Добавление спрайта

Для того, чтобы добавить спрайт, нам нужны будут изображения. Вы можете создать свои собственные или взять мои:
Как сделать простую игру для iPhone с помощью Cocos2D
Как сделать простую игру для iPhone с помощью Cocos2D
Как сделать простую игру для iPhone с помощью Cocos2D
Как только вы скачали файлы с изображениями – перетащите их в папку ресурсов в XCode и выберите “Copy items into destination group’s folder (if needed)”.

Изображения готовы и нам надо подумать где лучше всего расположить главного героя. Обратите внимание, что в Cocos2D левый нижний угол является начальной точкой отсчёта координат, то есть имеет координаты (0,0), а х и у координаты возрастают по мере продвижения, соответственно, вперёд и вправо. Поскольку наш проект имеет ландшафтную ориентацию, это значит что правый верхний угол имеет координаты (480, 320).

Также обратите внимание, что по умолчанию когда мы задаём позицию для объекта, позиция устанавливается относительно центра добавляемого спрайта. То есть, если мы хотим, чтобы спрайт нашего главного был вровень с левым краем экрана и отцентрирован по вертикали:

— Х координату зададим как: ширина спрайта главного героя / 2.
— У координату как: высота экрана / 2.

Вот картинка, которая иллюстрирует это наглядно:
Как сделать простую игру для iPhone с помощью Cocos2D
Ну что, давайте попробуем! Откройте папку Classes, кликните на HelloWorldLayer.m и замените содержимое метода init следующим кодом:

-(id) init
{
  if( (self=[super init] )) {
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    CCSprite *player = [CCSprite spriteWithFile:@"Player.png" 
      rect:CGRectMake(0, 0, 27, 40)];
    player.position = ccp(player.contentSize.width/2, winSize.height/2);
    [self addChild:player];		
  }
  return self;
}

Можно компилировать проект и запускать его на выполнение, и спрайт должен появиться на месте как положено, но обратите внимание, что по умолчанию бэкграунд чёрный. А для нашего творчества белый будет смотреться намного лучше. Легким способом задать бэкграунду слоя в Cocos2D необходимый цвет является использование класса CCLayerColor. Давайте попробуем. Кликните на HelloWorldLayer.h и измените интерфейс класса HelloWorld на следующий:

@interface HelloWorldLayer : CCLayerColor

Затем кликните на HelloWorldLayer.m и внесите небольшое изменение в init метод, чтобы бэкграунд изменился на белый:

if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {

Откомпилируйте и запустите проект, и вы должны увидите свой спрайт поверх белого бэкграунда. Ура, наш ниндзя готов к действию!
Как сделать простую игру для iPhone с помощью Cocos2D

Передвижение целей

Теперь к нашей сцене надо добавить врагов, с которыми будет сражаться наш ниндзя. Чтобы было интереснее – врагов надо сделать движущимися. Давайте будем создавать врагов немного правее экрана и задавать им движение влево.

Добавьте следующий метод прямо перед инитом:

-(void)addTarget {
 
  CCSprite *target = [CCSprite spriteWithFile:@"Target.png" 
    rect:CGRectMake(0, 0, 27, 40)]; 
 
  // Определяем У координату для создания цели
  CGSize winSize = [[CCDirector sharedDirector] winSize];
  int minY = target.contentSize.height/2;
  int maxY = winSize.height - target.contentSize.height/2;
  int rangeY = maxY - minY;
  int actualY = (arc4random() % rangeY) + minY;
 
  // Создаем цель чуть-чуть за правым краем экрана,
  // и на случайной позиции по оси У, как показано выше
  target.position = ccp(winSize.width + (target.contentSize.width/2), actualY);
  [self addChild:target];
 
  // Задаём скорость движения цели 
  int minDuration = 2.0;
  int maxDuration = 4.0;
  int rangeDuration = maxDuration - minDuration;
  int actualDuration = (arc4random() % rangeDuration) + minDuration;
 
  // Задаём действие
  id actionMove = [CCMoveTo actionWithDuration:actualDuration 
    position:ccp(-target.contentSize.width/2, actualY)];
  id actionMoveDone = [CCCallFuncN actionWithTarget:self 
    selector:@selector(spriteMoveFinished:)];
  [target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
 
}

Я оставил подробные комментарии в коде, чтобы всем всё было понятно. Основная часть кода основывается на том, с чем мы уже успели разобраться: мы выполняем несложные вычисления, для того, чтобы определить где мы будем создавать объект, задаём позицию объекта и добавляем его к сцене таким же образом, как и спрайт главного героя.

Новым элементом здесь является добавление действий ( actions ). Cocos2D предоставляет множество чрезвычайно удобных, встроенных действий, которые вы можете использовать, чтобы анимировать ваши спрайты – действия движения, прыжков, постепенного исчезновения, появления и многое другое. Мы применяем к нашей цели три действия:

— CCMoveTo: Мы используем CCMoveTo чтобы направлять объект влево. Обратите внимание, что мы можем задать длительность движения, и в данном случае длительность является случайной величиной в пределах 2 – 4 секунд.
— CCCallFuncN: Функция CCCallFuncN позволяет задать колбэк метод, который будет выполняться, когда действие выпонено. Мы задаём колбэк, который называется “spriteMoveFinished”, которого пока что нет, мы напишем его позже.
— CCSequence: Действие CCSequence позволяет нам создать цепочку дейтвий, которые будут выполняться по очереди. Таким образом, сначала выпонится действие CCMoveTo, а после него CCCallFuncN.

Теперь надо добавить колбэк функцию, на которую мы ссылаемся в действии CCCallFuncN. Её можно добавить прямо перед addTarget:

-(void)spriteMoveFinished:(id)sender {
  CCSprite *sprite = (CCSprite *)sender;
  [self removeChild:sprite cleanup:YES];
}

Смысл этой функции в том, чтобы убирать спрайты со сцены, как только они выходят за пределы экрана. Это очень важно, иначе мы получим утечки памяти из-за того, что за экраном у нас куча неиспользуемых спрайтов. Можно заметить, что существуют иные (и лучшие) способы решить эту проблему, например можно использовать массив многократно используемых спрайтов, но для простого туториала для начинающих и так сойдёт.

И ещё кое-что. Нам необходимо, собственно, вызывать метод, создающий цели. И чтобы было веселее, пусть цели создаются регулярно по прошествию промежутка времени. В Cocos2D этого можно достичь с помощью функции schedule:, в котрую передаётся селектор колбэковой функции, которая будет периодически вызываться. Один раз в секунду – как раз то, что надо. Добавьте следующую строчку в init метод прямо перед return:

[self schedule:@selector(gameLogic:) interval:1.0];

А теперь осталось просто реализовать колбэк функцию:

-(void)gameLogic:(ccTime)dt {
  [self addTarget];
}

Вот и всё! Теперь, если запустить проект на выполнение, то можно увидеть, как цели бодро движутся через экран:
Как сделать простую игру для iPhone с помощью Cocos2D

Метание звёздочек

В данный момент вступает в действие главный герой-ниндзя – давайте добавим метание звёздочек. Метание можно выполнить различными способами, но для этой игры мы сделаем так – когда пользователь прикасается к экрану, ниндзя бросает звёздочку в направлении касания.

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

Вот подходящая иллюстрация:
Как сделать простую игру для iPhone с помощью Cocos2D
Как видите, мы имеем небольшой треугольник, полученый с помощью разницы между позицией ниндзи и точкой касания по осям х и у. Нам всего лишь надо создать больший треугольник, с таким же точно соотношением сторон и чтобы одна из вершин треугольника находилась за пределами экрана.

Перейдём к коду. Сначала нам надо активировать обработку касаний. Добавьте следующую строчку в метод init:

self.isTouchEnabled = YES;

Поскольку обработка касаний активирована, мы теперь можем пользоваться колбэками, вызываемыми при касаниях. Давайте реализуем метод ccTouchesEnded, который будет вызываться всякий раз, когда пользователь убирает убирает палец с экрана, следующим образом:

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
  // Выбираем одно из касаний, с которым будем работать
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInView:[touch view]];
  location = [[CCDirector sharedDirector] convertToGL:location];
 
  // Устанавливаем начальное местоположение звёздочки
  CGSize winSize = [[CCDirector sharedDirector] winSize];
  CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png" 
    rect:CGRectMake(0, 0, 20, 20)];
  projectile.position = ccp(20, winSize.height/2);
 
  // Определяем смещение
  int offX = location.x - projectile.position.x;
  int offY = location.y - projectile.position.y;
 
  // Убеждаемся, что не стреляем назад
  if (0 >= offX)return;
 
// Можно добавлять звёздочку
  [self addChild:projectile];
 
  // Определяем направление стрельбы
  int realX = winSize.width + (projectile.contentSize.width/2);
  float ratio = (float) offY / (float) offX;
  int realY = (realX * ratio) + projectile.position.y;
  CGPoint realDest = ccp(realX, realY);
 
  // Определяем дистанцию стрельбы
  int offRealX = realX - projectile.position.x;
  int offRealY = realY - projectile.position.y;
  float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
  float velocity = 480/1; // 480pixels/1sec
  float realMoveDuration = length/velocity;
 
  // Передвигаем звёздочку в конечную точку
  [projectile runAction:[CCSequence actions:
    [CCMoveTo actionWithDuration:realMoveDuration position:realDest],
    [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)],
    nil]];

Сначала мы выбираем одно из касаний, с которым мы будем работать, получаем его позицию на экране, затем вызываем convertToGL, чтобы преобразовать координаты точки касания согласно нашей текущей системе координат. Это необходимо сделать, поскольку мы работаем в ландшафтном режиме.

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

Следует отметить, что алгоритм не совершенен. Наш снаряд продолжает двигаться до тех пор пока не пересечёт горизонтальную границу экрана, даже если уже пересёк вертикальную! Есть много способов сделать это иначе, например находить кратчайшую дистанцию до края экрана, или в методе gameLogic проверять есть ли звёздочки за пределами экрана и убирать их, но для данного туториала оставим всё как есть.

Последнее, что осталось сделать – определить длительность движения звёздочки. Мы хотим, чтобы она двигалась с постоянной скоростью, независимо от направления движения, так что нам снова нужно немного позаниматься математикой. Мы можем определить дистанцию движения с помощью Теоремы Пифагора. Как вы помните из геометрии, эта теорема состоит в том, что длина гипотенузы треугольника равна корню квадратному из суммы квадратов катетов.

Получив расстояние, мы делим его на скорость и получаем время движения. Поскольку скорость равна расстоянию делённому на время, то время, соответственно, равно расстоянию делённому на скорость.

На последок задаём необходимые действия, в точности как мы сделали это для целей. Запускайте проект, и теперь ваш ниндзя должен уметь пулять звёздочки по наступающим врагам.

Как сделать простую игру для iPhone с помощью Cocos2D

Уничтожение целей

Теперь наши звёздочки летают вовсю, но что действительно нужно нашему ниндзя – так это мочить врагов. Давайте добавим немного кода, с помощью которого будем определять пересечение звёздочки и цели.

Можно по разному реализовать это с помощью Cocos2D, например можно использовать физические библиотеки Box2D или Chipmunk. Но мы снова обойдёмся без лишних осложнений и реализуем уничтожение целей самостоятельно.

Чтобы выполнить это, нам, в первую очередь, надо тщательно отслеживать движение целей и звёздочек по сцене. Добавьте следующее в объявление класса HelloWorldLayer:

NSMutableArray *_targets;
NSMutableArray *_projectiles;

И инициализируйте эти массивы в методе init:

_targets = [[NSMutableArray alloc] init];
_projectiles = [[NSMutableArray alloc] init];

И пока мы об этом не забыли, надо освободить память в методе dealloc:

[_targets release];
_targets = nil;
[_projectiles release];
_projectiles = nil;

Сейчас модифицируйте метод addTarget таким образом, чтобы добавлять новую цель в массив целей и задавать им теги для использования в будущем:

target.tag = 1;
[_targets addObject:target];

Также модифицируйте метод ccTouchesEnded, чтобы добавлять новые звёздочки в массив звёздочек и также задавать им теги для дальнейшего использования:

projectile.tag = 2;
[_projectiles addObject:projectile];

И, в конце концов, измените метод spriteMoveFinished, чтобы удалять спрайты из соответствующих массивов согласно тегу.

if (sprite.tag == 1) { // target
  [_targets removeObject:sprite];
} else if (sprite.tag == 2) { // projectile
  [_projectiles removeObject:sprite];
}

Запустите проект на выполнение, чтобы убедиться, что всё по прежнему работает как положено. Каких-то заметных изменений быть не должно, но теперь у нас всё под контролем и мы можем реализовать обнаружение пересечений звёздочек и целей.

Сейчас необходимо добавить следующий метод в HelloWorldLayer:

- (void)update:(ccTime)dt {
 
  NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];
  for (CCSprite *projectile in _projectiles) {
    CGRect projectileRect = CGRectMake(
      projectile.position.x - (projectile.contentSize.width/2), 
      projectile.position.y - (projectile.contentSize.height/2), 
      projectile.contentSize.width, 
      projectile.contentSize.height);
 
    NSMutableArray *targetsToDelete = [[NSMutableArray alloc] init];
    for (CCSprite *target in _targets) {
      CGRect targetRect = CGRectMake(
        target.position.x - (target.contentSize.width/2), 
        target.position.y - (target.contentSize.height/2), 
        target.contentSize.width, 
        target.contentSize.height);
 
      if (CGRectIntersectsRect(projectileRect, targetRect)) {
        [targetsToDelete addObject:target];				
      }						
    }
 
    for (CCSprite *target in targetsToDelete) {
      [_targets removeObject:target];
      [self removeChild:target cleanup:YES];									
    }
 
    if (targetsToDelete.count > 0) {
      [projectilesToDelete addObject:projectile];
    }
    [targetsToDelete release];
  }
 
  for (CCSprite *projectile in projectilesToDelete) {
    [_projectiles removeObject:projectile];
    [self removeChild:projectile cleanup:YES];
  }
  [projectilesToDelete release];
}

Код достаточно простой. Мы проходим в цикле по нашим звёздочкам, создавая прямоугольники, соответствующие их рамкам и используем CGRectIntersectsRect для проверки на пересечение. Если пересечение найдено, мы удаляем спрайты со сцены и из массивов. Обратите внимание, что мы должны добавлять объекты в специальные массивы (targetsToDelete и projectilesToDelete), потому что невозможно удалять элементы массива при прохождении по нему в цикле. Опять-таки, существуют более оптимальные способы для осуществления такого рода вещей, но я буду придерживаться простого подхода.

Для полной готовности не хватает одного – надо сделать так, чтобы этот метод вызывался как можно чаще. Добавьте следующую строчку в инит:

[self schedule:@selector(update:)];

Откомпилируйте проект, запустите его и теперь если звёздочка и цель пересекаются – они должны исчезать!

Последние детали

Теперь давайте создадим ещё одну сцену, которая будет появляться по окончании игры. Кликните на папке Classes, выберите FileNew File, затем выберите Objective-C класс и убедитесь что выбрано наследование этого класса от NSObject. Нажмите Next, введите GameOverScene в качестве имени файла и убедитесь, что напротив “Also create GameOverScene.h” стоит галочка.

Теперь замените содержимое GameOverScene.h следующим кодом:

#import "cocos2d.h"
 
@interface GameOverLayer : CCLayerColor {
  CCLabelTTF *_label;
}
@property (nonatomic, retain) CCLabelTTF *label;
@end
 
@interface GameOverScene : CCScene {
  GameOverLayer *_layer;
}
@property (nonatomic, retain) GameOverLayer *layer;
@end

А содержимое GameOverScene.m таким вот кодом:

#import "GameOverScene.h"
#import "HelloWorldLayer.h"
 
@implementation GameOverScene
@synthesize layer = _layer;
 
- (id)init {
 
  if ((self = [super init])) {
    self.layer = [GameOverLayer node];
    [self addChild:_layer];
  }
  return self;
}
 
- (void)dealloc {
  [_layer release];
  _layer = nil;
  [super dealloc];
}
 
@end
 
@implementation GameOverLayer
@synthesize label = _label;
 
-(id) init
{
  if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
 
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    self.label = [CCLabelTTF labelWithString:@"" fontName:@"Arial" fontSize:32];
    _label.color = ccc3(0,0,0);
    _label.position = ccp(winSize.width/2, winSize.height/2);
    [self addChild:_label];
 
    [self runAction:[CCSequence actions:
      [CCDelayTime actionWithDuration:3],
      [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)],
      nil]];
 
  }	
  return self;
}
 
- (void)gameOverDone {
 
  [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
 
}
 
- (void)dealloc {
  [_label release];
  _label = nil;
  [super dealloc];
}
 
@end

Обратите внимание на присутствие двух разных классов: сцены и слоя. Сцена может содержать любое количество слоёв, хотя в данном случае содержит только один. Весь функционал слоя состоит в демонстрации посреди экрана метки в течении 3-х секунд, а затем возврату к сцене Hello World.

И наконец, давайте добавим немного крайне простой игровой логики. Давайте будем отслеживать звёздочки, которые уничтожает игрок. Добавьте переменную в HelloWorldLayer класс в HelloWorldLayer.h:

int _projectilesDestroyed;

Внутри HelloWorldLayer.m импортируйте класс GameOverScene

#import "GameOverScene.h

Увеличивайте счётчик и проверяйте условие победы в методе update внутри цикла targetsToDelete сразу же после removeChild:target

_projectilesDestroyed++;
if (_projectilesDestroyed > 30) {
  GameOverScene *gameOverScene = [GameOverScene node];
  _projectilesDestroyed = 0;
  [gameOverScene.layer.label setString:@"You Win!"];
  [[CCDirector sharedDirector] replaceScene:gameOverScene];
}

И вдобавок давайте сделаем так, что если хотя бы один враг достигает противоположного края – вы проиграли. Модифицируйте метод spriteMoveFinished, добавив следующий код внутрь условия tag == 1 сразу после removeChild:sprite:

GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];

Запустите игру и теперь вы можете выигрывать, проигрывать и видеть соответствующую сцену окончания игры.

Автор: maycro_ice


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka-pod-ios/15258