Как создать пошаговую RPG в Phaser 3 (часть 1)

В этом учебном пособии мы сделаем пошаговую RPG, похожую на раннюю версию игры Final Fantasy, и научимся использовать множество новых интересных функций, встроенных в текущую версию игрового движка Phaser 3. В первой части этого урока мы создадим мир, добавим игрока и научим его перемещаться по карте. Затем мы добавим несколько невидимых зон, где игрок встретит врагов. Вторая часть этой серии поможет вам научиться создавать боевую систему и пользовательский интерфейс. Мы заставим наших юнитов сражаться с врагами!

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

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

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

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

Вы узнаете, как использовать некоторые интересные эффекты, такие как дрожание и затухание, когда персонаж встречает врагов до начала битвы.

  • Средний уровень знаний JavaScript;
  • Редактор кода;
  • Веб-браузер;
  • Локальный веб-сервер;
  • Assets (мультимедийные ресурсы) - карта в формате JSON и изображения (вы можете использовать те, которые идут с этим уроком)

    Все 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.

Оригинал