Как стать автором
Обновить

Пишем игру-клон Super Mario Brothers (часть 2)

Время на прочтение11 мин
Количество просмотров27K
Автор оригинала: Jacob Gundersen
imageДобро пожаловать во вторую часть из серии туториалов о том, как написать собственный платформер по типу Super Mario Brothers!
В первой части мы написали простой физический движок на основе Tiled Map.
Во второй (и последней) части мы научим Коалио двигаться и прыгать — самая веселая часть любого платформера!

Мы научимся отслеживать столкновения с опасностями на уровне, обрабатывать победу и поражение; добавим великолепные звуковые эффекты и музыку!

Вторая часть на порядок легче (и короче) первой — небольшой отдых после тяжелой работы в прошлый раз! Так что включайте свое кодо-кунг-фу и наслаждайтесь!

Двигаем Коалио


Движения у нас будут очень простыми: только бег вперед и прыжки — прямо как в 1-bit Ninja. Если игрок касается левой части экрана, Коалио бежит вперед. Если игрок касается правой части экрана, Коалио прыгает.
Вы все поняли правильно: Коалио не может двигаться назад! Настоящие Коалы на отступают назад от опасности!
Так как Коалио будет двигаться вперед, нам нужна переменная, которую можно будет использовать в классе Player для обновления позиции Коалы. Добавьте следующее в класс Player:

В Player.h:

@property (nonatomic, assign) BOOL forwardMarch;
@property (nonatomic, assign) BOOL mightAsWellJump;

И синтезируем все в Player.m:

@synthesize forwardMarch = _forwardMarch, mightAsWellJump = _mightAsWellJump;

Теперь добавьте следующие методы обработки касаний в GameLevelLayer:

Нажми меня
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  
  for (UITouch *t in touches) {
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
    if (touchLocation.x > 240) {
      player.mightAsWellJump = YES;
    } else {
      player.forwardMarch = YES;
    }
  }
}
 
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
 
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
 
    //получаем предыдущее касание
    CGPoint previousTouchLocation = [t previousLocationInView:[t view]];
    CGSize screenSize = [[CCDirector sharedDirector] winSize];
    previousTouchLocation = ccp(previousTouchLocation.x, screenSize.height - previousTouchLocation.y);
 
    if (touchLocation.x > 240 && previousTouchLocation.x <= 240) {
      player.forwardMarch = NO;
      player.mightAsWellJump = YES;
    } else if (previousTouchLocation.x > 240 && touchLocation.x <=240) {
      player.forwardMarch = YES;
      player.mightAsWellJump = NO;
    }
  }
}
 
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
    if (touchLocation.x < 240) {
      player.forwardMarch = NO;
    } else {
      player.mightAsWellJump = NO;
    }
  }
}

Код выше довольно прост для понимания. Если X-координата касания экрана меньше 240 (половины экрана), то мы присваиваем переменной forwardMarch значение YES. Иначе (если Х-координата больше 240) присваиваем значение YES переменной mightAsWellJump.
touchesMoved чуть более сложный метод. Нам нужно менять значение переменных только тогда, когда касание пересекает середину экрана. Так что мы должны учитывать еще и предыдущее касание — переменную previousTouch. Иначе говоря, мы просто проверяем с какой стороны произошло пересечение и меняем значения булевых переменных соответственно.
Мы хотим, чтобы, когда игрок переставал касаться определенной области экрана, выключались только нужные булевы переменные.
Однако стоит изменить еще несколько вещей для правильной обработки касаний. Добавьте следующую строку в метод init:

	self.isTouchEnabled = YES;

Нам нужно включить мультитач в AppDelegate.m (вдруг наш игрок захочет прыгать и двигаться вперед одновременно). Добавьте следующее перед строкой [director_ pushScene: [GameLevelLayer scene]];:

	[glView setMultipleTouchEnabled:YES];

Теперь, когда у нас есть значение переменных в объекте класса Player, мы можем добавить немного кода в метод update, чтобы Коалио смог двигаться. Начнем с движения вперед. Измените метод update в Player.m на следующее:

Нажми меня
-(void)update:(ccTime)dt {
    CGPoint gravity = ccp(0.0, -450.0);
    CGPoint gravityStep = ccpMult(gravity, dt);
 
    CGPoint forwardMove = ccp(800.0, 0.0);
    CGPoint forwardStep = ccpMult(forwardMove, dt); //1
 
    self.velocity = ccpAdd(self.velocity, gravityStep);
    self.velocity = ccp(self.velocity.x * 0.90, self.velocity.y); //2
 
    if (self.forwardMarch) {
        self.velocity = ccpAdd(self.velocity, forwardStep);
    } //3
 
    CGPoint minMovement = ccp(0.0, -450.0);
    CGPoint maxMovement = ccp(120.0, 250.0);
    self.velocity = ccpClamp(self.velocity, minMovement, maxMovement); //4
 
    CGPoint stepVelocity = ccpMult(self.velocity, dt);
 
    self.desiredPosition = ccpAdd(self.position, stepVelocity);
}

Давайте разберем этот код шаг за шагом:

  1. Мы добавляем силу forwardMove, которая применяется, когда игрок касается экрана. Напомню, что мы изменяем это силу (800 точек в секунду) в зависимости от скорости кадров в секунду, чтобы получить постоянное ускорение.
  2. Здесь мы добавляем трение. Мы используем физику так же, как мы делали с гравитацией. Силы будут применяться каждый кадр. Когда сила исчерпывает себя, мы хотим, чтобы игрок остановился. Мы применяем трение силой 0.90. Другими словами, мы уменьшаем силу движения на 10% каждый кадр.
  3. В третьей секции мы проверяем, касается ли игрок экрана и добавляем, если нужно, силу forwardStep.
  4. В секции четыре мы устанавливаем ограничения. Ограничиваем максимальную скорость игрока. Как горизонтальную (скорость бега), так и вертикальную (скорость прыжка, скорость гравитации).
    Сила трения и ограничения позволяют нам контролировать скорость процессов в игре. Наличие этих ограничений предотвращает проблему со слишком высокой скоростью из первого туториала.
    Мы хотим, чтобы игрок достигал максимальной скорости за секунду или меньше. Так, движения Коалы все еще будут выглядеть натурально и, в то же время, поддаваться контролю. Мы установим ограничение в 120 (четверть ширины экрана в секунду) для максимальной скорости.
    Если мы хотим увеличить планку ускорения нашего игрока, мы должны увеличить обе величины соответственно: forwardMove и силу трения (0.90). Если мы хотим увеличить максимальную скорость игрока, нам нужно просто увеличить величину 120. Так же мы ограничиваем скорость прыжка до 250 и скорость падения до 450.

Запустите проект. Коалио должен начать движение, если вы нажмете на левую часть экрана. Смотрите, как резво двигается наша Коала!

image

Далее мы заставим Коалу прыгать!

Наш мак заставит ее… Прыгать, прыгать!


Прыжки это неотъемлемая часть любого платформера, которая приносит довольно много веселья. Мы должны удостовериться, что прыжок плавен и четко откалиброван. В этом туториале мы используем алгоритм прыжка из игры Sonic the Hedgehog, описанный здесь.
Добавим следующее в метод update перед строкой if (self.forwardMarch) { :

CGPoint jumpForce = ccp(0.0, 310.0);
 
if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
}

Если мы остановимся прямо сейчас и запустим проект, то мы получим классические прыжки в стиле Atari. Все прыжки будут одинаковой высоты. Мы применяем силу к Коале и ждем, пока гравитация ее преодолеет.
В современных платформерах у пользователей гораздо больше контроля над прыжками. Мы хотим контролируемые, абсолютно нереалистичные (но чертовски веселые) прыжки в стиле Mario Bros или Sonic.
Есть несколько способов добиться нашей цели, но мы возьмем за основу прыжки Sonic'а. Когда игрок перестает касаться экрана, сила прыжка начинает уменьшаться. Заменим код выше на следующее:

  CGPoint jumpForce = ccp(0.0, 310.0);
  float jumpCutoff = 150.0;
 
  if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
  } else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
    self.velocity = ccp(self.velocity.x, jumpCutoff);
  }

Этот код осуществляет еще один дополнительный шаг. Когда игрок перестает касаться экрана (значение self.mightAsWellJump становится NO), игра проверяет скорость Коалио, направленную вверх. Если эта величина больше, чем наш порог, то игра устанавливает значение порога переменной скорости.
Таким образом, при мгновенном касании экрана, Коала не прыгнет выше jumpCutoff, но при продолжительном нажатии на экран, Коала будет прыгать все выше и выше.
Запустите проект. Игра начинает походить на что-то стоящее! Сейчас вам, скорее всего, нужно протестировать приложение на реальном девайсе (если вы еще этого не сделали) для проверки работы обеих «кнопок».

image

Мы заставили Коалио бегать и прыгать, но он довольно быстро убегает за края экрана. Пришло время это починить!
Добавьте следующий сниппет из туториала по ячеечным картам в GameLevelLayer.m:

Нажми меня
- (void)setViewpointCenter:(CGPoint) position {
 
  CGSize winSize = [[CCDirector sharedDirector] winSize];
 
  int x = MAX(position.x, winSize.width / 2);
  int y = MAX(position.y, winSize.height / 2);
  x = MIN(x, (map.mapSize.width * map.tileSize.width) 
      - winSize.width / 2);
  y = MIN(y, (map.mapSize.height * map.tileSize.height) 
      - winSize.height/2);
  CGPoint actualPosition = ccp(x, y);
 
  CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
  CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
  map.position = viewPoint; 
}

Этот код закрепляет экран за позицией игрока. В случае, если Коалио дойдет до конца уровня, экран перестает центроваться на Коале и закрепляет грань уровня за гранью экрана.
Стоит обратить внимание, что в последней строке мы двигаем карту, а не игрока. Это возможно потому, что игрок — это потомок карты. Так что, когда игрок двигается вправо, карта движется влево и игрок остается в центре экрана.
Для более подробного объяснения, посетите этот туториал.
Нам нужно добавить следующий вызов в метод update:

	[self setViewpointCenter:player.position];

Запустите проект. Коалио может пробежать через весь уровень!

image

Агония поражения


Исхода игры два: либо победа, либо поражение.
Сначала разберемся со сценарием поражения. На нашем уровне есть опасности (hazards). Если игрок сталкивается с опасностью, игра заканчивается.
Так как опасности — это фиксированные ячейки, мы будем работать с ними, как с препятствиями из прошлого туториала. Однако вместо определения столкновений мы будем просто завершать игру. Мы на пути к успеху — осталось совсем чуть-чуть!
Добавьте следующий метод в GameLevelLayer.m:

- (void)handleHazardCollisions:(Player *)p {
  NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:hazards ];
  for (NSDictionary *dic in tiles) {
    CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height);
    CGRect pRect = [p collisionBoundingBox];
 
    if ([[dic objectForKey:@"gid"] intValue] && CGRectIntersectsRect(pRect, tileRect)) {
      [self gameOver:0];
    }
  }
}

Весь код выше должен быть вам знаком, так как он просто скопирован из метода checkAndResolveCollisions. Единственное нововведение — это метод gameOver. Мы посылаем ему 0, если игрок проиграл, и 1, если он победил.
Мы используем слой hazards, вместо слоя walls, так что нам нужно объявить переменную CCTMXLayer *hazards; в @ interface в начале исполнительного файла. Добавьте это в метод init (сразу после объявления переменной для слоя walls):

	hazards = [map layerNamed:@"hazards"];

Единственная вещь, которую осталось сделать — это добавить следующий код в метод update:

- (void)update:(ccTime)dt {
  [player update:dt];
 
  [self handleHazardCollisions:player];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

Теперь, если игрок влетит в любую опасную ячейку со слоя hazards, мы вызовем gameOver. Этот метод просто отобразит кнопку «Restart» с сообщением о том, что вы проиграли (или выиграли — такое тоже случается):

Нажми меня
- (void)gameOver:(BOOL)won {
	gameOver = YES;
	NSString *gameText;
 
	if (won) {
		gameText = @"You Won!";
	} else {
		gameText = @"You have Died!";
	}
 
  CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:gameText fontName:@"Marker Felt" fontSize:40];
  diedLabel.position = ccp(240, 200);
  CCMoveBy *slideIn = [[CCMoveBy alloc] initWithDuration:1.0 position:ccp(0, 250)];
  CCMenuItemImage *replay = [[CCMenuItemImage alloc] initWithNormalImage:@"replay.png" selectedImage:@"replay.png" disabledImage:@"replay.png" block:^(id sender) {
    [[CCDirector sharedDirector] replaceScene:[GameLevelLayer scene]];
  }];
 
  NSArray *menuItems = [NSArray arrayWithObject:replay];
  CCMenu *menu = [[CCMenu alloc] initWithArray:menuItems];
  menu.position = ccp(240, -100);
 
  [self addChild:menu];
  [self addChild:diedLabel];
 
  [menu runAction:slideIn];
}

