Как создать аркадную игру с помощью Phaser 3

Благодаря Phaser создание замечательных кроссплатформенных игр теперь стало проще, чем когда-либо. Phaser - это библиотека для разработки игр с открытым исходным кодом на JavaScript, разработанная Ричардом Дейви и его командой из Photonstorm. В игры, разработанные с помощью Phaser 3, можно играть в любом современном веб-браузере, а с помощью таких инструментов, как Cordova, можно превратить в приложения для телефона.

Цель этого урока - научить вас основам этой фантастической платформы (Phaser 3), разработав игру вроде "Frogger", которую вы видите ниже: Вы можете скачать код игры здесь. Все включенные ресурсы вы можете использовать их в своих творениях.

  • Научится создавать простые игры в Phaser 3
  • Работа со спрайтами и их трансформация
  • Основные методы сцены Phaser 3
  • Использование групп для объединения поведения спрайтов
  • Основные эффекты камеры (новая функция Phaser 3)

  • Базовые навыки JavaScript
  • Ваш любимый редактор кода
  • Веб-браузер
  • Локальный веб-сервер
  • Следовать требованиям данного учебного пособия
  • Никакого предшествующего опыта разработки игр не требуется

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

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

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

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

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

Самое простое решение, которое я нашел, - это приложение для Chrome, названное, что удивительно :), Web Server for Chrome . Как только вы установите это приложение, вы можете запустить его непосредственно из Chrome и загрузить папку своего проекта. После установки данного расширения перейдите по ссылке chrome://apps/, чтобы открыть установленные приложения в браузере Chrome: Запустите данное приложение. Откроется окно: Выберите в нем папку с вашим проектом и откройте ссылку веб-сервера, по умолчанию http://127.0.0.1:8887/.

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

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

В нашей папке проекта создайте файл index.html со следующим содержимым:

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8" />
    <title>Аркадная игра на Phaser 3</title>
    <script src="//cdn.jsdelivr.net/npm/phaser@3.20.1/dist/phaser.js"></script>
</head>
<body>
<script src="js/game.js"></script>
</body>
</html>

Теперь создайте папку с именем js, а внутри нее наш файл с игрой game.js:

// создаем новую сцену с именем "Game"
let gameScene = new Phaser.Scene('Game');

// конфигурация нашей игры
let config = {
  type: Phaser.AUTO,  // Phaser сам решает как визуализировать нашу игру (WebGL или Canvas)
  width: 640, // ширина игры
  height: 360, // высота игры
  scene: gameScene // наша созданная выше сцена
};

// создаем игру и передам ей конфигурацию
let game = new Phaser.Game(config);

Что мы здесь делаем:

  • Мы создаем новую сцену. Думайте о сценах как о комнатах, где происходит игровое действие. В игре может быть несколько сцен, а в Phaser 3 игра может даже иметь несколько открытых сцен одновременно (см. пример)
  • Нужно указать нашей игре, какими будут ее размеры в пикселях. Важно отметить, что это размер видимой области. Сама игровая среда не имеет заданного размера (как это было в Phaser 2 с объектом «игровой мир», которого нет в Phaser 3).
  • Игра Phaser может использовать различные системы рендеринга. Современные браузеры имеют поддержку WebGL, которая использует вашу графическую карту при визуализации содержимого страницы для повышения производительности. Canvas API присутствует во многих браузерах. Установив параметр рендеринга в «AUTO», мы говорим Phaser использовать WebGL, если он доступен, и, если нет, использовать Canvas.
  • Наконец, мы создаем наш реальный игровой объект. Если вы запустите это в браузере и откроете консоль, вы увидите сообщение о том, что Phaser запущен:

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

  • Когда начинается сцена, вызывается метод init. Здесь вы можете настроить параметры для вашей сцены или игры.
  • Далее следует фаза предварительной загрузки Phaser (метод preload). Как объяснялось ранее, Phaser загружает изображения и ресурсы в память перед запуском самой игры. Отличительной особенностью этого фреймворка является то, что если вы загружаете одну и ту же сцену дважды, ресурсы будут загружаться из кэша, поэтому это будет происходить быстрее.
  • По завершении фазы предварительной загрузки выполняется метод create. Этот метод, который выполняется один раз, является хорошим местом для создания основных сущностей для вашей игры (игрок, враги и т. д.).
  • Пока сцена запущена (не приостановлена), метод update выполняется несколько раз в секунду (игра будет стремиться к 60, но на менее производительном оборудовании, таком как простой телефон с Android, это может быть меньше). Данный метод мы также будем активно использовать.

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

