Как написать игру марио

Время на прочтение
23 мин

Количество просмотров 87K

imageДля многих из нас Super Mario Brothers была первой игрой, которая по-настоящему завораживала своим игровым процессом.
Интуитивное управление SMB и великолепный дизайн уровней от Nintendo заставляли проводить часы напролет в виртуальной вселенной сантехника и его напарника.

В этом чудесном туториале от Джейкоба Гандерсена мы создадим собственный платформер; но, так как главным героем будет Коала, мы назовем нашу игру «Super Koalio Brothers!» ;]
Также, чтобы упростить механику, мы забудем о движущихся врагах. Вместо них мы будем использовать шипованные блоки, встроенные в пол. Это позволит нам полностью сконцентрироваться на сердце платформера — физическом движке.

Внимание! Под катом невероятное количество переведенного текста, картинок, кода (код не переведен) и руководство по созданию собственного физического движка!

Этот туториал заранее подразумевает, что Вы знакомы с основами программирования на Cocos2D. Иначе, я настоятельно рекомендую сначала ознакомиться с парой-тройкой начальных уроков на сайте Рея.

Начнем

Для начала скачайте стартовый проект для этого туториала. Распакуйте его, откройте в Xcode, запустите. На экране эмулятора должно появится нечто подобное:

image

Все правильно — просто скучный пустой экран! :] Мы полностью его заполним по мере прохождения туториала
В стартовый проект уже добавлены все необходимые картинки и звуки. Пробежимся по содержимому проекта:

  • Гейм арт. Включает в себя бесплатный пакет гейм артов от жены Рея Вики.
  • Карта уровня. Я нарисовал карту уровня специально для вас, отталкиваясь от первого уровня в SMB.
  • Великолепные звуковые эффекты. Как-никак, туториал с raywenderlich.com! :]
  • Подкласс CCLayer. Класс с именем GameLevelLayer, который реализовывает большую часть нашего физического движка. Хотя сейчас он пуст как пробка. (Да, эта детка так и ждет, чтобы ее заполнили!)
  • Подкласс CCSprite. Класс с именем Player, который содержит логику Коалы. Прямо сейчас наша Коала так и норовит улететь вдаль!

Основы физических движков

Платформеры оперирут на основе физических движков, и в этом туториале мы напишем собственный физический движок.
Есть две причины, по которым нам нужно написать собственный движок, а не брать те же Box2D или Chipmink:

  1. Детальная настройка. Чтобы полностью познать дзен платформеров, вам нужно научиться полностью настраивать свой движок.
  2. Простота. В Box2D и Chipmunk есть множество настраиваемых функций, которые нам, по большому счету, не пригодятся. Да еще и ресурсы есть будут. А наш собственный движок будет есть ровно столько, сколько мы ему позволим.

Физический движок выполняет две главные задачи:
image

  1. Симулирует движение. Первая задача физического движка — симулировать противодействующие силы гравитации, передвижения, прыжков и трения.
  2. Определяет столкновения. Вторая задача — определять столкновения между игроком и другими объектами на уровне.

Например, во время прыжка на нашу Коалу действует сила, направленная вверх. Спустя некоторое время, сила гравитации превосходит силу прыжка, что дает нам классическое параболическое изменение скорости.
При помощи определения столкновений, мы будем останавливать нашу Коалу каждый раз, когда она захочет пройти сквозь пол под действием гравитации, и определять, когда наша Коала наступила на шипы (ай!).
Давайте посмотрим, как это работает на практике.

Создание физического движка

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

  1. Выбрано ли действие прыжка или движения?
  2. Если да, применить силу прыжка или движения на Коалу.
  3. Также, применить силу гравитации на Коалу.
  4. Вычислить полученную скорость Коалы.
  5. Применить полученную скорость на Коалу и обновить ее позицию.
  6. Проверить на предмет столкновений Коалы с другими объектами.
  7. Если произошло столкновение, то либо сдвинуть Коалу на такое расстояние от препятствия, что столкновений больше не происходит; либо нанести урон бедной Коале.

image
Мы будем проходить через эти действия каждый шаг программы. В нашей игре гравитация постоянно заставляет Коалу опускаться все ниже и ниже сквозь пол, но определение столкновений каждый раз возвращает ее обратно на точку над полом. Так же можно использовать эту особенность для того, чтобы определять, касается ли Коала земли. Если нет, то можно запретить игроку прыгать, когда Коала находится в состоянии прыжка или только что спрыгнула с какого-либо препятствия.
Пункты 1-5 происходят внутри объекта Коалы. Вся необходимая информация должна храниться внутри этого объекта и довольно логично разрешить Коале самой обновлять ее переменные.
Однако когда дело доходит до 6го пункта — определения столкновений — нам нужно принимать во внимание все особенности уровня, такие как: стены, пол, враги и другие опасности. Определение столкновений будет осуществляться каждый шаг программы при помощи GameLevelLayer — напомню, это подкласс CCLayer, который будет осуществлять большинство физических задач.
Если мы разрешим Коале обновлять ее позицию собственноручно, то в конце концов Коала коснется стены или пола. А GameLevelLayer вернет Коалу назад. И так вновь и вновь — что заставит Коалу выглядеть, как будто она вибрирует. (Слишком много кофе с утра, Коалио?)
И так, мы не будем разрешать Коале обновлять свое состояние. Вместо этого, мы добавим Коале новую переменную desiredPosition, которую Коала и будет обновлять. GameLevelLayer будет проверять можно ли переместить Коалу в точку desiredPosition. Если да, то GameLevelLayer обновит состояние Коалы.
Все ясно? Давайте посмотрим, как это выглядет в коде!

Загрузка TMXTiledMap

Я предполагаю, что вы знакомы, как работают карты типа Tile Maps. Если нет, то я советую прочесть о них в этом туториале.
Давайте взглянем на уровень. Запустите ваш Tiled map editor (загрузите, если вы не сделали этого раньше) и откройте level1.tmx из папки вашего проекта. Вы увидите следующее:

image

Если вы посмотрите на боковую панель, вы увидите, что у нас есть три разных слоя:

  • hazards: Этот слой содержит вещи, которых Коала должна остерегаться, чтобы остаться вживых.
  • walls: Этот слой содержит ячейки, через сквозь которые Коала не может пройти. В основном это ячейки пола.
  • background: Этот слой содержит исключительно эстетические вещи, такие как облака или холмики.

Пришло время кодить! Откройте GameLevelLayer.m и добавьте следующее после #import, но перед @implementation:

@interface GameLevelLayer()  {
  CCTMXTiledMap *map;
}
 
@end

Мы добавили локальную переменную map класса CCTMXTiledMap для работы с ячеистыми картами в наш головной класс.
Далее мы поместим ячеистую карту на наш слой прямо во время инициализации слоя. Добавим следующее в метод init:

CCLayerColor *blueSky = [[CCLayerColor alloc] initWithColor:ccc4(100, 100, 250, 255)];
[self addChild:blueSky];
 
map = [[CCTMXTiledMap alloc] initWithTMXFile:@"level1.tmx"];
[self addChild:map];

Во-первых, мы добавили задник (CCLayerColor) цвета синего неба. Следующие две строки кода это просто подгрузка переменной map (CCTMXTiledMap) и добавление ее на слой.

Далее, в GameLevelLayer.m импортируем Player.h:

#import "Player.h"

Все еще в GameLevelLayer.m добавим следующую локальную переменную в секцию @ interface:

Player * player;

Далее добавим Коалу на уровень следующим кодом в методе init:

player = [[Player alloc] initWithFile:@"koalio_stand.png"];
player.position = ccp(100, 50);
[map addChild:player z:15];

Этот код загружает спрайт-объект Коалы, задает ему позицию и добавляет его на объект нашей карты.
Вы спросите, зачем добавлять объект коалы на карту, вместо того, чтобы просто добавить его напрямую на слой? Все просто. Мы хотим непосредственно контролировать какой слой будет перед Коалой, а какой за ней. Так что мы делаем Коалу ребенком карты, а не главного слоя. Мы хотим, чтобы Коала была спереди, так что даем ей Z-порядок, равный 15. Так же, когда мы прокручиваем карту, Коала все еще находится на той же позиции, относительно карты, а не главного слоя.
Отлично, давайте попробуем! Запустите ваш проект и вы должны увидеть следующее:

image

Выглядит как игра, но Коалио игнорирует гравитацию! Пришло время опустить его с небес на землю — при помощи физического движка :]

Ситуация с гравитацией Коалио

image
Чтобы создать симуляцию физики, можно написать сложный набор разветвляющейся логики, который бы учитывал состояние Коалы и применял бы к ней силы, отталкиваясь от полученной информации. Но этот мир сразу станет слишком сложным — a реальная физика ведь так сложно не работает. В реальном мире гравитация просто постоянно тянет объекты вниз. Так, мы добавляем постоянную силу гравитации и применяем ее к Коале каждый шаг программы.
Другие силы тоже не просто отключаются и включаются. В реальном мире сила действуюет на объект пока другая сила не превзойдет или не будет равной первой.
Например, сила прыжка не отключает гравитацию; она какое-то время превосходит силу гравитации, до тех пор, пока гравитация вновь не прижмет Коалу к земле.
Вот так моделируется физика. Вы не просто решаете, применять или не применять гравитационную силу к Коале. Гравитация существует всегда.

Играем в бога

image
В логике нашего движка заложено, что если на объект действует сила, то он будет продолжать двигаться пока другая сила на превзойдет первую. Когда Коалио спрыгивает с уступа, он продолжает двигаться вниз с определенным ускорением, пока не встретит препятствие на своем пути. Когда мы двигаем Коалио, он не перестанет двигаться, пока мы не перестанем применять на него силу движения; трение будет действовать на Коалио, пока тот не остановится.
По мере создания физического движка вы увидите, как настолько простая игровая логика помогает решать сложные физические задачи, такие как: ледяной пол или падение со скалы. Эта поведенческая модель позволяет игре изменяться динамически.
Так же такой ход конем позволит нам сделать имплементацию проще, так как нам не нужно постоянно спрашивать состояние нашего объекта — объект просто будет следовать законам физики из реального мира.
Иногда нам нужно играть в бога! :]

Законы планеты Земля: CGPoint’ы и Силы

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

  • Скорость описывает, насколько быстро объект движется в определенном направлении.
  • Ускорение описывает, как скорость и направление объекта изменяются со временем.
  • Сила — это влияние, которое является причиной изменения в скорости или направлении.

В физической симуляции, примененная к объекту сила ускорит объект до определенной скорости, и объект будет двигаться с этой скоростью, пока не встретит на пути другую силу. Скорость — это величина, которая изменяется от одного кадра к следующему по мере появления новых действующих сил.
Мы будем представлять три вещи при помощи структур CGPoint: скорость, сила/ускорение и позиция. Есть две причины использования CGPoint структур:

  1. Они 2D. Скорость, сила/ускорение и позиция — всё это 2D величины для 2D игры. Можете заявить, что гравитация действует только в одном направлении, но что, если в один из моментов игры нам срочно нужно будет сменить направление гравитации? Подумайте о Super Mario Galaxy!
  2. Это удобно. Используя CGPoint, мы можем пользоваться различными функциями, встроенными в Cocos2D. В частности мы будем использовать ccpAdd (сложение), ccpSub (вычитание) и ccpMult (умножение на переменную типа float). Все это сделает наш код гораздо более удобным для чтения и отладки!

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

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

Давайте начнем с гравитацией. Напишем цикл run, в котором мы будем применять силы. Добавьте в метод init файла GameLevelLayer.m следующий код прямо перед закрытием условного блока if:

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

Далее добавьте новый метод в класс:

- (void)update:(ccTime)dt {
    [player update:dt];
}

Далее откройте Player.h и измените его, чтобы он выглядил так:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
 
@interface Player : CCSprite 
 
@property (nonatomic, assign) CGPoint velocity;
 
- (void)update:(ccTime)dt;
 
@end

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

Нажми меня

#import "Player.h"
 
@implementation Player
 
@synthesize velocity = _velocity;
 
// 1
- (id)initWithFile:(NSString *)filename {

    if (self = [super initWithFile:filename]) {
        self.velocity = ccp(0.0, 0.0);
    }
    return self;
}
 
- (void)update:(ccTime)dt {
 
    // 2
    CGPoint gravity = ccp(0.0, -450.0);
 
    // 3
    CGPoint gravityStep = ccpMult(gravity, dt);
 
    // 4
    self.velocity = ccpAdd(self.velocity, gravityStep);
    CGPoint stepVelocity = ccpMult(self.velocity, dt);
 
    // 5
    self.position = ccpAdd(self.position, stepVelocity);
}
 
@end

Давайте пройдемся по коду выше ступень за ступенью

  1. Здесь мы добавили новый метод init чтобы инициализировать объект и приравнять переменную скорости к нулю.
  2. Здесь мы обозначили значение вектора гравитации. Каждую секунду мы ускоряем скорость Коалы на 450 пикселов вниз.
  3. Здесь мы использовали ccpMult для того, чтобы уменьшить значение гравитационного вектора для удовлетворения скорости смены кадров. ccpMult получает float и CGPoint и возвращает CGPoint.
  4. Здесь, как только мы посчитали гравитацию для текущего шага, мы добавляем ее к текущей скорости.
  5. Наконец, когда мы посчитали скорость для одного шага, мы используем ccpAdd для обновления позиции Коалы.

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