Первая строка объявляет переменную gameOver. Мы будем использовать эту переменную для того, чтобы заставить метод update перестать осуществлять какие-либо изменения в позиции Коалио. Буквально через минутку мы это и осуществим.
Далее код создает текст (label) и придает ему значение won или lost. Также мы создаем кнопку «Restart».
Эти основанные на блоках методы объектов CCMenu довольно удобно использовать. В нашем случае мы просто заменяем наш уровень на его копию для того, чтобы начать уровень сначала. Мы используем CCAction и CCMove просто для того, чтобы красиво анимировать кнопочки.
Еще одна вещь, которую нужно сделать — это добавить булеву переменную gameOver в класс GameLevelLayer. Это будет локальная переменная, так как мы не будем ее использовать вне класса. Добавьте следующее в @ interface в начале файла GameLevelLayer.m:

CCTMXLayer *hazards;
BOOL gameOver;

Измените метод update следующим образом:

- (void)update:(ccTime)dt {
  if (gameOver) {
    return;
  }
  [player update:dt];
  [self checkForAndResolveCollisions:player];
  [self handleHazardCollisions:player];
  [self setViewpointCenter:player.position];
}

Запустите игру, найдите шипованный пол и прыгнете на него! Вы должны увидеть нечто подобное:

image

Только не надо повторять это слишком часто, а то могут появиться проблемы с защитниками животных! :]

Пропасть смерти


Теперь добавим сценарий падения в дырку между ячейками. В этом случае, мы просто завершаем игру.
Прямо сейчас при падении код скрашится с ошибкой «TMXLayer: invalid position». (Ужас!) Вот здесь нам и нужно вмешаться.
Добавьте следующий код в GameLevelLayer.m, в метод getSurroundingTilesAtPosition:, перед строкой tileGIDat:

if (tilePos.y > (map.mapSize.height - 1)) {
    //fallen in a hole
    [self gameOver:0];
    return nil;
}

Этот код запустит действие gameOver и предотвратит процесс создания массива ячеек. Нам также нужно предотвратить процесс прохода цикла через все ячейки в checkForAndResolveCollisions. Добавьте блок кода после строки NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ];:

  if (gameOver) {
    return;
  }

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

image

Победа!


Теперь, поработаем со случаем, когда наш Коалио выигрывает игру!

image

Все, что мы делаем — это следим за x-позицией нашей Коалы и показываем экран с победным сообщением, если она пересекает определенную метку. Длина этого уровня 3400 пикселов. Мы поставим победную метку на расстоянии 3130 пикселей.
Добавьте новый метод в GameLevelLayer.m и обновите метод update:

- (void)checkForWin {
  if (player.position.x > 3130.0) {
    [self gameOver:1];
  }
}

- (void)update:(ccTime)dt {
  [player update:dt];
 
  [self handleHazardCollisions:player];
  [self checkForWin];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

Запустите, попытайтесь выиграть. Если получится, то вы должны увидеть следующее:

image

Великолепная музыка и звуковые эффекты


Пришло время для великолепной музыки и звуковых эффектов!
Давайте начнем. Добавьте следующее в начало GameLevelLayer.m и Player.m:

#import "SimpleAudioEngine.h"

Теперь добавьте следующую строку в метод init файла GameLevelLayer.

	[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"level1.mp3"];

Мы только что подключили приятную игровую музыку. Спасибо Кевину из Incompetech.com за то, что он написал музыку (Brittle Reel). У него там целая туча лицензированной для CC музыки!
Теперь добавим звук прыжка. Перейдите в Player.m и добавьте следующее в код прыжка метода update:

if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
    [[SimpleAudioEngine sharedEngine] playEffect:@"jump.wav"];
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
    self.velocity = ccp(self.velocity.x, jumpCutoff);
}

И наконец, проиграем звук поражения, когда Коалио дотрагивается до опасной ячейки или падает в яму. Добавьте в метод gameOver файла GameLevelLayer.m следующее:

- (void)gameOver {
  gameOver = YES;
  [[SimpleAudioEngine sharedEngine] playEffect:@"hurt.wav"];  
  CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:@"You have Died!" fontName:@"Marker Felt" fontSize:40];
  diedLabel.position = ccp(240, 200);

Запустите и насладитесь новыми приятными мелодиями.
Вот и все! Вы написали платформер! Вы великолепны!

А вот и исходный код нашего законченного проекта.


Примечание переводчика

Решил время от времени переводить туториалы с сайта raywenderlich.com.
Слeдующая моя цель — перевод серии туториалов о том, как создать собственную игру наподобии Fruit Ninja на основе Box2D и Cocos2D.
Напишите в комментариях, стоит ли. Если перевод будет востребован, переведу.
Обо всех найденных неточностях и опечатках, пожалуйста, пишите в хабрапочте или тут в комментариях.
С радостью отвечу на все вопросы по туториалу!
Теги:
Хабы:
+40
Комментарии6

Публикации

Изменить настройки темы

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн