В этом учебном пособии мы сделаем пошаговую RPG, похожую на раннюю версию игры Final Fantasy, и научимся использовать множество новых интересных функций, встроенных в текущую версию игрового движка Phaser 3. В первой части этого урока мы создадим мир, добавим игрока и научим его перемещаться по карте. Затем мы добавим несколько невидимых зон, где игрок встретит врагов. Вторая часть этой серии поможет вам научиться создавать боевую систему и пользовательский интерфейс. Мы заставим наших юнитов сражаться с врагами!
В этом уроке мы узнаем, как создавать и использовать несколько сцен и как переключаться между ними. У нас будет одна сцена для карты мира и другая для битвы. На самом деле, у нас будет две сцены, запущенные одновременно во время боя: сцена битвы и сцена пользовательского интерфейса, где вы увидите статистику героев, информацию об уроне и ходы врага.
У нас будет спрайт нашего героя на карте мира, и он будет использовать несколько изображений для анимации движения в разных направлениях.
Вы узнаете, как использовать тайловую (плиточную) карту на игровом движке Phaser 3. Для этого урока вы можете создать свою собственную карту в программе Tiled или использовать ту, которая поставляется в исходниках. Вы узнаете, как создавать слои карты и заставлять игрока сталкиваться с элементами слоя карты.
Мы будем использовать аркадную физику, чтобы перемещать персонажа на карте мира и обрабатывать некоторые столкновения. Вы узнаете, как использовать аркадные физические группы, коллайдеры и зоны в Phaser 3.
Вы узнаете, как использовать некоторые интересные эффекты, такие как дрожание и затухание, когда персонаж встречает врагов до начала битвы.
Все assets, используемые в этом руководстве, имеют лицензию CC0. Тайлы созданы Kenney Vleugels и могут быть скачены на www.kenney.nl Спрайты игроков можно найти здесь - https://opengameart.org/content/rpg-character-sprites.
Вы можете скачать исходный код учебника здесь.
Вначале создадим простейший index.html, в котором подключим библиотеку Paser и наш скрипт game.js, а также разместим div, в котором и будет находиться наша игра.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Простая RPG на Phaser 3</title>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<script src="js/phaser.min.js"></script>
<script src="js/game.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="content" style="display: block; width: 100%; text-align: center"></div>
</body>
</html>
Мы начнем создание нашей игры с объекта config. Пока что в нашей игре будут две сцены: Boot Scene и World Scene. Сценами в Phaser 3 управляет Scene Manager, и, как мы увидим в следующей части этого урока, вы можете иметь более одной активной сцены одновременно. Теперь начнем с чего-то более простого - создания игры и загрузки ресурсов.
Вот как должен выглядеть ваш пустой проект и объект конфигурации:
var BootScene = new Phaser.Class({
Extends: Phaser.Scene,
initialize:
function BootScene ()
{
Phaser.Scene.call(this, { key: 'BootScene' });
},
preload: function ()
{
// здесь будет загрузка ресурсов
},
create: function ()
{
this.scene.start('WorldScene');
}
});
var WorldScene = new Phaser.Class({
Extends: Phaser.Scene,
initialize:
function WorldScene ()
{
Phaser.Scene.call(this, { key: 'WorldScene' });
},
preload: function ()
{
},
create: function ()
{
// здесь мы создадим сцену мира
}
});
var config = {
type: Phaser.AUTO,
parent: 'content',
width: 320,
height: 240,
zoom: 2,
pixelArt: true,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 }
}
},
scene: [
BootScene,
WorldScene
]
};
var game = new Phaser.Game(config);
Обратите внимание на параметр pixelArt
при значении true
при масштабировании он предотвратит размытие текстур. Мы будем использовать его вместе с параметром zoom
, чтобы увеличить масштаб игры. В объекте config
мы указали Phaser включить по умолчанию аркадную физику, что поможет нам перемещать персонаж.
В конце у нас есть параметр scene
, в котором перечислены все сцены. На данный момент обе сцены выглядят практически одинаково. Единственное отличие заключается в методе create
в сцене BootScene, из которого мы вызываем сцену WorldScene:
this.scene.start('WorldScene');
Загрузка ресурсов в Phaser 3 очень проста - вам просто нужно добавить все необходимые ресурсы в загрузчик. Добавьте этот код в функцию preload
для сцены BootScene:
// тайлы для карты
this.load.image('tiles', 'assets/map/spritesheet.png');
// карта в json формате
this.load.tilemapTiledJSON('map', 'assets/map/map.json');
// наши два персонажа (я лично увидел 4-х персонажей)
this.load.spritesheet('player', 'assets/RPG_assets.png', { frameWidth: 16, frameHeight: 16 });
Теперь мы создадим нашу сцену мира с загруженной картой. Это произойдет в методе create
сцены WorldScene:
var map = this.make.tilemap({ key: 'map' });
Параметр key
- это имя, которое мы дали нашей карте, когда мы использовали метод this.load.tilemapTiledJSON
для ее загрузки.
Теперь, если вы обновите игру, она все еще имеет вид черного прямоугольника. Чтобы карта была в игре, нам нужно загрузить слои карты.
Карта для этого примера создана с помощью Tiled Editor. Чтобы следовать за руководством, вы можете использовать карту, которая есть в исходных файлах, или создать свою собственную карту. В исходниках представлена простая карта, состоящая только из двух слоев - первый называется «Grass» (Трава) и содержит только тайлы с травой, а второй - «Obstacles» (Препятствия) и на нем расположено несколько деревьев. Вот как вы добавляете их в игру. Добавьте этот код в конец функции create
для WorldScene:
var tiles = map.addTilesetImage('spritesheet', 'tiles');
var grass = map.createStaticLayer('Grass', tiles, 0, 0);
var obstacles = map.createStaticLayer('Obstacles', tiles, 0, 0);
obstacles.setCollisionByExclusion([-1]);
Первая строка создает изображение из тайлов для карты. Следующие две строки добавляют слои на карту. Последняя строка - метод setCollisionByExclusion
устанавливает определение столкновений со всеми тайлами в этом слое, за исключением тех, индексы, которых указаны в передаваемом массиве. Отправка массива с индексом -1, как указано в нашем случае, делает, чтобы все тайлы определялись при столкновении на этом слое.
Если вы сейчас запустите игру в своем браузере, у вас должно быть что-то вроде этого:
Пришло время добавить в игру спрайт персонажа. Добавьте этот код в конец метода create
для WorldScene:
this.player = this.physics.add.sprite(50, 100, 'player', 6);
Первый параметр - это координата x, второй - y, третий - ресурс изображения, а последний - его кадр.
Для перемещения по нашей карте мира мы будем использовать аркадную физику. Чтобы можно было отследить столкновения игрока с препятствиями на карте, мы создадим его с помощью этой системы физики - this.physics.add.sprite
.
Добавьте еще три строки:
this.physics.world.bounds.width = map.widthInPixels;
this.physics.world.bounds.height = map.heightInPixels;
this.player.setCollideWorldBounds(true);
Это заставит игрока оставаться в границах карты. Сначала мы устанавливаем границы мира, а затем устанавливаем для свойства персонажа collideWorldBounds
значение true.
Пришло время заставить игрока двигаться по карте. Для этого нам нужно обработать ввод данных с клавиатуры пользователя. В этой игре мы будем использовать клавиши со стрелками.
Добавьте этот код в конце метода create
:
this.cursors = this.input.keyboard.createCursorKeys();
Для перемещения игрока мы будем использовать физический движок. Мы установим скорость тела спрайта в соответствии с направлением движения. Теперь нам нужно добавить метод update
в WorldScene и добавим туда логику движения игрока.
Вот как должен выглядеть ваш метод update
:
update: function (time, delta)
{
this.player.body.setVelocity(0);
// горизонтальное перемещение
if (this.cursors.left.isDown)
{
this.player.body.setVelocityX(-80);
}
else if (this.cursors.right.isDown)
{
this.player.body.setVelocityX(80);
}
// вертикальное перемещение
if (this.cursors.up.isDown)
{
this.player.body.setVelocityY(-80);
}
else if (this.cursors.down.isDown)
{
this.player.body.setVelocityY(80);
}
}
Сначала мы сбрасываем скорость тела в 0. Затем в зависимости от нажатой клавиши , мы устанавливаем скорость для x или для y. Вы можете проверить движение персонажа в вашем браузере.
Наш игрок может двигаться, но камера не следует за ним. Чтобы заставить камеру следовать за спрайтом персонажа, нам нужно вызвать ее метод startFollow
.
Добавьте этот код в конец метода create
WorldScene:
// ограничиваем камеру размерами карты
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
// заставляем камеру следовать за игроком
this.cameras.main.startFollow(this.player);
//своего рода хак, чтобы предотвратить пояление полос в тайлах
this.cameras.main.roundPixels = true;
Первая строка ограничивает камеру размерами карты. Вторая заставляет камеру следовать за игроком.
Третья строка this.cameras.main.roundPixels = true;
что-то вроде хака, чтобы предотвратить появление полос между тайлами.
Опять попробуйте запустить игру в браузере. Как видите, движения игрока выглядят очень скучно, так как нет анимации ходьбы, поэтому нам нужно оживить персонажа. Анимации в Phaser3 выполняются с помощью Animation Manager (Менеджер анимации). Вот как мы добавим анимацию нашему персонажу.
Чтобы задать анимацию, добавьте этот код в метод create
WorldScene:
// анимация клавиши 'left' для персонажа
// мы используем одни и те же спрайты для левой и правой клавиши, просто зеркалим их
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('player', { frames: [1, 7, 1, 13]}),
frameRate: 10,
repeat: -1
});
// анимация клавиши 'right' для персонажа
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('player', { frames: [1, 7, 1, 13] }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'up',
frames: this.anims.generateFrameNumbers('player', { frames: [2, 8, 2, 14]}),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'down',
frames: this.anims.generateFrameNumbers('player', { frames: [ 0, 6, 0, 12 ] }),
frameRate: 10,
repeat: -1
});
Приведенный выше код определяет набор анимаций для клавиш, по одной для каждого направления. В нашем случае нам не нужны отдельные анимации для левой и правой стороны, так как мы просто перевернем спрайт и будем использовать одни и те же кадры.
В методе update
вам нужно переключиться на правильную анимацию. Добавьте этот код в конце метода:
// В конце обновляем анимацию и устанавливаем приоритет анимации
// left/right над анимацией up/down
if (this.cursors.left.isDown)
{
this.player.anims.play('left', true);
this.player.flipX = true; //Разворачиваем спрайты персонажа вдоль оси X
}
else if (this.cursors.right.isDown)
{
this.player.anims.play('right', true);
this.player.flipX = false; //Отменяем разворот спрайтов персонажа вдоль оси X
}
else if (this.cursors.up.isDown)
{
this.player.anims.play('up', true);
}
else if (this.cursors.down.isDown)
{
this.player.anims.play('down', true);
}
else
{
this.player.anims.stop();
}
Теперь, если все правильно, у вас должна работать хорошая анимация ходьбы в каждом направлении. Но наш игрок ходит через деревья и препятствия, а нам нужно, чтобы он сталкивался с тайлами на слое препятствий. Для этого добавьте эту строку в конце метода create
WorldScene:
this.physics.add.collider(this.player, obstacles);
Эта строчка создает физический объект коллайдера и принимает два параметра. В нашем случае: спрайта и слой с тайлами карты. Напоминаем, что мы сделали чтобы все тайлы из слоя obstacles
определялись при столкновении, вызвав ранее obstacles.setCollisionByExclusion([-1]);
.
Теперь, если вы запустите игру, вы увидите, что игрок больше не может проходить сквозь препятствия и должен огибать их.
Пришло время подумать, как игрок встретит врагов. Для размещения врагов мы будем использовать группу объектов зоны (Phaser.GameObjects.Zone
). Когда персонаж сталкивается с объектом зоны, начинается битва.
Phaser.GameObjects.Zone
- это невидимый объект, чтобы увидеть его во время разработки, вы можете установить debug: true
следующим образом:
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: true
}
},
Мы создадим 30 зон в группе physics
и будем использовать эту группу для проверки столкновений с персонажем.
Добавьте этот код в конце метода create
WorldScene:
this.spawns = this.physics.add.group({ classType: Phaser.GameObjects.Zone });
for(var i = 0; i < 30; i++) {
var x = Phaser.Math.RND.between(0, this.physics.world.bounds.width);
var y = Phaser.Math.RND.between(0, this.physics.world.bounds.height);
// параметры: x, y, width, height
this.spawns.create(x, y, 20, 20);
}
this.physics.add.overlap(this.player, this.spawns, this.onMeetEnemy, false, this);
В последней строке мы заставляем игрока взаимодействовать с нашими зонами. Когда персонаж пересекается с одной из зон, вызывается метод onMeetEnemy
. Теперь нам нужно добавить этот метод в WorldScene.
onMeetEnemy: function(player, zone) {
// начало боя
},
Во второй части этого урока отсюда мы будем вызывать сцену боя. На данный момент наша функция onMeetEnemy
будет проще. Мы переместим зону в другое случайное место. Для выбора случайной координаты мы будем использовать Phaser.Math.RND.between(min, max)
.
Добавьте строчки в метод onMeetEnemy
.
onMeetEnemy: function(player, zone) {
// мы перемещаем зону в другое место
zone.x = Phaser.Math.RND.between(0, this.physics.world.bounds.width);
zone.y = Phaser.Math.RND.between(0, this.physics.world.bounds.height);
// начало боя
},
Мы не будем уничтожать зону, а переместим ее в другое случайное место. Чтобы битва стала более пугающей, добавим что-то классное - эффект встряски.
Эффект встряхивания в Phaser 3 можно добавить через камеру - camera.shake(продолжительность)
.
onMeetEnemy: function(player, zone) {
// мы перемещаем зону в другое место
zone.x = Phaser.Math.RND.between(0, this.physics.world.bounds.width);
zone.y = Phaser.Math.RND.between(0, this.physics.world.bounds.height);
// встряхиваем мир
this.cameras.main.shake(300);
// начало боя
},
Вы можете немного поиграть со значением, чтобы изменить длительность этого эффекта. В качестве упражнения вы можете попробовать изменить эффект на вспышку или исчезновение.
this.cameras.main.flash(300);
this.cameras.main.fade(300);
И на этом первая часть урока закончена. Во второй части мы создадим нашу сцену боя и сцену пользовательского интерфейса и переключимся на них, когда встретим врага. Когда битва закончится, мы вернемся к нашей сцене WorldScene.