image

Уууууупс — Коалио падает сквозь пол! Давайте это починим.

Удары в ночи – определение столкновений

Определение столкновений это основа любого физического движка. Есть множество различных видов определения столкновений, от простого использования рамок изображений, до комплексных столкновений 3D объектов. К счастью для нас, платформер не требует сложных структур.
Чтобы определять столкновения Коалы с объектами, мы будем использовать TMXTileMap для ячеек, которые непосредственно окружают Коалу. Далее, используя несколько встроенных в iOS функций мы будем проверять пересекает ли спрайт Коалы спрайт какой-либо ячейки.
Функции CGRectIntersectsRect и CGRectIntersection делают такие проверки очень простыми. CGRectIntersectsRect проверяет, пересекаются ли два прямоугольника, а CGRectIntersection возвращает прямоугольник пересечения.
Во-первых, нам нужно определить рамку нашей Коалы. Каждый загруженный спрайт имеет рамку, которая является размером текстуры и к которой можно получить доступ при помощи параметра с именем boundingBox.
Зачем определять рамку, если она уже есть в boundingBox? Текстура обычно имеет вокруг себя прозрачные края, которые мы совсем не хотим учитывать при определении столкновений.
Иногда нам не нужно учитывать даже пару-тройку пикселей вокруг реального изображения спрайта (не прозрачного). Когда марио врезается в стену, разве он чуть-чуть касается ее, или его нос слегка утопает в блоке?
Давайте попробуем. Добавьте в Player.h:

-(CGRect)collisionBoundingBox;

И добавьте в Player.m:

- (CGRect)collisionBoundingBox {
  return CGRectInset(self.boundingBox, 2, 0);
}

CGRectInset сжимает CGRect на количество пикселов из второго и третьего аргументов. В нашем случае, ширина нашей рамки столкновений будет на шесть пикселов меньше — три пиксела с каждой стороны.

Поднятие тяжестей

Пришло время поднимать тяжести. («Эй, ты сейчас назвал меня толстым?» — говорит Коалио).
Нам потребуется ряд методов в нашем GameLevelLayer для определения столкновений. В частности:

  • Метод, возвращающий координаты восьми ячеек, окружающих текущую ячейку Коалио.
  • Метод, определяющий, которая из ячеек является препятствием (и имеются ли таковые в общем). Некоторые ячейки не имеют физических свойств (облака), и Коалио не будет сталкиваться с ними.
  • Метод, обрабатывающий столкновения в приоритетном порядке.

Мы создадим две вспомогательные функции, которые упростят методы, описанные чуть выше.

  • Метод, который определяет позицию ячейки Коалио.
  • Метод, который получает координаты ячейки и возвращает прямоугольник ячейки в Cocos2D координатах.

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

- (CGPoint)tileCoordForPosition:(CGPoint)position {

  float x = floor(position.x / map.tileSize.width);
  float levelHeightInPixels = map.mapSize.height * map.tileSize.height;
  float y = floor((levelHeightInPixels - position.y) / map.tileSize.height);
  return ccp(x, y);
}
 
- (CGRect)tileRectFromTileCoords:(CGPoint)tileCoords {

  float levelHeightInPixels = map.mapSize.height * map.tileSize.height;
  CGPoint origin = ccp(tileCoords.x * map.tileSize.width, levelHeightInPixels - ((tileCoords.y + 1) * map.tileSize.height));
  return CGRectMake(origin.x, origin.y, map.tileSize.width, map.tileSize.height);
}

Первый метод возвращает нам координаты ячейки, находящейся на координатах в пикселях, которые мы передаем в метод. Чтобы получить позицию ячейки, мы просто делим координаты на размер ячеек.
Нам нужно инвертировать координаты высоты, так как координаты системы Cocos2D/OpenGL начинаются с левого нижнего угла, а системные координаты начинаются с левого верхнего угла. Стандарты — ну разве это не круто?
Второй метод делает все наоборот. Он умножает координату ячейки на размер ячеек и вовзращает CGRect данной ячейки. Опять же, нам нужно развернуть высоту.
Зачем нам добавлять единицу к y-координате высоты? Запомните, координаты ячеек начинаются с нуля, так 20 ячейка имеет реальную координату 19. Если мы не добавим единицу к высоте, точка будет 19 * tileHeight.

Я окружен ячейками!

Теперь перейдем к методу, который определяет окружающие Коалу ячейки. В этом методу мы создадим массив, который и будем возвращать. Этот массив будет содержать GID ячейки, координаты ячейки и информацию о CGRect этой ячейки.
Мы организуем этот массив в порядке приоритета, в котором мы будем определять столкновения. Например, мы ведь хотим определять столкновения сверху, слева, справа, снизу перед тем, как определять диагональные. Также, когда мы определим столкновение Коалы с нижней ячейкой мы выставляем флаг касания земли.
Добавим этот метод в GameLevelLayer.m:

Нажми меня

- (NSArray *)getSurroundingTilesAtPosition:(CGPoint)position forLayer:(CCTMXLayer *)layer {
 
  CGPoint plPos = [self tileCoordForPosition:position]; //1
 
  NSMutableArray *gids = [NSMutableArray array]; //2
 
  for (int i = 0; i < 9; i++) { //3
    int c = i % 3;
    int r = (int)(i / 3);
    CGPoint tilePos = ccp(plPos.x + (c - 1), plPos.y + (r - 1));
 
    int tgid = [layer tileGIDAt:tilePos]; //4
 
    CGRect tileRect = [self tileRectFromTileCoords:tilePos]; //5
 
    NSDictionary *tileDict = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNumber numberWithInt:tgid], @"gid",
                 [NSNumber numberWithFloat:tileRect.origin.x], @"x",
                 [NSNumber numberWithFloat:tileRect.origin.y], @"y",
                 [NSValue valueWithCGPoint:tilePos],@"tilePos",
                 nil];
    [gids addObject:tileDict];
 
  }
 
  [gids removeObjectAtIndex:4];
  [gids insertObject:[gids objectAtIndex:2] atIndex:6];
  [gids removeObjectAtIndex:2];
  [gids exchangeObjectAtIndex:4 withObjectAtIndex:6];
  [gids exchangeObjectAtIndex:0 withObjectAtIndex:4]; //6
 
  for (NSDictionary *d in gids) {
    NSLog(@"%@", d);
  } //7
 
  return (NSArray *)gids;
}

Пфф — целая туча кода. Не беспокойтесь, мы детально по нему пройдемся.
Но перед этим заметьте, что у нас есть три слоя на нашей карте.
Наличие разных слоев позволяет нам по-разному определять столкновения для каждого слоя.

  • Koala и hazards. Если произошло столкновение, то мы убиваем Коалу (достаточно брутально, не так ли?).
  • Koala и walls. Если произошло столкновение, то мы не разрешаем Коале дальше двигаться в этом направлении. «Стой, кобыла!»
  • Koala и backgrounds. Если произошло столкновение, то мы ничего и не делаем. Ленивый программист — лучший программист. Ну или как там, в народе, говорят?

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

1. Для начала мы получаем координаты ячейки для ввода (которыми и будут координаты Коалы).
2. Далее, мы создаем новый массив, который будет возвращать информацию о ячейке.
3. Далее, мы запускаем цикл 9 раз — так как у нас есть 9 возможных ячеек перемещения, включая ячейку, в которой коала уже находится. Следующие несколько строк определяют позиции девяти ячеек и сохраняют из в переменной tilePos.

Обратите внимание: нам нужна информация только о восьми ячейках, так как нам никогда не придется определять столкновения с ячейкой, на которой коала уже находится.
Мы всегда должны ловить этот случай и перемещать Коалу в одну из ячеек вокруг. Если Коалио находится внутри твердой ячейки, значит больше половины спрайта Коалио вошло внутрь. Он не должен двигаться так быстро — как минимум, в этой игре!
Чтобы легче оперировать над этими восьми ячейками, просто добавим ячейку Коалио по началу, а в конце ее удалим.

4. В четвертой секции мы вызываем метод tileGIDAt:. Этот метод возвращает GID ячейки на определенной координате. Если на полученных координатах нет ячейки, метод возвращает ноль. Далее мы будем использовать ноль в значении «не найдено ячейки».
5. Далее мы используем вспомогательный метод, чтобы вычислить CGRect для ячейки на данных Cocos2D координатах. Полученную информацию мы сохраняем в NSDictionary. Метод возвращает массив из полученных NSDictionary.
6. В шестой секции мы убираем ячейку Коалы из массива и сортируем ячейки в приоритетном порядке.
image
Часто, в случае определения столкновений с ячейкой под Коалой, мы так же определяем столкновения с ячейками по-диагонали. Смотрите рисунок справа. Определяя столкновения с ячейкой под Коалой, веделенной красным, мы так же определяем столкновения с блоком #2, выделенным синим.
Наш алгоритм определения столкновений будет использовать некоторые допущения. Эти допущения верны скорее для прилегающих, нежели для диагональных ячеек. Так что мы постараемся избегать действий с диагональными ячейками настолько, насколько это возможно.
А вот и картинка, которая наглядно показывает нам порядок ячеек в массиве до и после сортировки. Можно заметить, что верхняя, нижняя, правая и левая ячейки обрабатываются в первую очередь. Зная порядок ячеек, вам будет легче определять, когда Коала касается земли или летает в облаках.
image

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

Мы почти готовы к следующему запуску нашей игры! Однако все еще нужно сделать пару вещиц. Нам нужно добавить слой walls как переменную в класс GameLevelLayer так, чтобы мы смогли ее использовать.

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

// Добавить в @interface
CCTMXLayer *walls;
 
// Добавить в метод init, после того, как на слой добавляется карта
walls = [map layerNamed:@"walls"];
 
// добавить в метод update
[self getSurroundingTilesAtPosition:player.position forLayer:walls];

Запускайте! Но, к сожалению, игра крашится. Мы видим в консоли нечто следующее:

image

Сначала мы получаем информацию о позициях ячеек и значения GID (хотя в основном нули, так как сверху пустая местность).
В конце, все крашится с ошибкой «TMXLayer: invalid position». Такое происходит, когда в метод tileGIDat: передается позиция, которая находится вне краев карты.
Мы избежим этой ошибки чуть позже — но сначала, мы собираемся изменить существующее определение столкновений.

Отбираем привилегии Коалы назад

До этого момента Коала сама обновляла себе позицию. Но сейчас мы забираем у нее эту привилегию.

image

Если Коала будет самостоятельно обновлять свою позицию, то в конце концов она начнет скакать как бешеная! А мы же этого не хотим, нет?
Так что Коала требует дополнительной переменной desiredPosition, при помощи которой она будет взаимодействовать с GameLevelLayer.
Мы хотим, чтобы класс Коалы самостоятельно высчитывал свою следующую позцию. Но GameLevelLayer должен перемещать Коалу в желаемую позицию только после проверки ее на валидность. То же самое применимо и к циклу определения столкновений — мы не хотим обновлять реальный спрайт до того, как все ячейки были проверены на предмет столкновений.
Нам нужно поменять несколько вещей. Сначала, добавьте следующее в Player.h

@property (nonatomic, assign) CGPoint desiredPosition;

И синтезируйте добавленное в Player.m:

@synthesize desiredPosition = _desiredPosition;

Теперь, измените метод collisionBoundingBox в Player.m, чтобы он выглядел так:

- (CGRect)collisionBoundingBox {

  CGRect collisionBox = CGRectInset(self.boundingBox, 3, 0);
  CGPoint diff = ccpSub(self.desiredPosition, self.position);
  CGRect returnBoundingBox = CGRectOffset(collisionBox, diff.x, diff.y);
  return returnBoundingBox;
}

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

Обратите внимание: Есть множество различных способов вычисления рамок столкновений. Вы можете написать код, похожий на тот, что уже имеется в классе CCNode, но наш нынешний способ гораздо проще, несмотря на некоторую неочевидность.

Далее, осуществите следующие изменения в методе update так, чтобы он обновлял desiredPosition заместо текущей позиции:

// Замените 'self.position = ccpAdd(self.position, stepVelocity);' на:
self.desiredPosition = ccpAdd(self.position, stepVelocity);

Давайте начнем определять столкновения!

Пришло время для серьезных свершений. Мы собираемся собрать все вместе. Добавьте следующий метод в GameLevelLayer.m:

Нажми меня

- (void)checkForAndResolveCollisions:(Player *)p {  

  NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ]; //1
 
  for (NSDictionary *dic in tiles) {
    CGRect pRect = [p collisionBoundingBox]; //2
 
    int gid = [[dic objectForKey:@"gid"] intValue]; //3
 
    if (gid) {
      CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height); //4
      if (CGRectIntersectsRect(pRect, tileRect)) {
        CGRect intersection = CGRectIntersection(pRect, tileRect); //5
 
        int tileIndx = [tiles indexOfObject:dic]; //6
 
        if (tileIndx == 0) {
          //Ячейка прямо под Коалой
          p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height);
        } else if (tileIndx == 1) {
          //Ячейка прямо над Коалой
          p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y - intersection.size.height);
        } else if (tileIndx == 2) {
          //Ячейка слева от Коалы
          p.desiredPosition = ccp(p.desiredPosition.x + intersection.size.width, p.desiredPosition.y);
        } else if (tileIndx == 3) {
          //Ячейка справа от Коалы
          p.desiredPosition = ccp(p.desiredPosition.x - intersection.size.width, p.desiredPosition.y);
        } else {
          if (intersection.size.width > intersection.size.height) { //7
            //Ячейка диагональна, но решаем проблему вертикально
            float intersectionHeight;
            if (tileIndx > 5) {
              intersectionHeight = intersection.size.height;
            } else {
              intersectionHeight = -intersection.size.height;
            }
            p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height );
          } else {
          	//Ячейка диагональна, но решаем проблему горизонтально
            float resolutionWidth;
            if (tileIndx == 6 || tileIndx == 4) {
              resolutionWidth = intersection.size.width;
            } else {
              resolutionWidth = -intersection.size.width;
            }
            p.desiredPosition = ccp(p.desiredPosition.x , p.desiredPosition.y + resolutionWidth);
          } 
        } 
      }
    } 
  }
  p.position = p.desiredPosition; //7
}

Отлично! Давайте посмотрим на код, который мы только что написали.

1. Сначала мы получаем набор ячеек, окружающих Коалу. Далее мы проходимся циклом по каждой ячейке из этого набора. Каждый раз, когда мы проходимся по ячейке, мы проверяем ее на предмет столкновений. Если произошло столкновение, мы меняем desiredPosition у Коалы.
2. Внутри каждой петли цикла, мы сначала получаем текущую рамку Коалы. Каждый раз, когда определяется столкновение, переменная desiredPosition меняет свое значение на такое, при котором столкновения больше не происходит.
3. Следующий шаг это получение GID, который мы хранили в NSDictionary, который может являться нулем. Если GID равен нулю, то текущая петля завершается и мы переходим к следующей ячейке.
4. Если в новой позиции находится ячейка, нам нужно получить ее CGRect. В ней может быть, а может и не быть столкновения. Мы осуществляем этот процесс при помощи следующей строчки кода и сохраняем в переменную tileRect. Теперь, имея CGRect Коалы и ячейки, мы можем проверить их на предмет столкновения.
5. Чтобы проверить ячейки на предмет столкновения, мы запускаем CGRectIntersectsRect. Если произошло столкновение, то мы получим CGRect, описывающий CGRect пересечения при помощи функции CGRectIntersection().

Остановимся подумать на дилеммой…

Довольно интересный случай. Нам нужно додуматься как правильно определять столкновения.
Можно подумать, что лучший способ двигать Коалу — двигать ее в противоположную сторону от столкновения. Некоторые физические движки и вправду работают по этому принципу, но мы собираемся применить решение по-лучше.
Подумайте: гравитация постоянно тянет Коалу вниз в ячейки под ней, и эти столкновения происходят постоянно. Если вы представите Коалу, движущуюся вперед, то, в то же время, Коалу все еще тянет вниз гравитацией. Если мы будем решать эту проблему простым изменением движения в обратную сторону, то Коала будет двигаться вверх и влево — а ведь нам нужно нечто иное!
Наша Коала должна смещаться на достаточное расстояние, чтобы все еще оставаться над этими ячейками, но продолжать двигаться вперед с тем же темпом.
image
Та же проблема произойдет, если Коала будет скатываться вниз по стене. Если игрок будет прижимать Коалу к стене, то желаемая траектория движения Коалы будет направлена диагонально вниз и в стену. Просто обратив направление, мы заставим Коалу двигаться вверх и от стены — опять, совсем не то! Мы то хотим, чтобы Коала оставалась вне стены, но все еще спускалась вниз с тем же темпом!
image
Так что, нам нужно решить, когда работать со столкновениями вертикально, а когда горизонтально, и обрабатывать оба действия взаимоисключающе. Некоторые физические движки постоянно обрабатывают сначала первое событие, а потом второе; но мы-то хотим сделать решение по-лучше, основываясь на позиции ячейки Коалы. Так, например, когда ячейка прямо под Коалой, мы хотим, чтобы определитель столкновений возвращал Коалу вверх.
А что если ячейка диагональна позиции Коалы? В этом случае мы используем CGRect пересечения, чтобы понять, как мы должны двигать Коалу. Если ширина этого прямоугольника больше высоты, то возвращать Коалу нужно вертикально. Если высота больше ширины, то Коала должна смещаться горизонтально.
image
Этот процесс будет работать правильно до тех пор, пока скорость Коалы и скорость смены кадров будут в пределах определенных рамок. Чуть позднее мы научимся избегать случаев, когда Коала падает слишком быстро и проскакивает через ячейку вниз.
Как только мы определили, как двигать Коалу — вертикально или горизонтально, мы используем размер CGRect пересечения для определения, насколько нужно сместить Коалу. Смотрим на ширину или высоту соответственно и используем эту велечину как дистанцию смещения Коалы.
Зачем же проверять ячейки в определенном порядке? Вам всегда нужно сначала работать с прилегающими ячейками, а потом с диагональными. Ведь если вы захотите проверить на столкновение ячейку справа снизу от Коалы, то вектор смещения будет направлен вертикально.
image
Однако все еще есть шанс, что CGRect столкновения будет вытянутым вверх, когда Коала чуть-чуть касается ячейки.
Посмотрите на рисунок справа. Синяя область вытянута вверх, потому что прямоугольник столкновения — это лишь малая часть общего столкновения. Однако если мы уже решили проблему с ячейкой прямо под Коалой, то нам уже не нужно определять столкновения с ячейкой снизу справа от Коалы. Так мы и обходим появившиеся проблемы.

Назад к коду!

Вернемся к монструозному методу checkForAndResolveCollisions:

6. Шестая секция позволяет нам получить индекс текущей ячейки. Мы используем индекс ячейки чтобы получать позицию ячейки. Мы собираемся оперировать над прилегающими ячейками индивидуально, смещая Коалу, вычитая или добавляя длину или высоту столкновения. Довольно просто. Однако как только дело доходит до диагональных ячеек, мы собираемся применять алгоритм, описаный в предыдущей секции.
7. В седьмой секции мы определяем, какая наша область столкновения: широкая или вытянутая вверх? Если широкая — работаем вертикально. Если индекс ячейки больше 5, то двигаем Коалу вверх. Если область вытянута вверх — работаем горизонтально. Действуем по похожему принципу порядка индексов ячейки. В конце мы присваиваем Коале полученую позицию.

Этот метод — мозг нашей системы определения столкновений.

Давайте используем все имеющиеся знания на практике! Измените метод update (все еще в GameLevelLayer:)

// Замените "[self getSurroundingTilesAtPosition:player.position forLayer:walls];" на:
[self checkForAndResolveCollisions:player];

Также вы можете удалить или закомментировать блок getSurroundingTilesAtPosition:forLayer:

	/*
  for (NSDictionary *d in gids) {
    NSLog(@"%@", d);
  } //8 */

Запускаем! Удивлены результатом?

image

Пол останавливает Коалио, но тот тут же в него утопает! Почему?
Можете догадаться, что мы упустили? Помните — каждый шаг игры мы добавляем силу гравитации к скорости Коалы. Это обозначает, что Коала постоянно ускоряется вниз.
Мы постоянно добавляем скорость к траектории Коалы вниз, пока она не становится размером с ячейку — мы перемещаемся сквозь целую ячейку за один шаг, что и вызывает проблемы (помните, мы недавно об этом говорили).
Как только мы засекаем столкновение, нам нужно обнулять скорость коалы в направлении ячейки, с которой столкнулись! Коала перестала двигаться, так что и скорость должна с ней считаться.
Если мы этого не осуществим, то у нас будет довольно странное поведение игры. Как мы уже заметили ранее, нам нужен способ определять, касается ли Коала земли, чтобы Коала не смогла прыгать еще выше. Мы выставим этот флажок прямо сейчас. Добавьте следующие строки в checkForAndResolveCollisions:

Нажми меня

- (void)checkForAndResolveCollisions:(Player *)p {
 
  NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ]; //1
 
  p.onGround = NO; //////Здесь
 
  for (NSDictionary *dic in tiles) {
    CGRect pRect = [p collisionBoundingBox]; //3
 
    int gid = [[dic objectForKey:@"gid"] intValue]; //4
    if (gid) {
      CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height); //5
      if (CGRectIntersectsRect(pRect, tileRect)) {
        CGRect intersection = CGRectIntersection(pRect, tileRect);
        int tileIndx = [tiles indexOfObject:dic];
 
        if (tileIndx == 0) {
          //ячейка под Коалой
          p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height);
          p.velocity = ccp(p.velocity.x, 0.0); //////Здесь
          p.onGround = YES; //////Здесь
        } else if (tileIndx == 1) {
          //ячейка над Коалой
          p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y - intersection.size.height);
          p.velocity = ccp(p.velocity.x, 0.0); //////Здесь
        } else if (tileIndx == 2) {
          //ячейка слева
          p.desiredPosition = ccp(p.desiredPosition.x + intersection.size.width, p.desiredPosition.y);
        } else if (tileIndx == 3) {
          //ячейка справа
          p.desiredPosition = ccp(p.desiredPosition.x - intersection.size.width, p.desiredPosition.y);
        } else {
          if (intersection.size.width > intersection.size.height) {
            //tile is diagonal, but resolving collision vertially
			p.velocity = ccp(p.velocity.x, 0.0); //////Здесь
            float resolutionHeight;
            if (tileIndx > 5) {
              resolutionHeight = intersection.size.height;
              p.onGround = YES; //////Здесь
            } else {
              resolutionHeight = -intersection.size.height;
            }
            p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + resolutionHeight);
 
          } else {
            float resolutionWidth;
            if (tileIndx == 6 || tileIndx == 4) {
              resolutionWidth = intersection.size.width;
            } else {
              resolutionWidth = -intersection.size.width;
            }
            p.desiredPosition = ccp(p.desiredPosition.x + resolutionWidth, p.desiredPosition.y);
          } 
        } 
      }
    } 
  }
  p.position = p.desiredPosition; //8
}

Каждый раз, когда под Коалой есть ячейка (либо прилегающая, либо диагональная), мы выставляем значение переменной p.onGround равное YES и обнуляем скорость. Также, если под Коалой есть прилегающая ячейка, мы обнуляем его скорость. Это позволит нам правильно реагировать на текущую скорость Коалы.
Мы выставляем значение переменной onGround равное NO в начале цикла. В этом случае, у onGround будет значение YES только тогда, когда мы обнаружим столкновение Коалы с ячейкой под ней. Мы можем использовать эту особенность для того, чтобы определять может Коала прыгать или нет в текущий момент времени.
Добавьте следующий код в загаловочный файл (и затем синтезируйте все необходимое в исполнительном) в Player.h:

@property (nonatomic, assign) BOOL onGround;

И в Player.m:

@synthesize onGround = _onGround;

Запускаем! Все работает так, как и задумывалось? Да! О, этот великолепный день! Ура!

image

Что дальше?

Поздравляю! Вы полностью закончили свой физический движок! Если вы добрались до этого текста, то можете вздохнуть с облегчением. Это была сложная часть — ничего сложного во второй части туториала не будет.
А вот и исходники проекта который мы сейчас закончили.
Во второй части мы заставим нашего Коалио бегать и прыгать. Так же мы сделаем шипованные блоки в полу опасными для нашей Коалы и создадим экраны выигрыша и проигрыша.
Если вы хотите получить еще больше знаний о физических движках для платформеров, то я советую вам посетить следующие ресурсы:
The Sonic the Hedgehog Wiki — отличное объяснение тому, как Sonic взаимодействует с твердыми ячейками.
Возможно, лучший гайд по созданию платформеров от Higher-Order Fun.


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

Решил перевести этот туториал, вдохновившись этой статьей.
Сам сейчас пишу игру-платформер на iOS и активно пользуюсь туториалами на сайте raywenderlich.com. Советую всем!
После сравнительно большого количества потраченного времени на перевод первой части, задумался о переводе второй части. Напишите в комментариях, нужно ли. Если будет востребована — переведу.
Обо всех найденных неточностях и опечатках, пожалуйста, пишите в хабрапочте или тут в комментариях.
С радостью отвечу на все вопросы по туториалу!

Делаем дендивскую игру Марио на Scratch

Новый урок и рассказ о новой игре! Сегодня мы начнем программировать легендарную игру Марио на Скретч. Обратите внимание, что описание первой части программного кода для Mario Bros. будет координально отличаться от второй части игры. Сделано это для повторения пройденного материала и применения некоторых элементов на втором уроке по детскому программированию в Scratch. Вначале нужно загрузить спрайты — делаем это самостоятельно, без помощи пап, мам и учителей! Вторым шагом стараемся написать движения для Марио, гриба и врага самостоятельно и только если совсем все не получается читаем подготовленный материал. Что же надо реализовать:

  • Марио должен бегать и прыгать;
  • Враг ходить от трубы (именно от трубы, а не от края) до края экрана взад-вперед;
  • Гриб должен появляться при ударе Марио головой по камн и спрыгивать вниз;
  • Из трубы должен появляться цветок-убийца, готовый съесть нашего Scratch героя

Простая и динамичная игра, когда-то была суперхитом игровой приставки Dendy. Постарайтесь запрограммировать игру, в которую играли ваши родители! Показав ее папе или маме вы очень их порадуете, ведь в памяти оживут старые воспоминания. Так что программирование, даже детское может быть эмоциональным и захватывающим при правильной подачи. Ну а сейчас скачиваем спрайты и фон для будущей игры:

  • скачать персонаже Mario Turbo
  • загрузить фон игры

Когда графическая часть будет загружена на компьютер  начинаем подгружать ее в среду разработки Scratch. Напоминаю, что загрузка персонажей и фона в среде разработки Скретч осуществляется через кнопку показану на фотографии ниже:

Загружаем спрайты в среду разработки Скретч для Марио

Пишем программный код для героя Марио

Чтобы заставить Марио двигаться мы будем использовать старые приемы:

  • движение вправо-влево;
  • прыжок вверх-вниз.

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

img

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

Библиотека (англ. library) — это набор готовых функций, классов и объектов для решения каких-то задач.

Ну а теперь давайте познакомимся с модульным программированием на практике. Для особо любопытных будет полезна статья о разработке фонарика для смартфонов на базе Андройд в AppInvector (приступить к чтению урока). В этом уроке явным образом используется и подгружается библиотека — это уже по серьезному! Ну а мы продолжим создавать собственный набор программ для игры:

  1. переходим в раздел: Другие блоки;
  2. нажимаем: Создать блок;
  3. даем название блоку;
  4. нажимаем: ОК.

Создаем мини библиотеку для скретч

А теперь самое интересное — добавляем стандартный набор блоков Скретч для движения спрайта вправо-влево, но делаем это через условие: ЕСЛИ … ТО … ИНАЧЕ

Движение спрайта через условие в Scratch

Далее добавляем из раздела: Движение два блока:

  • Повернуться в направлении (выбираем направление в свойствах блока)
  • Идти 10 шагов (меняем шаг на 2)

Идти два шага и повернуться в нужном направлении в скретч

Таким образом мы с вами получили первую команду в библиотеке под название Марио. Обычно библиотеки состоят из множества команд, которые вызываются по их названию, дальше я покажу как это выглядит. Если мы наполним героя Марио множествами команд и сделаем выгрузку в формате спрайта, то его вполне можно будет назвать библиотекой, ведь при загрузке в другой проект подгрузятся и все написанные команды. Конечно пример достаточно условный, но очень наглядный. Остается добавить цикл: Повторять всегда, чтобы движение работало на протяжениии всей игры, а не однократно и все — вызываем команду из библиотеки!

Заключаем код в вечный цикл, работающий на протяжении всей игры

Теперь, чтобы заставить Марио двигаться не нужно добавлять множество блоков, достаточно разместить один блок вправо-влево:

Вызываем команду из библиотеки Скретч

Попробуйте сделать тоже самое, но только для программирования прыжка — делаем это самостоятельно и пользуемся подсказкой по ссылке вначале урока. Пишем код таким образом, чтобы прыжок был плавным, не дерганным! Должно получиться что-то такое:

Прыжок через библиотеку команд Scratch

img

ВНИМАНИЕ!
Посмотри на модули: Прыжок и Вправо-влево. Обрати внимание, что они отличаются. В чем отличия кода? Подумай почему для прыжка не будет работать порядок написания кода блока: Вправо-влево?

САМОСТОЯТЕЛЬНАЯ РАБОТА:

Допиши программный код Скретч так, чтобы у Марио происходила смена костюмов и было видно, что герой движется.

Код для движения врага Марио

Теперь оживим первого врага Марио и сделаем это обычным способом:

код движения для врага Марио

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

  1. Первый блок устанавливает объект в начальную точку;
  2. Далее вводим Цикл постоянного движения на протяжении всей игры Марио;
  3. Говорим объекту перемещаться со скоростью два шага;
  4. Указываем простое условие- Если касается края оттолкнуться;
  5. Добавляем сложное условие, по которому при касании трубы объект поворачивается в обратном направлении;
  6. Итоговый код написан.

Надеюсь всем все понятно и можно продолжить дальше!

Программируем выползание цветка из трубы

САМОСТОЯТЕЛЬНАЯ РАБОТА:

Попробуйте сами написать код для выползания цветка из трубы, но только так, чтобы движение было плавным — как в прыжке (подсказка!). Не забудьте чтобы цветок-хищник ждал какое-то время. А лучше, чтобы время все время было случайным значением! И конечно, чтобы цветок прятался за трубу, делается это с помощью блока:

Перейти на задний слой

Пишем код для гриба дающего силу

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

Прячем граб суперсилы в игре Марио

Теперь напишем условие, при котром Марио ударяет в гриб и он вылезает на поверхность, ну и конечно продолжает движение. но перед написанием этого года протестируем его работу по нажатию кнопки ПРОБЕЛ. И только потом поместим полученный результат внутрь условия взаимодействия Марио и Гриба.

Программируем появление и движения гриба суперсилы

Добавляем условие взаимодействия спрайтов в Скретч:

Условие взаимодействия спрайтов в Скретч

Переносим код движения внутрь условия. И наслаждаемся демонстрацией отработки кода во время прыжка Марио.

ВНИМАНИЕ:

Я рекомендую сразу немного изменить код, ведь когда грип спрыгнет и столкнется с Марио мы наделим героя супер силой, а потому придется еще раз использовать код: КАСАЕТСЯ МАРИО. Поэтому опишем первое взаимодействие объектов через цвет шапочки героя:

Меняем условие для Марио

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

Модернизируем код движения Марио

Материал в стадии дописывания …

Выпущенная в 1985 году игра Super Mario Bros является, вероятно, самой популярной игрой всех времен и точно главным проектом Nintendo.

Марио — игра-платформер, в этом жанре герой должен прыгать по платформам в 2D-мире и собирать очки. Пройдя с нами до последней буквы этой статьи-руководства, вы создадите своего Марио в визуальной событийно-ориентированной среде «Скретч». Интересно? Тогда давайте приступим!

Как делают игру? Сначала пишут ее «движок». Это что-то вроде скелета, на который потом можно наращивать остальной организм: графика, общие правила взаимодействия «спрайтов», то есть графических объектов. Эти аспекты не изменятся по мере создания остальных компонентов игры, уровней, новых частей. Поэтому их делают в первую очередь.

Другой вариант создания игры «Марио» в Scratch можно посмотреть на нашем YouTube-канале

Шаг №1. «Спрайты» и фон

«Скретч» иногда называют языком программирования, а иногда — средой разработки. Попросту говоря, это учебный язык, где роль операторов и сложных команд настоящих языков выполняют визуальные блоки. Фактически, это конструктор.

Установлена и настроена среда программирования Scratch может быть по-разному. Самый простой вариант — воспользоваться онлайн-версией.

Мы будем указывать названия элементов интерфейса Scratch на русском и на английском, так как вы можете столкнуться с разными языками интерфейса.

Писать код «Марио» на «Скретч» начнем с создания фона и графических объектов (игровых ассетов — на языке геймдева): персонажа, препятствий, наград и тому подобного.

  • в правом нижнем углу нажмите на плашку «Сцена» (Stage) -> «Выбрать фон» (Backdrops)-> «Загрузить фон» (Upload backdrops);

  • загрузите наш файл «Assets»;
  • загрузите таким же образом файл «sky» .

Если плашка «Сцена» у вас в активном состоянии, то есть вы кликнули по ней левой кнопкой мыши, то слева вам доступны загруженные в проект фоны, в том числе и Assets. Кликните по маленькой иконке Assets, чтобы посмотреть, с чем мы работаем. Закройте демонстрационный материал в центре окна Scratch, чтобы увидеть Assets не только в правом большом окне, но и в центральной, рабочей области. Видите?

Здесь собрано все, что нам понадобится: элементы, по которым Марио будет прыгать, труба перемещения и даже монетки и облака. Этот фон сам по себе мы не будем нигде использовать. Просто удобно, если все собрано в одном месте. Новые игры или дополнения к нашей игре обычно включают немало тех же самых объектов, которые мы уже использовали на этапе разработки первых уровней. Имея единый файл с игровыми ассетами, вы сможете просто копировать компоненты, которые в игре уже встречались.

Теперь взгляните левее плашки «Сцена». Там будет другое поле — «Спрайт» (Sprite). На нем по умолчанию доступен спрайт котик. Нажмите на него. Слева вы увидите маленькие иконки «Костюмов» (Costumes). «Костюмы» — это различные положения, которые может принимать наш спрайт в движении. Если «Спрайт» добавлен на «Сцену», то он будет отображаться поверх всех изображений всех фонов (backdrops) «Сцены».

Чтобы лучше это понять, давайте добавим нашему спрайту-котику (он-то и будет нашим Марио) шляпу знаменитого итальянского водопроводчика. Для этого достаточно выбрать самый верхний инструмент слева от центральной области. Кликните по кепке Марио и нажмите «Копировать» (Copy) в горизонтальном меню сверху. Теперь вы можете вставить нужное изображение на любом спрайте, если он относится к нужной «Сцене».

А сейчас поработаем с фоном. Переключитесь на фон sky. Сейчас у нас только заливка голубым цветом. В фоне «Assets» скопируйте белое облако и вставьте его несколько раз в верхней части фона sky. Получилось?

Шаг №2. Создаем уровни

Все получается? Смелее! Если мы создадим Марио, то сумеем сделать любую игру-платформер на «Скретч». На плашке спрайтов, где у нас находится персонаж, создайте новый спрайт. Назовите его «Фон» или Background. Этот спрайт будет важной частью игры. Каждый его костюм будет представлять собой новый уровень игры. Вы можете создать сколько угодно спрайтов, но мы в нашей игре пока сделаем только пять. Просто копируйте объекты из нашего рабочего файла Assets в очередной уровень – так и создаются новые локации в игре «Марио».

Можно назвать уровни Screen-1, Screen-2 как здесь или Уровень 1, Уровень 2 и так далее. Теперь подготовим первый код для нашей игры. Его еще нельзя будет запустить, но в комплексе с другими командами он заработает. Кликните по спрайту «Фон» и переключитесь на вкладку «Код» (Code). Вы увидите справа разноцветные элементы, которые можно выделять мышью и переносить в центр, закрепляя один за другим. Эти «кирпичики» на языке Scratch называются блоками и служат для создания сценариев, которые оперируют с добавленными нами изображениями, музыкой и другими составляющими программы. Добавьте блоки Scratch таким же образом, как показано на скриншоте:

В русскоязычном интерфейсе все будет так же, например, блоку broadcast будет соответствовать блок «передать». В некоторых блоках нужно установить аргументы, то есть дополнительные параметры того, как они должны будут себя вести. Если вы называли уровни игры на русском языке, то в аргументе блока switch costume (изменить костюм на) поставьте «Уровень 1».

Шаг №3. Добавляем блок с «?»

Вы могли заметить, что мы оставили немного места между блоками, когда создавали «Уровень 1» (Screen-1). Некоторые из этих промежутков мы дополним блоком со знаком вопроса. Помните, Марио надо было прыгать, и он головой выбивал какой-нибудь случайный бонус?

Так как ведет себя блок «?» иначе, чем другие, то ему нужен собственный спрайт. В игре нужно симулировать состояние блока со знаком вопроса до и после того, как Марио его «посмотрел», поэтому в спрайте у нас будет два костюма: желтенький блок со знаком вопроса и без знака вопроса.

Напишем код для блока. Кликните на нужный спрайт и только после этого переключитесь на вкладку «Код» (Code). Воспроизведите приведенную ниже структуру у себя. За появление блоков на разных уровнях отвечают условия «if» из самой длинной последовательности команд. Когда выполняется первое условие, активируются координаты X и Y, отвечающие за появление спрайта со знаком вопроса в первом уровне.

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

Вы сможете увидеть, как блок со знаком вопроса внутри перемещается по карте мира. Создавая уровни, мы не зря оставляли промежутки между объектами, которые находятся над поверхностью. Спрайт со знаком вопроса можно поместить именно туда. Так часто делали в свое время разработчики оригинального «Марио».

Как сделать убойную РПГ на Scratch, смотри в нашем видео

Шаг №4. Сделаем спрайты обнаружения препятствий

Хорошая новость: мы уже получили мощный практикум по программированию в среде Scratch. Плохая новость: многого для полноценного Марио у нас еще нет. Например, как наш герой будет понимать, что перед ним: стена или другое препятствие? Самый простой способ решить эту проблему — создать несколько спрайтов в виде прямой черты, которые мы разместим с таким расчетом, чтобы они отделяли фигурку персонажа от окружающего мира: спереди, сзади, снизу и сверху.

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

Например, если мы касаемся передней линией фонового объекта, то можем установить значение right touch в позицию 1. Так Марио поймет, что нечто находится перед ним. Если установим значение 0, значит перед ним ничего не находится. Посмотрите на скриншоты ниже и воспроизведите логику для каждой стороны персонажа. Будьте внимательны, переключайтесь между спрайтами для формирования команд.

(Спрайты правой, левой, верхней и нижней стороны можно скачать или нарисовать самостоятельно в редакторе).

Шаг №5. Передвигаем Марио стрелочками взад-вперед

Чтобы получилась полноценная игра, персонаж должен быть способен двигаться влево-вправо, прыгать в том случае, если он на чем-то стоит или падать, если под ним ничего нет.

Всего этого мы сможем добиться с нашими спрайтами обнаружения препятствий из предыдущего шага. Нам понадобится как-то удостовериться, что спрайт Марио анимирован по мере движения с помощью своих костюмов (помните, у нас в этом спрайте два положения персонажа?). Уровень 1 (Screen-1) — персонаж должен начинать в правильной точке, глядя в правильном направлении. Приступим!

Один из методов запрограммировать движение персонажа в Scratch состоит в том, чтобы переходить на новый уровень каждый раз, когда он достигает правой границы предыдущего. Мы решим проблему постоянной проверкой координаты X нашего Марио. Если оказалось, что он коснулся правой границы экрана, то включается broadcast с текстом «Next Screen». Таков будет принцип нашей программы.

Можно сделать и иначе. Весь «мир» делаем одним большим костюмом спрайта «Фон» (Background) и сдвигаем координату X этого спрайта налево по мере того, как наш герой продвигается вперед. Последний способ — остроумное и изящное решение, но заставит в будущем постоянно редактировать ваш проект с добавлением новых уровней. Причем, написать придется гораздо больше, чем если бы вы просто сделали движение с помощью подхода, который мы описали первым. Думайте о расширяемости своих проектов. Программист — это человек, который выбирает наилучший путь.

Код, состоящий из блоков Scratch, который у нас ниже представлен, все равно будет довольно объемным, хотя и реализует первый подход. Хорошей практикой является комментирование. Новый комментарий можно добавить, щелкнув по тому или иному блоку правой кнопкой мыши и нажав «Add Comment». Переключившись на спрайт нашего котика «Марио» (помните, мы надевали на него шляпу?), добавьте блоки движения персонажа из скриншота ниже.

Шаг №6. Добавляем «монетки», которые собирает Марио

Сейчас мы займемся созданием счета очков в игре. Точно так же, как мы добавляли блоки со знаком вопроса, распределите в уровнях новый спрайт – монетки. Изображение монет можно взять из нашего фона (backdrop) Assets. Как и раньше, мы будем «пролистывать» уровни и клонировать спрайт для них. После этого добавим блок с постоянной проверкой: касается Марио данного клона монетки или нет. В зависимости от этого прибавляем счет. Можно создать несколько костюмов для спрайта монетки и анимировать ее, чтобы она слегка колебалась в воздухе. Переключитесь на спрайт монетки, кликнув по ней и добавьте внутрь блоки в соответствии со скриншотом ниже.

Хотите узнать, как сделать программу-кликер в Scratch? Смотрите здесь.

Шаг №7. Добавляем призы при контакте персонажа с блоками «?»

В оригинальном Super Mario, если герой касался блока «?», то он мог получить либо монетку, либо увеличение силы. Чаще всего вы будете получать монетки, но иногда знак вопроса скрывает звезду, взяв которую ваш Марио получит временную непобедимость. Непобедимость защитит от врагов, которых мы добавим позже. Нам предстоит создать код к спрайту блока со знаком вопроса, который без врагов мы пока не сможем протестировать.

План такой: когда Марио будет касаться нашего блока с сюрпризом, нам надо будет поменять костюм этому блоку. Подвигайте его вверх и вниз в качестве визуального эффекта, выберите приз, который блок даст персонажу. Координаты X и Y спрайта блока «?» будут отвечать за движение призовой Звезды или призовой Монеты после их появления. Теперь настало время создать отдельные спрайты «Призовая звезда» (Prize Star Sprite) и «Призовая монета». Спрайты можно скопировать из фона «Assets».

Когда они будут получать определенное сообщение из блока broadcast, то появятся около блока «?», который только что был затронут. Если выпала монета, то она сразу появляется и исчезает, пополняя счет. Если выпала звезда, то она должна продвинуться и ждать, когда Марио ее заберет. В тот самый момент программа включает broadcast и передает invincible (непобедимый).

Шаг №8. Добавляем непобедимость

На языке программирование Scratch это лучше сделать так: передаем блоком broadcast сообщение «invincible», если Марио касается призовой звезды. Задача в том, чтобы он сразу изменился в ответ на появление этого сообщения. Во-первых, нам надо создать переменную непобедимости. Во-вторых, Марио должен изменить цвет, чтобы показать, что он временно неуязвим для врагов. Непобедимость продлится, скажем, 5 секунд, после чего эта переменная станет снова равна 0, когда мы нажмем на зеленый флаг.

Сейчас самое время дополнить игру врагами! Для этого учебного проекта мы создадим трех. Каждый будет вести себя немного по-своему. Для каждого сделаем собственный спрайт. Вы можете также нарисовать других противников. Созданных персонажей добавьте в фон Asset, помните? Там у нас собраны игровые ассеты.

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

Логика, прописанная для каждого противника, должна проверять, не является ли Марио неуязвимым, когда он с ним соприкасается. Если Марио не получил неуязвимость, то может проиграть, но код, регулирующий это последнее событие, мы еще добавим в спрайт «Марио» позже.

Шаг №9. Добавляем первого врага — ежика

Ежик маленький, и Марио будет легко его пропустить, поэтому он опасный противник. Мы добавим этого врага в уровни 1, 2, 5. Если наш герой его касается, будучи уязвимым, то игра окончена, он проиграл.

Шаг №10. Добавляем второго врага — динозавра

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

Технически это можно реализовать, использовав нижний спрайт обнаружения препятствий Марио. Мы будем проверять, касается ли он динозавра и находится ли при этом герой выше врага. Если при этом номер костюма динозавра в нужный момент меньше 4, то Марио одержал победу. Динозавр будет присутствовать в уровнях 3 и 5.

Шаг №11. Третий, самый опасный противник, — попугай

Попугай, вероятно, самый опасный противник Марио. Он бросает в него яйца, а герой должен уворачиваться, продвигаясь по уровню. Нужно добавить не только спрайт самого попугая, но и отдельный спрайт яйца, которое в нашем коде будет время от времени передавать с помощью broadcast сообщение «Throw Egg».

Попугай встретиться лишь на 4 уровне игры.

Шаг №12. Добавляем проверку на поражение

Мы спроектировали алгоритмы для всех наших врагов. Теперь нам нужно в спрайте нашего котика в шляпе, играющего роль Марио, дописать логику, связанную с касанием до врага (в отсутствии неуязвимости). Нужно добиться, чтобы при касании первого и второго врага, когда их костюм – 4, или яйца, или попугая, нам выдавалось сообщение о том, что игра окончена и мы потерпели поражение (Game over).

При этом код, связанный с Марио, должен «умереть», а сам спрайт персонажа — спрятаться. Просмотрев этот фрагмент кода, мы увидим, что он реализован в ряде почти одинаковых логических конструкций If-Then. Возникает вопрос: почему бы не объединить все это в один большой «if»? В будущем, программируя на настоящих языках программирования, вы не раз столкнетесь с ситуацией, когда лучше написать побольше кода, но чтобы он выглядел и читался просто. Так легче код потом менять. Нет никакой пользы от лаконичного фрагмента, если работать с ним тяжело и невозможно понять, что там происходит.

Шаг №13. Проверка на победу

Давайте создадим два новых спрайта. Один будет «Флаг победителя» (или Victory Flag) и спрайт надписи «Победа» (Win).

Флаг победы будет ждать нас в конце «Уровня 5», и мы его разместим точно так же, как мы размещали врагов, с помощью блока «Go To X, Y» (Перейти в x, y). Когда Марио касается флага, broadcast передает «Победа» (Win), показывается спрайт «Победа», а также счет.

Шаг №14. Добавляем фоновую музыку

Самое время позаботиться о звуковом оформлении. Наш «Супер Марио на Скретч» почти закончен. Если вы хотите создать атмосферу традиционного Super Mario Bros, то лучше добавьте аранжировку из самой игры, файл с которой легко отыскать в интернете. В этой тренировочной игре мы используем стандартные звуки Scratch (Video Game 1). Еще нам понадобится специальный звуковой эффект для момента, когда наш Марио получает непобедимость, а также звук, сопровождающий сообщения о выигрыше и проигрыше.

Когда мы запускаем один звук, другие должны перестать звучать. К счастью, сделать это не так сложно. Достаточно использовать в нужном спрайте блок «Стоп» (Stop) с параметром в положении «другие скрипты спрайта» (other scripts in sprite). Еще один broadcast потребуется, чтобы начать опять основную мелодию, когда время неуязвимости Марио уже истекло.

Ура! Победа!

Итак, мы создали нашу игру, классно провели время и изучили основы программирования Scratch. Нам пришлось поломать голову, приложить внимание, усидчивость и терпение. И результат — вот он! Реальное дело наших рук, которым можно гордиться.

Написав Марио на Scratch, вы в будущем можете сделать любую игру-платформер на «Скретч»! Если вы или ваш ребенок хотите приобщиться к захватывающему миру программирования, записывайте его на один из учебных курсов Pixel! Будет очень интересно! Кроме программирования для детей на Scratch, Lua и Python, в школе есть множество других курсов на любой вкус.

Создаем игру с стиле Mario на python

Создаем игру с стиле Mario на python
Случайно разметим монеты, подальше от стен и других монет
"""
No coins on the walls

Simple program to show basic sprite usage. Specifically, create coin sprites that
aren't on top of any walls, and don't have coins on top of each other.

Artwork from https://kenney.nl

If Python and Arcade are installed, this example can be run from the command line with:
python -m arcade.examples.sprite_no_coins_on_walls
"""
import arcade
import random
import os

SPRITE_SCALING = 0.5
SPRITE_SCALING_COIN = 0.2

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Sprite No Coins on Walls Example"

NUMBER_OF_COINS = 50

MOVEMENT_SPEED = 5


class MyGame(arcade.Window):
    """ Main application class. """

    def __init__(self, width, height, title):
        """
        Initializer
        """
        super().__init__(width, height, title)

        # Set the working directory (where we expect to find files) to the same
        # directory this .py file is in. You can leave this out of your own
        # code, but it is needed to easily run the examples using "python -m"
        # as mentioned at the top of this program.
        file_path = os.path.dirname(os.path.abspath(__file__))
        os.chdir(file_path)

        # Sprite lists
        self.all_sprites_list = None
        self.coin_list = None

        # Set up the player
        self.player_sprite = None
        self.wall_list = None
        self.physics_engine = None

    def setup(self):
        """ Set up the game and initialize the variables. """

        # Sprite lists
        self.all_sprites_list = arcade.SpriteList()
        self.wall_list = arcade.SpriteList()
        self.coin_list = arcade.SpriteList()

        # Set up the player
        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
                                           SPRITE_SCALING)
        self.player_sprite.center_x = 50
        self.player_sprite.center_y = 64

        # -- Set up the walls
        # Create a series of horizontal walls
        for y in range(0, 800, 200):
            for x in range(100, 700, 64):
                wall = arcade.Sprite(":resources:images/tiles/boxCrate_double.png", SPRITE_SCALING)
                wall.center_x = x
                wall.center_y = y
                self.wall_list.append(wall)

        # -- Randomly place coins where there are no walls
        # Create the coins
        for i in range(NUMBER_OF_COINS):

            # Create the coin instance
            # Coin image from kenney.nl
            coin = arcade.Sprite(":resources:images/items/coinGold.png", SPRITE_SCALING_COIN)

            # --- IMPORTANT PART ---

            # Boolean variable if we successfully placed the coin
            coin_placed_successfully = False

            # Keep trying until success
            while not coin_placed_successfully:
                # Position the coin
                coin.center_x = random.randrange(SCREEN_WIDTH)
                coin.center_y = random.randrange(SCREEN_HEIGHT)

                # See if the coin is hitting a wall
                wall_hit_list = arcade.check_for_collision_with_list(coin, self.wall_list)

                # See if the coin is hitting another coin
                coin_hit_list = arcade.check_for_collision_with_list(coin, self.coin_list)

                if len(wall_hit_list) == 0 and len(coin_hit_list) == 0:
                    # It is!
                    coin_placed_successfully = True

            # Add the coin to the lists
            self.coin_list.append(coin)

            # --- END OF IMPORTANT PART ---

        self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list)

        # Set the background color
        arcade.set_background_color(arcade.color.AMAZON)

    def on_draw(self):
        """
        Render the screen.
        """

        # This command has to happen before we start drawing
        self.clear()

        # Draw all the sprites.
        self.wall_list.draw()
        self.coin_list.draw()
        self.player_sprite.draw()

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. """

        if key == arcade.key.UP:
            self.player_sprite.change_y = MOVEMENT_SPEED
        elif key == arcade.key.DOWN:
            self.player_sprite.change_y = -MOVEMENT_SPEED
        elif key == arcade.key.LEFT:
            self.player_sprite.change_x = -MOVEMENT_SPEED
        elif key == arcade.key.RIGHT:
            self.player_sprite.change_x = MOVEMENT_SPEED

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. """

        if key == arcade.key.UP or key == arcade.key.DOWN:
            self.player_sprite.change_y = 0
        elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
            self.player_sprite.change_x = 0

    def on_update(self, delta_time):
        """ Movement and game logic """

        # Call update on all sprites (The sprites don't do much in this
        # example though.)
        self.physics_engine.update()