Давайте покажем на экране наш первый спрайт — фон игры. Ресурсы для этого урока можно скачать здесь. Поместите изображения в папку с именем assets. Следующий код идет после let gameScene = new Phaser.Scene('Game'); :

// загрузка файлов ресурсов для нашей игры
gameScene.preload = function() {

  // загрузка изображений
  this.load.image('background', 'assets/background.png');
};

// выполняется один раз, после загрузки ресурсов
gameScene.create = function() {

   // фон
   this.add.sprite(0, 0, 'background');
}
  • Теперь наше игровое фоновое изображение «background.png» загружено. Мы даем этому ресурсы имя background. Это произвольное название, вы можете назвать его как угодно.
  • Когда все изображения загружены, создается спрайт. Спрайт помещается в точку x = 0, y = 0. Ресурс, используемый этим спрайтом, - это ассет с именем background. Давайте посмотрим на результат: Это не совсем то, что мы хотели, верно? Ведь полное фоновое изображение выглядит так: Прежде чем решить эту проблему, давайте сначала рассмотрим, как устанавливаются координаты в Phaser.

Начало координат (0,0) в Phaser - это верхний левый угол экрана. Ось x направлена вправо, а ось y - вниз: Спрайты по умолчанию имеют начальную точку в своем центре. Это важное отличие от Phaser 2, где спрайты имели так называемую опорную точку в верхнем левом углу.

Это означает, что, когда мы поместили фон по координатам (0,0), мы фактически сказали Phaser: поместите центр спрайта в (0,0). Отсюда и результат, который мы получили.

Чтобы поместить верхний левый угол нашего спрайта в верхний левый угол экрана, мы можем изменить начальную точку спрайта, чтобы она была в верхнем левом углу:

// выполняется один раз, после загрузки ресурсов
gameScene.create = function() {

   // фон
   let bg = this.add.sprite(0, 0, 'background');

  // перемещаем начальную точку в верхний левый угол
  bg.setOrigin(0,0);
};

Фон теперь будет отображаться правильно:

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

// загрузка файлов ресурсов для нашей игры
gameScene.preload = function() {

  // загрузка изображений
  this.load.image('background', 'assets/background.png');
  this.load.image('player', 'assets/player.png');
  this.load.image('dragon', 'assets/dragon.png');
  this.load.image('treasure', 'assets/treasure.png');
};

Затем мы добавим спрайт player и уменьшим его размер на 50% в методе create:

  // игрок
  this.player = this.add.sprite(40, this.sys.game.config.height / 2, 'player');

  // уменьшить масштаб
  this.player.setScale(0.5);
  • По горизонтали мы размещаем наш спрайт по координате x = 40. По вертикале (для y) мы размещаем его в середине окна просмотра игры. Объект this дает нам доступ к текущей сцене, а свойство this.sys.game дает нам доступ к глобальному игровому объекту. Таким образом свойство this.sys.game.config позволяет нам получить конфигурацию, которую мы определили при запуске нашей игры.
  • Обратите внимание, что мы сохраняем нашего игрока в текущий объект сцены - this.player. Это позволит нам получить доступ к этой переменной из других методов в нашей сцене.
  • Чтобы уменьшить спрайт персонажа, мы используем метод setScale, который в нашем случае использует масштаб 0,5 к x и y (вы можете получить прямой доступ к свойствам спрайта scaleX и scaleY).

Наша Валькирия готова к действиям! Теперь нам нужно добавить ей способность перемещаться с помощью мыши или сенсорного экрана.

Phaser 3 предоставляет множество способов работы с пользовательским вводом и событиями. В этой игре мы не будем использовать события, а просто проверим, включен ли «активный ввод» (по умолчанию, левая кнопка мыши или касание экрана).

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

Чтобы проверить ввод таким способом, нам нужно добавить метод update к нашему объекту сцены, который обычно вызывается 60 раз в секунду (он основан на методе requestAnimationFrame, в менее производительных устройствах он будет вызываться реже, поэтому не используйте 60 в вашей игровой логике):

