Благодаря Phaser создание замечательных кроссплатформенных игр теперь стало проще, чем когда-либо. Phaser - это библиотека для разработки игр с открытым исходным кодом на JavaScript, разработанная Ричардом Дейви и его командой из Photonstorm. В игры, разработанные с помощью Phaser 3, можно играть в любом современном веб-браузере, а с помощью таких инструментов, как Cordova, можно превратить в приложения для телефона.
Цель этого урока - научить вас основам этой фантастической платформы (Phaser 3), разработав игру вроде "Frogger", которую вы видите ниже:
Вы можете скачать код игры здесь. Все включенные ресурсы вы можете использовать их в своих творениях.
Минимальная среда разработки, необходимая нам состоит из редактора кода, веб-браузера и локального веб-сервера. Первые два требования тривиальны, но последний требует немного большего объяснения. Почему нам нужен локальный веб-сервер?
Когда вы загружаете обычный веб-сайт, часто содержимое страницы загружается перед изображениями, верно? Ну, представьте, если это произошло в игре. Это действительно выглядело бы ужасно, если бы игра загружалась, но изображение игрока не было готово.

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);
Что мы здесь делаем:
Чтобы добавить первые изображения в нашу игру, нам необходимо выработать базовое понимание жизненного цикла сцены:

init. Здесь вы можете настроить параметры для вашей сцены или игры.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. Это произвольное название, вы можете назвать его как угодно.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
}
});

repeat) спрайтов, используя ресурс с меткой dragon.stepX) и на 20 по оси y (stepY) для каждого дополнительного спрайта.Драконы слишком большие. Давайте уменьшим их:
// масштабируем врагов
Phaser.Actions.ScaleXY(this.enemies.getChildren(), -0.5, -0.5);
Phaser.Actions.ScaleXY - это утилита, которая уменьшает масштаб на 0,5 для всех передаваемых спрайтов.getChildren возвращает нам массив со всеми спрайтами, которые принадлежат группе.Так выглядит лучше:

При создании игр и реализации механики, на мой взгляд, всегда стоит хорошо обрисовывать их и понимать задолго до попытки реализации. Движение драконов вверх и вниз будет следовать логике:
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);
}
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);
}
this.cameras.main.resetFX();, чтобы вернуться к нормальному состоянию, для этого добавьте это в конец метода create, или экран будет оставаться черным после перезапуска сцены:
// сброс эффектов камеры
this.cameras.main.resetFX();
Вот и все для этого урока! Надеюсь, вы нашли этот урок полезным.