def main():
    """ Main function """
    window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

https://t.me/pythonl

Просмотры: 846

Super Mario game in Python

Have you ever wanted to have the power to create the Mario game from scratch? Now, you can create your own Super Mario game in Python and Pygame! This step-by-step tutorial with source code will walk you through the entire process, from setting up the game environment to creating your own Mario game in Python.

In addition, we’ll be able to customize the game by adding characters, levels, and power-ups to make the game unique. So get ready to explore the world of game development and learn how to create a Super Mario game in Python using Pygame.

  1. Setting Up the Game Environment
  2. Folder structure of game
  3. Designing the Level and Adding Sprites
  4. Programming the Character Movement
  5. Creating the Enemy AI
  6. Adding the Power-Ups and Coins
    • Power Up:
    • Tiles and Coins:
  7. Making the Level Interactive
  8. Adding the Title Screen and Game Over Screen
    • Title Screen:
    • Game Screen:
    • Game Over Screen:
  9. Complete code for the Super Mario game in Python using Pygame
  10. Output:
  11. Conclusion

Setting Up the Game Environment

We want to set up our game environment so that we can start designing our game. At the highest level, we’ll have three modules in your code: the game module, the level module, and the sprite module. The game module will contain game logic (i.e., what happens when a player dies), and the level module will include game design (i.e., what the level looks like). 

The sprite module will contain our sprites (i.e., your characters, coins, blocks, etc.). It’s important to separate our game design and game logic because we want to minimize the dependencies between our modules so that they are easier to understand and modify. 

We’ll also need to install the Pygame library( pip install pygame==2.1.3.dev8 ), allowing us to create the game from scratch. You can find installation instructions here. Once you have Pygame installed, you should be able to open up your Python IDE and start creating your game.

Folder structure of game

Folder structure of game

Designing the Level and Adding Sprites

Now that we have our environment set up for the Mario game in Python, it’s time to create the level for the game. A level represents the game space that includes the general environment and obstacles that the player needs to overcome (e.g., ground, grass, rocks, trees, mountains, vines, clouds, and rivers). We can create the level using a graphical editor like Tiled. There are also many visual-level design tools available online.

You can create a level by placing various sprites, such as coins, blocks, and vines. Once we have your level designed, we will need to save it as an image file (.png) and then load it into your Python IDE. You can also find thousands of sprites online, which you can use to create various characters and obstacles in your game.

Designing the Level and Adding Sprites

Programming the Character Movement

Once the level is designed, you can start programming the character movement. First, we’ll need to import the pygame library in our IDE and then write a few functions for a player to do the following things:

  • Move left and right – Move up or down a level. 
  • Collect coins. 
  • Avoid rocks and vines. 
  • And die when it hits an enemy. 

Once We’ve written these functions, you can call them from your main game loop. You’ll also need to create a function that tells the player what to do when a power-up is collected. You can also create a function to create the enemy characters by spawning them at random places.

Creating the Enemy AI

Enemy characters are controlled by the computer and move toward the player’s character. They are programmed to avoid moving toward the edge of the level unless they are going down a level. Create a function that will tell the computer how to decide where to go. For example, the enemy can go toward the player or a random location.

We can also choose whether the enemy should go down a level or stay on the same level. To make our enemies more challenging, We can add conditions to their movement, such as going toward the player only if the player is close to them. Also, we add an idle state to make your enemies stop moving when the player is far away from them.

When the player gets close to the enemy, the enemy should start running toward the player again. Then create a function to generate a new enemy at a random location whenever the player goes down a level.

enemy images

Adding the Power-Ups and Coins

To create the power-ups, We will need to create a function that will pick a random power-up from a list of power-ups. We can also create a function to tell the computer when to give the player a power-up after making a list of coins for the level. Pick a random coin from a list of coins, and then place the coin at a random location in the level. Finally, we can create a function that tells the computer when to give the player a coin. Once we have started these functions, you can add them to the game loop.

Power Up:

power up

Tiles and Coins:

tiles and coins

Making the Level Interactive

We can make the level more interactive by adding sounds whenever a power-up is collected, or the player picks up a coin. Also, make vines come down from the top of the level when a player goes down a level. We can make ground disappear when a player goes down a level and make rocks fall down from the top of the level.

We can also make vines come down from the bottom of the level when the player goes up a level. We can make the ground appear when a player goes up a level, and rocks appear at the bottom when a player goes down a level. We can also make water flow from the top to the bottom when a player goes down a level.

Level – 1

level 1

Level – 2

level 2

Level – 3

level 3

Level – 4

level 4

Adding the Title Screen and Game Over Screen

Now that We have completed most of our game Create a title screen and a game over screen to finish the game. Which tells the player how many coins they’ve collected and their score. You can create your title screen using a logo or a picture, and you can create your game over the screen by using a message to inform the player that the game has ended.

Title Screen:

title screen

Game Screen:

screen during play

Game Over Screen:

game over screen

Complete code for the Super Mario game in Python using Pygame

Output:

Conclusion

Now that you have completed your game, you can test it to ensure that it works as expected. You can also use feedback from friends and family to improve the gameplay of your game. Once you have made the necessary changes to your game, you can publish it online for people to play.

Thank you for visiting our website.


Also Read:

Во-первых, вам нужен крутой футбольный фон. Перейдите в раздел « Фон » в нижнем левом углу и щелкните значок изображения, чтобы выбрать один из фонов «Царапины». Есть один, который называется Goal1, который будет работать отлично. Затем щелкните значок спрайта, чтобы добавить новый готовый спрайт. Вы должны увидеть Ball-Soccer в списке — дважды щелкните по нему, чтобы добавить его к своим спрайтам.

Теперь, когда графика готова, вам нужно начать с нескольких блоков. Выберите кошку, потому что он тот, кто выполняет действие. Помните, что вкладка « События » содержит множество стартовых блоков? Посмотрите, и вы увидите тот, который срабатывает при нажатии на зеленый флаг. Это идеальная отправная точка — перетащите ее в рабочую область сценариев .

Как только вы начинаете, вы хотите, чтобы кошка переехала и пнула мяч, верно? Это звучит как что-то, что попадет под вкладку Motion . У вас есть несколько вариантов, чтобы заставить кошку двигаться, например, блок шагов Move X. Но мы не можем гарантировать, сколько шагов потребуется кошке, чтобы достичь мяча. Для этого есть лучший блок — попробуйте Glide X секунд, чтобы заблокировать. Одна секунда — хорошее время, и вам нужно нажать на синий значок i футбольного мяча, чтобы увидеть его координаты. Введите их в блок, и ваше первое действие завершено!

Пинать мяч

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

Обратите внимание на то, как блок « Дождаться» имеет удлиненную шестиугольную форму внутри. Многие блоки Sensing соответствуют этой форме, поэтому посмотрите на них, чтобы найти правильный. Видите трогательный блок в верхней части списка? Вы можете перетащить это прямо в отверстие внутри Подождите, пока . Измените раскрывающийся список на Cat или как вы назвали спрайта cat.

скретч-код футбольного мяча

Теперь вам просто нужно, чтобы футбольный мяч влетел в цель. Glide X секунд, чтобы заблокировать в категории движения, которую мы использовали ранее для кошки, будет работать нормально. Зафиксируйте этот блок под ожиданием до и поместите указатель мыши на сетку ворот. Вы увидите координаты X и Y под сценой — вставьте их в блок Glide . Мяч должен двигаться довольно быстро при ударе, поэтому давайте попробуем 0,5 секунды на время.

И толпа сходит с ума

Последний шаг — добавление звука! Выберите вкладку Звуки над рабочей областью, чтобы добавить новую. Щелкните значок динамика под « Новый звук», чтобы получить его из библиотеки Scratch. Есть категория под названием « Приветствие» в категории « Человек », которая идеально подходит. Дважды щелкните по нему, чтобы добавить, а затем вернитесь в рабочее пространство для футбольного мяча.

Вы найдете блок с надписью Play sound в категории Sound . Сделайте это под блоком скольжения , и все готово! Нажмите на зеленый флаг, чтобы воспроизвести анимацию. Кошка подбегает к мячу, и когда он касается его, мяч летит в цель, и толпа приветствует.

Это было не слишком сложно! Теперь, когда мы выяснили, как блоки соотносятся и сочетаются друг с другом, давайте посмотрим, как вы можете создать игру Mario, используя Scratch.

Создание базовой игры Mario

Приведенный выше пример простого футбола показывает, как вы можете использовать блоки для управления спрайтами, но в нем нет геймплея, анимации или музыки. Давайте углубимся в это и создадим простую игру Mario. Мы могли бы потратить тысячи слов на каждый аспект создания игры, поэтому мы будем придерживаться основ.

Обратите внимание: поскольку я не художник, для целей данного урока я копирую спрайты Mario из Интернета. Графика Mario принадлежит Nintendo, и вы не должны публиковать игры с использованием защищенных авторским правом спрайтов. Это представлено только в качестве примера.

Импортировать графику

Первый шаг — импортирование ваших спрайтов и фонов в Scratch. Так как мы используем изображения из Интернета, я буду загружать их, а затем загружать их в Scratch. Создавать логику, чтобы Марио мог прыгать на врагов, чтобы победить их, но умирал от прикосновения к их сторонам, слишком сложно для этого урока, поэтому мы попросим его собирать монеты.

В конце урока я предоставлю ZIP-файл с окончательными данными, которые я использовал. Если вы используете их, вам не придется беспокоиться об инструкциях по работе с изображениями в руководстве. Если вы хотите скачать все самостоятельно, продолжайте. Вот спрайты, которые я скачал:

  • Марио бегает (нам достаточно двух кадров)
  • Прыжки марио
  • Анимированная монета
  • Наземные блоки
  • Облака

У Scratch есть фон Blue Sky 3 , который отлично подойдет для наших нужд.

Редактировать Спрайт Костюмы

Поскольку есть две спрайты, которые составляют анимацию бега Марио, вам нужно добавить их в виде отдельных костюмов. Используйте графический редактор, такой как Paint.NET максимально использовать чтобы сохранить две рамки Mario как отдельные файлы — вы можете проигнорировать третью. Загрузите первый спрайт Mario, затем выберите его и используйте вкладку Костюмы, чтобы загрузить другой спрайт в качестве его второго костюма. Дайте им различимые имена, такие как Mario-1 и Mario-2 . Добавьте прыгающего спрайта в качестве другого костюма для Марио.

скретч марио костюмы

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

Для земли вам понадобится много блоков, так как Марио бежит вдоль них. Используйте Paint.NET, чтобы захватить шесть блоков в середине изображения наземных блоков , а затем сохраните их как отдельный файл. Вам понадобится около 12 блоков, чтобы покрыть всю нижнюю часть экрана, как только вы уменьшите их до хорошего размера. Таким образом, вы должны поместить две копии этих шести блоков бок о бок для вашего наземного спрайта. Загрузите это, а затем сделайте два дубликата наземных спрайтов в Scratch.

Монета — это анимированный GIF, поэтому она немного другая. Всякий раз, когда вы загружаете его, Scratch создает костюмы для каждого кадра анимации. Это изображение имеет всего 11 кадров, но, к сожалению, также имеет белую рамку вокруг него, которая смотрится на синем фоне. Вы должны будете открыть каждый костюм для монеты в редакторе Царапин. Используйте инструмент пипетки, чтобы выбрать синий цвет фона, затем используйте инструмент заливки, чтобы изменить белые края монеты на бледно-голубой.

окраска царапины монеты

Вам нужно будет изменить размер спрайтов с помощью кнопок « Увеличить» и « Сжать» в верхней части экрана, прямо над зеленой кнопкой флага. Нажмите любую кнопку, затем щелкните спрайт, который вы хотите изменить, на сцене слева. Это также изменит размеры всех костюмов. Приблизительный их пока; Вы можете настроить позже.

Импортировать звуки

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

  • Super Mario Bros. Theme
  • Марио прыгать звук
  • Монета собирать звук

Оживить монеты

