О чем этот пример

Механика пошагового перемещения по сетке — основа для классических RPG, головоломок и тактических игр. Этот пример показывает, как связать управление клавишами WASD с проверкой тайлов, чтобы персонаж двигался дискретными шагами и реагировал на препятствия. Вы научитесь работать с тайловыми картами, определять проходимость клеток и управлять движением спрайта, что станет отличной базой для вашей казуальной или ролевой игры.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('tiles', 'assets/tilemaps/tiles/drawtiles-spaced.png');
        this.load.image('car', 'assets/sprites/car90.png');
        this.load.tilemapCSV('map', 'assets/tilemaps/csv/grid.csv');
    }

    create ()
    {
        const map = this.make.tilemap({ key: 'map', tileWidth: 32, tileHeight: 32 });
        const tileset = map.addTilesetImage('tiles', null, 32, 32, 1, 2);
        const layer = map.createLayer(0, tileset, 0, 0);

        const player = this.add.image(32 + 16, 32 + 16, 'car');

        //  Left
        this.input.keyboard.on('keydown-A', event =>
        {

            const tile = layer.getTileAtWorldXY(player.x - 32, player.y, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                player.x -= 32;
                player.angle = 180;
            }

        });

        //  Right
        this.input.keyboard.on('keydown-D', event =>
        {

            const tile = layer.getTileAtWorldXY(player.x + 32, player.y, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                player.x += 32;
                player.angle = 0;
            }

        });

        //  Up
        this.input.keyboard.on('keydown-W', event =>
        {

            const tile = layer.getTileAtWorldXY(player.x, player.y - 32, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                player.y -= 32;
                player.angle = -90;
            }

        });

        //  Down
        this.input.keyboard.on('keydown-S', event =>
        {

            const tile = layer.getTileAtWorldXY(player.x, player.y + 32, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                player.y += 32;
                player.angle = 90;
            }

        });

        this.add.text(8, 8, 'Move with WASD', {
            fontSize: '18px',
            fill: '#ffffff',
            backgroundColor: '#000000'
        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    pixelArt: true,
    backgroundColor: '#1a1a2d',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и загрузка данных

В методе preload загружаются необходимые ресурсы: изображение тайлсета, спрайт машины и сама карта в формате CSV. Важно, что для карты используется load.tilemapCSV. Этот формат прост для чтения и отладки — каждая строка файла соответствует ряду тайлов на карте.

this.load.image('tiles', 'assets/tilemaps/tiles/drawtiles-spaced.png');
this.load.image('car', 'assets/sprites/car90.png');
this.load.tilemapCSV('map', 'assets/tilemaps/csv/grid.csv');

В create мы создаём саму тайловую карту. Ключевые параметры: tileWidth и tileHeight указывают размер одного тайла в пикселях. Затем создаётся тайлсет и слой. Обратите внимание на дополнительные параметры addTilesetImage: они учитывают отступы (margin и spacing) в изображении тайлсета.

const map = this.make.tilemap({ key: 'map', tileWidth: 32, tileHeight: 32 });
const tileset = map.addTilesetImage('tiles', null, 32, 32, 1, 2);
const layer = map.createLayer(0, tileset, 0, 0);

Игрок (спрайт машины) создаётся с помощью this.add.image. Его начальные координаты рассчитаны так, чтобы он оказался по центру первой клетки сетки: 32 + 16. Здесь 32 — это размер тайла, а 16 — его половина, что и даёт центр.

Обработка ввода и логика движения

Движение привязано к клавишам WASD через слушатели keydown. Каждый обработчик проверяет, можно ли сделать шаг в заданном направлении, и если да — перемещает спрайт и поворачивает его.

Логика едина для всех направлений: 1. Рассчитываются мировые координаты клетки, в которую игрок хочет перейти. 2. С помощью layer.getTileAtWorldXY получаем объект тайла по этим координатам. Третий параметр true означает, что координаты именно мировые, а не тайловые индексы. 3. Проверяется свойство tile.index. В этом примере индекс `2` соответствует непроходимому тайлу (например, стене). 4. Если тайл проходим, координаты спрайта изменяются на 32 пикселя (ровно на один шаг сетки), и его угол поворота обновляется для визуальной обратной связи.

Вот обработчик для движения влево (клавиша A):

this.input.keyboard.on('keydown-A', event => {
    const tile = layer.getTileAtWorldXY(player.x - 32, player.y, true);
    if (tile.index === 2) {
        //  Заблокировано, движение невозможно
    } else {
        player.x -= 32;
        player.angle = 180;
    }
});

Обработчики для клавиш D (вправо), W (вверх) и S (вниз) построены по аналогии, меняя только координаты для проверки и значение player.angle.

Определение проходимости тайлов

Сердцевина механики — проверка tile.index === 2. Это жёстко закодированное условие, где `2` — индекс непроходимого тайла в используемом тайлсете. В вашей игре логика может быть сложнее.

if (tile.index === 2) {
    //  Движение заблокировано
}

Вот как можно улучшить этот подход: * **Использовать свойства тайлов (Tile Properties):** В редакторах карт (Tiled, Ogmo) вы можете назначать тайлам пользовательские свойства, например, { "passable": false }. В Phaser к ним можно обратиться через tile.properties.passable. * **Маска проходимости:** Создать отдельный массив или использовать определённый слой карты, который хранит только данные о проходимости. * **Несколько типов препятствий:** Проверять tile.index на вхождение в массив заблокированных индексов: [2, 5, 7].includes(tile.index).

Такой подход отделяет логику игры от визуального представления, что делает код гибче.

Конфигурация игры и настройки

Конфигурационный объект config задаёт базовые параметры игры. Для пиксель-арт графики важен параметр pixelArt: true, который отключает сглаживание. backgroundColor задаёт цвет фона.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    pixelArt: true,
    backgroundColor: '#1a1a2d',
    scene: Example
};

Инициализация игры происходит стандартным образом: new Phaser.Game(config). Вся логика, описанная выше, находится в классе сцены Example, который передан в конфигурацию.

Что попробовать дальше

Вы реализовали базовую систему пошагового движения по сетке с проверкой коллизий на тайловой карте. Это фундамент для множества жанров. Для экспериментов попробуйте: добавить анимацию плавного перемещения между клетками, реализовать систему ходов (один ход — одно действие), интегрировать более сложную логику проходимости из редактора Tiled или создать генератор случайных карт, где непроходимые тайлы расставляются алгоритмически.