Благодаря 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
.
Давайте посмотрим на результат:
Начало координат (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();
Вот и все для этого урока! Надеюсь, вы нашли этот урок полезным.