Теперь, когда все активы готовы, пришло время их оживить. Мы начнем с монет, так как это легко. Выберите спрайт монеты и вкладку Сценарии . Поскольку наши монеты являются анимированными GIF-файлами , мы можем использовать ряд блоков, чтобы постоянно прокручивать их костюмы, чтобы они, казалось, двигались.

Анимационный скрипт выглядит примерно так:

скрипт анимации монет

Короче говоря, этот скрипт устанавливает монету в ее состояние по умолчанию, когда вы нажимаете зеленый флаг. Затем он бесконечно циклически перебирает кадры со скоростью, заданной в качестве переменной FPS на вкладке « Данные ». Поиграйте с этим числом, если вам не нравится скорость.

Снимите флажок рядом с Coin-FPS на вкладке « Данные » (это настраиваемая переменная, которую вы создаете), чтобы она не отображалась на экране.

Заставить Марио двигаться

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

Во-первых, вам нужно сделать четыре переменные на вкладке Data . Все четыре из них для всех спрайтов, кроме Velocity , который предназначен только для Mario:

поцарапать Марио переменные

  • Гравитация — это постоянная величина, которая притягивает Марио к земле, когда он прыгает.
  • OnGround отслеживает, касается ли Марио земли или нет.
  • ScrollX измеряет горизонтальное движение экрана.
  • Скорость (только Mario) контролирует скорость, с которой Mario прыгает.

Анимация Земли

Вы уже сделали две копии своего наземного спрайта, щелкнув по нему правой кнопкой мыши и выбрав « Дублировать» . Перетащите Ground-1 в крайний левый угол экрана, чтобы его левый блок коснулся крайнего левого края экрана. Затем перетащите другой наземный спрайт справа от первого. Выровняйте края вверх, и это будет выглядеть так, будто земля — ​​это одна сплошная часть.

Вот кодовый блок, который вам понадобится для каждого наземного спрайта:

блок наземного кода

Это помещает землю в нижней части экрана, а затем просто прокручивает блоки по мере движения Марио. ScrollX — позиция блоков; 0 — это позиция по умолчанию, которая запускается при нажатии на зеленый флаг. Вы заметите, что не можете двигаться влево сразу после начала.

Для второго (и последующих) наземных блоков увеличьте цифру 0 в ScrollX + 480 * 0 на единицу для каждого нового участка земли. Это сместит его, чтобы он плавно прокручивался.

Логика марио

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

Сделайте свою собственную игру Mario! Основы царапин для детей и взрослых 10 Mario Arrow Key Movement

Этот блок кода изменяет переменную ScrollX при перемещении Марио. Всякий раз, когда вы нажимаете влево или вправо, Марио поворачивается в соответствующем направлении и делает шаг, увеличивая ScrollX на 3. Если вы обнаружите, что Марио переворачивается вверх ногами, когда вы двигаетесь влево, нажмите синий значок i на его спрайте и убедитесь, что стиль вращения настроен на второй вариант. Это перевернет его влево и вправо, а не по кругу.

изменения костюма марио

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

Марио земля трогательная логика

Простой фрагмент кода, который вычисляет переменную OnGround . Если он касается одного из наземных блоков, OnGround равняется 1 (true). Когда он прыгает, OnGround равен 0 (false).

Марио прыжки код

Эти два блока кода управляют скоростью прыжка Марио. Слева блок, который гарантирует, что у Марио нет импульса, если он находится на земле. Если он находится в воздухе, то его скорость постепенно замедляется силой тяжести, которая является постоянной величиной. Правый блок заставляет Марио прыгать всякий раз, когда вы нажимаете клавишу пробела. Звучит его скачкообразный звук, и скорость поднимает его в воздух, пока сила тяжести не возьмет верх.

Марио код установки

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

Собирать монеты

Давайте вернемся к монетам. Нам нужно убедиться, что, когда Марио хватает один, он издает звук и исчезает. Давайте создадим отдельный скрипт для этого — разделение скриптов по функциям является важной практикой в ​​программировании. Большое количество блоков затрудняет определение проблемы, когда что-то идет не так.

Вот наш скрипт коллекции монет:

скрипт коллекции монет

Это довольно просто: всякий раз, когда Марио касается монеты, звучит коллекция и монета прячется. В коде анимации монет мы поместили блок « Показать», чтобы монеты снова появлялись при перезапуске.

Свиток монет и облаков

Ты почти там! Поскольку Марио не двигается, но земля прокручивается, мы должны убедиться, что монеты тоже прокручиваются, чтобы Марио мог их собрать. Вот как это работает:

код прокрутки монеты

Это помещает монету в значение Y (это вертикальное положение экрана), где Марио может легко схватить ее. Затем он использует аналогичную логику с наземными блоками, чтобы прокрутить в сторону Марио. Обратите внимание, что мы увеличили скорость прокрутки до 0,75, чтобы монеты быстро двигались в сторону Марио. Для второй и третьей монет мы увеличиваем значение y в поле до -40 и -20, поэтому они немного выше и Марио труднее их захватить. В поле « Установить блок увеличьте значения 150 * 1 до 150 * 3 и 150 * 5 для второй и третьей монет, чтобы разместить их дальше за пределами экрана.

Облака используют практически идентичный блок кода:

код облачной прокрутки

Опять же, это помещает облако на определенную высоту, а затем прокручивает его по мере движения Марио. Для второго облака, которое находится перед Марио, а не позади него, измените набор x для блокировки на (ScrollX * 0.1) + (150 * 1) , как монеты.

Добавить границы

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

исправления границы нуля Марио

Самый простой способ сделать это — щелкнуть правой кнопкой мыши по сцене и щелкнуть Сохранить изображение сцены . Откройте это в Paint.NET и используйте инструмент пипетки, чтобы выбрать синий цвет фона. Добавьте новый слой, используя правый нижний диалог. Затем с помощью инструмента «Прямоугольник» нарисуйте закрашенный синий прямоугольник с обеих сторон экрана. Покройте примерно половину каждого блока, затем удалите фоновый слой.

скретч марио paint.net создание границы

Сохраните это как файл PNG и загрузите его как новый спрайт с именем Border . Поскольку вы нарисовали границы прямо над экраном, вы можете выровнять его идеально.

Тогда вам просто нужно несколько блоков, чтобы граница всегда была впереди:

скретч-код границы

Расширяя свою игру

Попробуйте окончательный продукт ниже!

Mario.JS

Mario.JS — это игровой 2D-движок, написанный с использованием Javascript и HTML5 Canvas API, предназначенный для обучения основам разработки игр.

В данном уроке мы на его примере научимся:

  • Считывать ввод игрока с клавиатуры и двигать игрового персонажа (сущность)
  • Симулировать физику (силу тяжести, коллизии)
  • Анимировать персонажа с помощью спрайтов
  • Создавать игровые сущности и добавлять им искусственный интеллект
  • Добавлять персонажу взаимодействие с другими сущностями
  • Добавлять в игру звуки

Подготовка к работе

Чтобы скачать и установить движок:
git clone https://github.com/lvl5hm/mariojs
cd mariojs
npm install

Для работы вам понадобится веб-браузер (например, Google Chrome), консоль (например, cmd или powershell) и текстовый редактор. Вы можете выбрать любой редактор на ваш вкус (даже блокнот подойдет), но я рекомендую Visual Studio Code, так как он поддерживает многие часто используемые фичи ‘из коробки’ и не требует особой настройки.

  • Откройте папку Mario.JS в консоли. Вся дальнейшая работа будет происходить в ней
  • Чтобы сделать это в Visual Studio Code:
    • Откройте приложение
    • Файл -> Открыть папку -> выберите папку mariojs -> Выбор папки
    • Нажмите ctrl + ~
  • Введите команду npm start
  • Откройте браузер и перейдите по адресу localhost:8080

Если все сделано правильно, вы увидите кирпичные блоки на сером фоне:
screen

Базовое движение

Откройте файл src/map.js
В нем вы увидите наш игровой уровень в виде ASCII-арта:

const asciiMapRows = [
	 '                                                          ',
	 '                                                          ',
	 '                                                          ',
	 '                                                          ',
	 '                                                          ',
	 '                                                          ',
	 '                                                          ',
	 '                                                          ',
	 '#                                                         ',
	 '#      #    ####                                          ',
	 '#           #  #                                          ',
	 '#############  ###   #####################################'
];

В этих строках каждый символ означает какую-либо игровую сущность, а пробел — ее отсутствие. Символ # означает кирпичный блок (стену). При запуске игры движок считывает символы по порядку и создает уровень в игре. Попробуйте добавить или удалить блоки, отредактировав строки asciiMapRows. Чтобы проверить свои изменения сохраните файл ctrl + S.

Символ @ означает персонажа игрока. Вставьте его в уровень и проверьте, что Марио появился в игре:

const asciiMapRows = [
	'                                                          ',
	'                                                          ',
	'                                                          ',
	'                                                          ',
	'                                                          ',
	'                                                          ',
	'                                                          ',
	'                                                          ',
	'#   @                                                     ',
	'#      #    ####                                          ',
	'#           #  #                                          ',
	'#############  ###   #####################################'
];

screen
Чтобы настроить поведение Марио, откройте файл src/entities/mario.js

//#region imports...

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);
}

Когда игра запущена, функция updateMario выполняется каждый кадр и рисует спрайт Марио на экране с помощью функции drawSprite.

/**
 * Рисует спрайт по координатам заданной сущности
 * @param {object} sprite: Спрайт, который будет нарисован на экране
 * @param {object} entity: Игровая сущность, по координатам x и y которой
 *   будет нарисован спрайт
 * @param {number} animationSpeed: Скорость смены кадров спрайта
 * @param {number} scaleX: Масштаб по оси x - например, если этот
 *   параметр равен -1, то спрайт будет отражен по горизонтали
 * @param {number} scaleY: Масштаб по оси y
 */
function drawSprite(
    sprite, entity,
    animationSpeed = 0, scaleX = 1, scaleY = 1,
) {...}

Параметры animationSpeed, scaleX и scaleY опциональны, так как у них есть значения по умолчанию.


Чтобы Марио начал двигаться, функция updateMario должна изменять его координаты mario.x и mario.y. Изменим ее.
Измененные строчки отмечены знаком >. Этот символ вводить не надо.

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);
>   mario.x += 10 * time.deltaTime;
}

Значение time.deltaTime означает промежуток времени, прошедший за один кадр. Если интересно, зачем оно нужно, спросите меня =)

Теперь каждый кадр mario.x будет увеличиваться на небольшое значение. Сохраните изменения и посмотрите, что получилось. Попробуйте вместо 10 вставить -10.


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

const keyRight = keys[keyCode.ARROW_RIGHT];
/**
 * keyRight= {
 *     isDown,    // true, если клавиша нажата в данный момент
 *     wentDown,  // true, если клавиша была нажата в данный кадр
 *     wentUp,    // true, если клавиша отпущена в данный кадр
 * };
 */

Теперь мы можем управлять движением Марио с помощью условного оператора if:

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);
	
>   const keyRight = keys[keyCode.ARROW_RIGHT];
>   if (keyRight.isDown) {
>       mario.x += 10 * time.deltaTime;
>   }
}

Марио будет двигаться только тогда, когда нажата клавиша ->.
Самостоятельное задание: по аналогии добавьте код, чтобы при нажатии <- Марио двигался влево.

Решение

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);

    const keyRight = keys[keyCode.ARROW_RIGHT];
    if (keyRight.isDown) {
        mario.x += 10 * time.deltaTime;
    }
    
>   const keyLeft = keys[keyCode.ARROW_LEFT];
>   if (keyLeft.isDown) {
>       mario.x -= 10 * time.deltaTime;
>   }
}

Теперь спустим Марио с небес на землю силой гравитации. Для этого мы изменим значение mario.speedY, которое отвечает за вертикальное перемещение Марио.
Как мы знаем из физики, под воздействием силы тяжести объекты движутся с ускорением, направленным вниз, то есть их скорость постоянно увеличивается. Чтобы симулировать это, мы каждый кадр будем увеличивать скорость Марио на константу settings.gravity:

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);
	
    const keyRight = keys[keyCode.ARROW_RIGHT];
    if (keyRight.isDown) {
        mario.x += 10 * time.deltaTime;
    }
    const keyLeft = keys[keyCode.ARROW_LEFT];
    if (keyLeft.isDown) {
        mario.x -= 10 * time.deltaTime;
    }
	
>   mario.speedY += settings.gravity * time.deltaTime;
>   mario.y += mario.speedY * time.deltaTime;
}

Продвинутое движение

К этому моменту все уже заметили еще один фатальный недостаток нашего персонажа — он проходит сквозь стены. Чтобы решить эту проблему, в движке есть специальная функция:

/**
 * Передвигает сущность и останавливает ее, если
 *   она сталкивается с другими заданными сущностями
 * @param {object} entity: Игровая сущность, которая будет
 *   передвигаться на расстояние entity.speedX и entity.speedY
 * @param {array} otherTypes: Список типов сущностей, с которыми
 *   будет рассчитываться столкновение
 * @returns {object} Сущности, с которыми произошло столкновение в данный кадр
 *   по горизонтали и вертикали
 */