// выполняется каждый кадр (ориентировочно 60 раз в секунду)
gameScene.update = function() {

  // проверяем активный ввод 
  if (this.input.activePointer.isDown) {

    // игрок перемещается вперед

}
  • Свойство this.input дает нам доступ к объекту ввода для сцены. Разные сцены имеют свой собственный объект ввода и могут иметь разные настройки ввода.
  • Этот код будет работать, когда пользователь нажимает левую кнопку мыши игровой области или касается экрана.

Когда вход активен, мы увеличим позицию X игрока:

// проверяем активный ввод 
if (this.input.activePointer.isDown) {

    // игрок перемещается вперед
    this.player.x += this.playerSpeed;
}

this.playerSpeed - это параметр, который мы еще не объявили. Для этого будет использоваться метод init, который вызывается перед методом preload. Добавьте следующий код перед определением preload (фактический порядок объявления методов не имеет значения, но это сделает наш код более понятным). Заодно мы добавим и другие параметры, которые мы будем использовать позже:

// некоторые параметры для нашей сцены (это наши собственные переменные - они НЕ являются частью Phaser API)
gameScene.init = function() {
  this.playerSpeed = 1.5;
  this.enemyMaxY = 280;
  this.enemyMinY = 80;
}

Теперь мы можем управлять нашим игроком и перемещать его до конца видимой области!

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

Поскольку мы уже предварительно загрузили все ресурсы, перейдем прямо к части создания спрайта. Обратите внимание, как мы размещаем сундук по оси X на 80 пикселей слева от края экрана:

// место назначения
this.treasure = this.add.sprite(this.sys.game.config.width - 80, this.sys.game.config.height / 2, 'treasure');
this.treasure.setScale(0.6);

В этом уроке мы не используем физическую систему, такую как Arcade (которая поставляется с Phaser). Вместо этого мы проверяем столкновение с помощью служебного метода, который входит в Phaser и позволяет нам определить, перекрываются ли два прямоугольника.

Мы разместим эту проверку в метод update, так как это то, что мы хотим постоянно проверять:

// проверка на столкновение с сокровищем
if (Phaser.Geom.Intersects.RectangleToRectangle(this.player.getBounds(), this.treasure.getBounds())) {
    this.gameOver();
}
  • Метод спрайта GetBounds возвращает координаты прямоугольника в нужном формате.
  • Метод Phaser.Geom.Intersects.RectangleToRectangle вернет true, если прямоугольники пересекаются.

Давайте объявим наш метод gameOver (это наш собственный метод, вы можете вызывать его как хотите - он не является частью API!). В этом методе мы перезапускаем сцену, чтобы играть заново:

// конец игры
gameScene.gameOver = function() {

  // перезапускаем сцену
  this.scene.restart();
}

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

Далее мы создадим группу движущихся драконов. У наших врагов будет движение вверх-вниз: то, что вы ожидаете увидеть в клоне игры Frogger.

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

  // группа врагов
  this.enemies = this.add.group({
    key: 'dragon',
    repeat: 5,
    setXY: {
      x: 110,
      y: 100,
      stepX: 80,
      stepY: 20
    }
  });

  • Мы создаем 5 (свойство repeat) спрайтов, используя ресурс с меткой dragon.
  • Первый из них размещается по координатам (110, 100).
  • От этой первой точки мы перемещаем на 80 по оси x (stepX) и на 20 по оси y (stepY) для каждого дополнительного спрайта.
  • В дальнейшем члены группы называются дочерними.

Драконы слишком большие. Давайте уменьшим их:

// масштабируем врагов
Phaser.Actions.ScaleXY(this.enemies.getChildren(), -0.5, -0.5);
  • Метод Phaser.Actions.ScaleXY - это утилита, которая уменьшает масштаб на 0,5 для всех передаваемых спрайтов.
  • Метод getChildren возвращает нам массив со всеми спрайтами, которые принадлежат группе.

Так выглядит лучше:

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

  • У врагов есть вертикальная скорость, имеющая ограничение по координате Y на максимальное и минимальное значение (у нас уже они объявлены в методе init).
  • Мы хотим увеличивать вертикальное положение врага, пока он не достигнет максимального значения.
  • Затем мы хотим развернуть движение, пока враг не достигнет минимальное значение.
  • Когда минимальное значение будет достигнуто, вновь враг начнет двигаться вверх.

Поскольку у нас есть массив врагов, мы будем обновлять этот массив в update и применять эту логику движения к каждому врагу.

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

  // движение врагов
  let enemies = this.enemies.getChildren();
  let numEnemies = enemies.length;

  for (let i = 0; i < numEnemies; i++) {

    // перемещаем каждого из врагов
    enemies[i].y += enemies[i].speed;

    // разворачиваем движение, если враг достиг границы
    if (enemies[i].y >= this.enemyMaxY && enemies[i].speed > 0) {
      enemies[i].speed *= -1;
    } else if (enemies[i].y <= this.enemyMinY && enemies[i].speed < 0) {
      enemies[i].speed *= -1;
    }
  }

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

// задаем скорость врагов
Phaser.Actions.Call(this.enemies.getChildren(), function(enemy) {
    enemy.speed = Math.random() * 2 + 1;
}, this);

Метод Phaser.Actions.Call позволяет нам вызывать анонимную функцию для каждого элемента массива. Мы передаем его как контекст (хотя и не используем его в качестве контекста). Теперь наше движение вверх и вниз завершено!

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

  // движение врагов
  let enemies = this.enemies.getChildren();
  let numEnemies = enemies.length;

  for (let i = 0; i < numEnemies; i++) {

    // перемещаем каждого из врагов
    enemies[i].y += enemies[i].speed;

    // разворачиваем движение, если враг достиг границы
    if (enemies[i].y >= this.enemyMaxY && enemies[i].speed > 0) {
      enemies[i].speed *= -1;
    } else if (enemies[i].y <= this.enemyMinY && enemies[i].speed < 0) {
      enemies[i].speed *= -1;
    }

     // столкновение с врагами
    if (Phaser.Geom.Intersects.RectangleToRectangle(this.player.getBounds(), enemies[i].getBounds())) {
      this.gameOver();
      break;
    }
  }

Отличная особенность Phaser 3 - это эффекты камеры. В нашу игру можно играть, но будет лучше, если мы добавим какой-то эффект дрожания камеры. Давайте заменим gameOver на:

// конец игры
gameScene.gameOver = function() {

  // дрожание камеры
  this.cameras.main.shake(500);

  // перезапускаем сцену через 500мс
  this.time.delayedCall(500, function() {
    this.scene.restart();
  }, [], this);
}
  • Камера трясётся 500 миллисекунд.
  • Через 500 мс мы перезапускаем сцену с помощью метода this.time.delayCall, который позволяет отложено запустить выполнение какой-то функции.

Существует проблема в этой реализации, вы можете догадаться какая?

После столкновения с врагом метод gameOver будет вызываться много раз в течение 500 мс. Нам нужен какой-то переключатель, чтобы при столкновении с драконом игровой процесс зависал.

Добавьте следующее в конце create:

  // флаг, что игрок жив
  this.isPlayerAlive = true;

Код ниже идет в самом начале метода update, таким образом, этот метод выполняется только если игрок жив:

  // выполняем код, если игрок жив
  if (!this.isPlayerAlive) {
    return;
  }

Наш новый метод gameOver:

// конец игры
gameScene.gameOver = function() {
  // устанавливаем флаг, что игрок умер
  this.isPlayerAlive = false;

  // дрожание камеры
  this.cameras.main.shake(500);

  // перезапускаем сцену через 500мс
  this.time.delayedCall(500, function() {
    this.scene.restart();
  }, [], this);
}

Теперь метод не будет активирован много раз подряд.

Прежде чем попрощаться, мы добавим эффект затухания, который начнется в середине тряски камеры:

// конец игры
gameScene.gameOver = function() {
  // устанавливаем флаг, что игрок умер
  this.isPlayerAlive = false;

  // дрожание камеры
  this.cameras.main.shake(500);

  // затухание камеры через 250мс
  this.time.delayedCall(250, function() {
    this.cameras.main.fade(250);
  }, [], this);

  // перезапускаем сцену через 500мс
  this.time.delayedCall(500, function() {
    this.scene.restart();
  }, [], this);
}
  • Через 250 мс мы запускаем наш эффект затухания, который будет длиться 250 мс.
  • Этот эффект оставит игру черной, даже после перезапуска нашей сцены, поэтому нам нужно вызвать this.cameras.main.resetFX();, чтобы вернуться к нормальному состоянию, для этого добавьте это в конец метода create, или экран будет оставаться черным после перезапуска сцены:
    // сброс эффектов камеры
    this.cameras.main.resetFX();

    Вот и все для этого урока! Надеюсь, вы нашли этот урок полезным.

Оригинал