function moveAndCheckForObstacles(entity, otherTypes) {...}

Изменим нашу функцию updateMario. Теперь мы не будем изменять координаты mario.x и mario.y напрямую, а будем только управлять его скоростью mario.speedX и mario.speedY и позволим функции moveAndCheckForObstacles сделать остальное:

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);
	
>   const keyRight = keys[keyCode.ARROW_RIGHT];
>   const keyLeft = keys[keyCode.ARROW_LEFT];

>   mario.speedX = (keyRight.isDown - keyLeft.isDown) * 5;
>   mario.speedY += settings.gravity * time.deltaTime;

>   moveAndCheckForObstacles(mario, [ENTITY_TYPE_WALL]);    
}

Теперь Марио не проваливается сквозь пол и стены:
screen


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

const { horizWall, vertWall } = moveAndCheckForObstacles(mario, [ENTITY_TYPE_WALL]);
/**
 * horizWall - результат проверки горизонтальной коллизии
 * vertWall - результат проверки вертикального коллизии
 */

Если в данном кадре не произошло коллизии по вертикали или горизонтали, horizWall или vertWall соответственно будут равны null. То есть, если Марио стоит на земле, vertWall будет не равен null.

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);

    const keyRight = keys[keyCode.ARROW_RIGHT];
    const keyLeft = keys[keyCode.ARROW_LEFT];
>   const keyJump = keys[keyCode.SPACE];

    mario.speedX = (keyRight.isDown - keyLeft.isDown) * 5;
    mario.speedY += settings.gravity * time.deltaTime;

>   if (mario.isOnGround && keyJump.wentDown) {
>       mario.speedY = -12;
>   }

>   const { horizWall, vertWall } = moveAndCheckForObstacles(mario, [ENTITY_TYPE_WALL]);
>   mario.isOnGround = vertWall !== null;
}

И давайте теперь добавим код, чтобы камера следовала за Марио:

export function updateMario(mario) {
    drawSprite(assets.sprMarioIdle, mario);

    const keyRight = keys[keyCode.ARROW_RIGHT];
    const keyLeft = keys[keyCode.ARROW_LEFT];
    const keyJump = keys[keyCode.SPACE];

    mario.speedX = (keyRight.isDown - keyLeft.isDown) * 5;
    mario.speedY += settings.gravity * time.deltaTime;

    if (mario.isOnGround && keyJump.wentDown) {
        mario.speedY = -12;
    }

    const { horizWall, vertWall } = moveAndCheckForObstacles(mario, [ENTITY_TYPE_WALL]);
    mario.isOnGround = vertWall !== null;
	
>   camera.x = mario.x;
}

Готово! Теперь у нас есть полноценный игровой персонаж.

Анимация

Чтобы Марио не оставался в стоячем положении, когда он бегает и прыгает, добавим ему анимацию с помощью уже известной нам функции drawSprite.

Во-первых, мы можем передать туда опциональный четвертый аргумент scaleX, чтобы изменить направление изображения, чтобы Марио не бегал спиной вперед. Для этого запишем новое значение mario.direction, которое будет меняться в зависимости от того, какая клавиша была нажата последней:

export function updateMario(mario) {
    const keyRight = keys[keyCode.ARROW_RIGHT];
    const keyLeft = keys[keyCode.ARROW_LEFT];
    const keyJump = keys[keyCode.SPACE];

    // анимация
>   drawSprite(assets.sprMarioIdle, mario, 0, mario.direction);

    // движение и коллизии
    mario.speedX = (keyRight.isDown - keyLeft.isDown) * 5;
    mario.speedY += settings.gravity * time.deltaTime;

    if (mario.isOnGround && keyJump.wentDown) {
        mario.speedY = -12;
    }

    const { horizWall, vertWall } = moveAndCheckForObstacles(mario, [ENTITY_TYPE_WALL]);
    mario.isOnGround = vertWall !== null;
	
    camera.x = mario.x;
}

Самостоятельное задание: нужно изменять значение mario.direction на 1, если была нажата клавиша ->, и на -1, если <-.

Решение

export function updateMario(mario) {
    const keyRight = keys[keyCode.ARROW_RIGHT];
    const keyLeft = keys[keyCode.ARROW_LEFT];
    const keyJump = keys[keyCode.SPACE];
    
    // анимация
>   if (keyLeft.isDown) {
>      mario.direction = -1;
>   }
>   if (keyRight.isDown) {
>       mario.direction = 1;
>   }

    drawSprite(assets.sprMarioIdle, mario, 0, mario.direction);

    // движение и коллизии
	...
}

Во-вторых, мы можем добавить анимацию бега. Для этого нам нужно проверить, равна ли скорость Марио по оси x нулю, и если да, рисовать спрайт покоя:

drawSprite(assets.sprMarioIdle, mario, 0, mario.direction);

Иначе рисовать спрайт бега:

drawSprite(assets.sprMarioRunning, mario, 0.2, mario.direction);

Попробуйте сделать это самостоятельно

Решение

export function updateMario(mario) {
    const keyRight = keys[keyCode.ARROW_RIGHT];
    const keyLeft = keys[keyCode.ARROW_LEFT];
    const keyJump = keys[keyCode.SPACE];
    
    // анимация
    if (keyLeft.isDown) {
       mario.direction = -1;
    }
    if (keyRight.isDown) {
        mario.direction = 1;
    }
    
>   if (mario.speedX === 0) {
>       drawSprite(assets.sprMarioIdle, mario, 0, mario.direction);
>   } else {
>       drawSprite(assets.sprMarioRunning, mario, 0.2, mario.direction);
>   }

    // движение и коллизии
    ...
}

Теперь осталось добавить только анимацию прыжка. Для того, чтобы определить, находится Марио в воздухе или нет, у нас уже есть значение mario.isOnGround:

export function updateMario(mario) {
    const keyRight = keys[keyCode.ARROW_RIGHT];
    const keyLeft = keys[keyCode.ARROW_LEFT];
    const keyJump = keys[keyCode.SPACE];
    
    // анимация
    if (keyLeft.isDown) {
       mario.direction = -1;
    }
    if (keyRight.isDown) {
        mario.direction = 1;
    }

>   if (!mario.isOnGround) {
>       drawSprite(assets.sprMarioJumping, mario, 0, mario.direction);
>   } else if (mario.speedX === 0) {
        drawSprite(assets.sprMarioIdle, mario, 0, mario.direction);
    } else {
        drawSprite(assets.sprMarioRunning, mario, 0.2, mario.direction);
    }

    // движение и коллизии
    ...
}

Враги

Добавим на карту в файле src/map.js символ G:

const asciiMapRows = [
    '                                                          ',
    '                                                          ',
    '                                                          ',
    '                                                          ',
    '                                                          ',
    '                                                          ',
    '                                                          ',
    '                                                          ',
    '#   @  #                                                  ',
    '#           ####                                          ',
    '#     G     #  #                                          ',
    '#############  ###   #####################################'
];

screen

В игре появился гумба, но если мы заглянем в файл src/entities/goomba.js, то увидим, что его функция updateGoomba выглядит почти так же, как и updateMario до начала нашей работы:

export function updateGoomba(goomba) {
    drawSprite(assets.sprGoomba, goomba, 0.1);
}

То есть она лишь рисует спрайт, все остальное мы сейчас добавим сами.
Применим к гумбе силу тяжести, как мы делали это с Марио:

export function updateGoomba(goomba) {
    drawSprite(assets.sprGoomba, goomba, 0.1);
    
>   goomba.speedY += settings.gravity * time.deltaTime;
>   const { horizWall, vertWall } = moveAndCheckForObstacles(goomba, [ENTITY_TYPE_WALL]);
}

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

export function updateGoomba(goomba) {
>   if (!goomba.isInit) {
>       goomba.speedX = 2;
>       goomba.isInit = true;
>   }

    drawSprite(assets.sprGoomba, goomba, 0.1);
    
    goomba.speedY += settings.gravity * time.deltaTime;
    const { horizWall, vertWall } = moveAndCheckForObstacles(goomba, [ENTITY_TYPE_WALL]);
}

После того, как функция выполнится первый раз, значение goomba.isInit станет равно false, и условие !goomba.isInit больше никогда не позволит выполнить этот сегмент.


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

Теперь мы используем horizWall, чтобы узнать, столкнулись ли мы со стеной по оси х.

Попробуйте решить это самостоятельно

Подсказка: Если было столкновение (horizWall !== null) мы можем сравнить положение goomba.x и vertWall.x, чтобы узнать, уперся ли гумба в стену слева или справа от него, и изменить goomba.speedX соответственно.

Решение

	export function updateGoomba(goomba) {
    if (!goomba.isInit) {
	goomba.speedX = 2;
	goomba.isInit = true;
    }

    drawSprite(assets.sprGoomba, goomba, 0.1);

    goomba.speedY += settings.gravity * time.deltaTime;
    const { horizWall, vertWall } = moveAndCheckForObstacles(goomba, [
	ENTITY_TYPE_WALL
    ]);

>   if (horizWall !== null) {
>       if (horizWall.x > goomba.x) {
>           goomba.speedX = -2;
>       } else {
>           goomba.speedX = 2;
>       }
>   }
}

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

/**
 * Проверяет, произошло ли столкновение сущности entity с какой-либо
 *   другой сущностью
 * @param {object} entity: Сущность, которая проверяет столкновение
 * @param {array} otherTypes: Список типов сущностей, с которыми будет
 *   рассчитываться столкновение
 * @returns {object}: Сущность, с которой произошло столкновение в данный
 *   кадр или null
 */
function checkCollision(entity, otherTypes) {...}

Она тоже рассчитывает коллизии, но не передвигает сущность, а лишь сообщает нам результат, столкнулись мы с кем-то или нет.

const mario = checkCollision(goomba, [ENTITY_TYPE_MARIO]);

В значение mario будет записана сущность Марио, если мы с ней столкнемся в данный кадр. И, если mario !== null, нам осталось только проверить, меньше ли goomba.y, чем mario.y, и удалить одного из них с помощью функции removeEntity:

/**
 * Удаляет сущность из игры
 * @param {object} entity Удаляемая сущность
 */
function removeEntity(entity) {...}

Попробуйте сделать это самостоятельно

Решение

	export function updateGoomba(goomba) {
    if (!goomba.isInit) {
        goomba.speedX = 2;
        goomba.isInit = true;
    }

    drawSprite(assets.sprGoomba, goomba, 0.1);

    goomba.speedY += settings.gravity * time.deltaTime;
    const { horizWall, vertWall } = moveAndCheckForObstacles(goomba, [
        ENTITY_TYPE_WALL
    ]);

    if (horizWall !== null) {
        if (horizWall.x > goomba.x) {
            goomba.speedX = -2;
        } else {
            goomba.speedX = 2;
        }
    }

>   const mario = checkCollision(goomba, [ENTITY_TYPE_MARIO]);
>   if (mario != null) {
>       if (mario.y < goomba.y) {
>           removeEntity(goomba);
>       } else {
>           removeEntity(mario);
>       }
>   }
}

Звуки

/**
 * Проигрывает звук
 * @param {object} sound: Звук
 * @param {bool} loop: Зацикливание звука
 * @param {number} volume: Громкость
 */
function playSound(sound, loop = false, volume = 0.02) {...}

/**
 * Останавливает проигрывание звука
 * @param {object} sound: Звук
 */
function stopSound(sound) {...}

Чтобы оживить нашу игру, вставим везде разных звуков.
Включим в начале игры главную тему. Для этого в src/map.js добавим вызов этой функции, и передадим вторым аргументом true, чтобы зациклить звук.

import './entityTypes';
import { camera, createMap, playSound } from './engine/engine';
import assets from './assets';

>playSound(assets.sndMainTheme, true);

const asciiMapRows = ...

Самостоятельное задание:
У нас есть следующие звуки:
assets.sndJump
assets.sndStomp
assets.sndGameOver
Сделайте так, чтобы они играли в подходящие моменты. Когда начинается воспроизведение assets.sndGameOver, стоит остановить воспроизведение главной темы.

Дополнительное задание

  • Сделайте так, чтобы Марио подпрыгивал, когда убивал гумбу
  • Попробуйте изменить гравитацию settings.gravity
  • Попробуйте изменить скорость течения времени settings.timeSpeed
  • Добавьте в игру монетки. Символ на карте — 0 (ноль), функция updateCoin лежит в файле src/entities/coin.js. Сделайте так, чтобы монетки исчезали со звуком assets.sndCoin, когда их касается Марио.

Спасибо за участие :3

Если вам понравилось, и вы хотите узнать больше:

  • Если вы хотите больше узнать о JavaScript
    https://learn.javascript.ru
  • Если вы воспринимаете английский на слух, и хотите узнать о геймдеве на C++
    https://hero.handmade.network/episode/intro-to-c/day1/
  • Если хотите узнать, как написать свой движок на JS, спросите меня)

Понравилась статья? Поделить с друзьями:
  • Как написать игру крестики нолики на python
  • Как написать игру кликер на python
  • Как написать игру змейку на python
  • Как написать игру змейка на python
  • Как написать игру дома