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

Создание изометрической проекции — популярный подход в играх, от стратегий до RPG. Однако корректное отображение объектов, чтобы дальние блоки не перекрывали ближние, требует правильной сортировки по глубине (depth sorting). В этой статье мы разберем пример из официальной коллекции Phaser, который не только строит изометрическую сетку, но и анимирует её с сохранением правильного порядка отрисовки. Вы научитесь управлять глубиной спрайтов и создавать динамичные изометрические сцены.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('isoblocks', 'assets/atlas/isoblocks.png', 'assets/atlas/isoblocks.json');
    }

    create ()
    {
        var frames = this.textures.get('isoblocks').getFrameNames();

        //  blocks are 50x50

        var mapWidth = 40;
        var mapHeight = 40;

        var tileWidthHalf = 20;
        var tileHeightHalf = 12;

        var centerX = (mapWidth / 2) * tileWidthHalf;
        var centerY = -100;

        var blocks = [];

        for (var y = 0; y < mapHeight; y++)
        {
            for (var x = 0; x < mapWidth; x++)
            {
                var tx = (x - y) * tileWidthHalf;
                var ty = (x + y) * tileHeightHalf;

                var block = (x % 2 === 0) ? 'block-123' : 'block-132';

                var tile = this.add.image(centerX + tx, centerY + ty, 'isoblocks', block);

                tile.setData('row', x);
                tile.setData('col', y);

                tile.setDepth(centerY + ty);

                blocks.push(tile);
            }
        }

        this.tweens.add({

            targets: blocks,

            x: function (target, key, value) {

                return (value - (30 - (target.getData('col')) * 4));

            },

            y: function (target, key, value) {

                return (value - (target.getData('row') * 5));

            },

            yoyo: true,
            ease: 'Sine.easeInOut',
            repeat: -1,
            duration: 700,
            delay: function (target, key, value, targetIndex, totalTargets, tween) {

                return (target.getData('row') * 60) + (target.getData('col') * 60);

            }
        });

        const cursors = this.input.keyboard.createCursorKeys();

        const controlConfig = {
            camera: this.cameras.main,
            left: cursors.left,
            right: cursors.right,
            zoomIn: cursors.up,
            zoomOut: cursors.down,
            acceleration: 0.04,
            drag: 0.0005,
            maxSpeed: 0.7
        };

        this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

        this.cameras.main.zoom = 0.62;
        this.cameras.main.scrollX = -110;
    }

    update (time, delta)
    {
        this.controls.update(delta);
    }
}

const config = {
    type: Phaser.WEBGL,
    width: 1024,
    height: 768,
    backgroundColor: '#0d0d0d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

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

Класс Example расширяет Phaser.Scene. В методе preload загружается атлас текстур isoblocks. Атлас — это единое изображение, содержащее несколько отдельных спрайтов (фреймов), и JSON-файл с координатами этих фреймов. Такой подход оптимизирует загрузку и отрисовку.

this.load.atlas('isoblocks', 'assets/atlas/isoblocks.png', 'assets/atlas/isoblocks.json');

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

Математика изометрической проекции

Ключ к изометрии — преобразование координат сетки (x, y) в экранные координаты (tx, ty). В примере используется стандартная формула для проекции "ромбиком" (diamond-top).

var tx = (x - y) * tileWidthHalf;
var ty = (x + y) * tileHeightHalf;

Здесь tileWidthHalf и tileHeightHalf — это половины ширины и высоты базового тайла. Умножение на эти значения превращает шаги по сетке в корректные смещения на экране. Переменные centerX и centerY смещают всю сгенерированную карту относительно центра камеры.

Блоки выбираются по чередующемуся шаблону для создания шахматного узора.

var block = (x % 2 === 0) ? 'block-123' : 'block-132';

Управление глубиной спрайтов

Самая важная часть для корректного отображения — установка глубины (z-index) каждого спрайта. В изометрии объекты, находящиеся "выше" (имеющие большую сумму координат x+y), должны перекрывать те, что "ниже".

tile.setDepth(centerY + ty);

Глубина задается значением координаты ty (с учетом смещения centerY). Поскольку ty увеличивается при движении вниз и вправо по сетке, блоки, которые должны быть визуально дальше (ниже на экране), получают большее значение глубины и, следовательно, отрисовываются раньше. Более близкие блоки (с меньшим ty) отрисовываются позже и перекрывают их. Это гарантирует, что визуальный порядок всегда будет правильным. Каждому блоку также записываются в данные (setData) его исходные координаты в сетке для последующей анимации.

Сложная анимация с Tween

Пример оживляет сцену с помощью твина, который применяется ко всем блокам одновременно. Особенность в том, что конечные значения `xиy` для каждого блока вычисляются динамически на основе его позиции в сетке.

x: function (target, key, value) {
    return (value - (30 - (target.getData('col')) * 4));
},

Функции-геттеры для `xиyиспользуют сохраненные ранее данные (rowиcol), чтобы сместить каждый блок на уникальное расстояние. Это создает эффект "волны" или перестроения сетки. Дополнительный параметрdelayтакже рассчитывается на основе координат, что приводит к последовательному, каскадному запуску анимации для разных блоков. Параметрyoyo: trueиrepeat: -1` заставляют анимацию непрерывно повторяться туда-обратно.

Управление камерой

Чтобы можно было рассмотреть всю большую сцену, реализовано плавное управление камерой с клавиатуры с помощью Phaser.Cameras.Controls.SmoothedKeyControl.

const controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    zoomIn: cursors.up,
    zoomOut: cursors.down,
    acceleration: 0.04,
    drag: 0.0005,
    maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

Этот контроллер предоставляет инерционное движение и зум. В методе update необходимо вызывать this.controls.update(delta), чтобы обработка управления работала каждый кадр. Камера также инициализируется с небольшим зумом и смещением для лучшего стартового обзора.

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

Пример наглядно демонстрирует два ключевых аспекта работы с изометрией в Phaser: преобразование координат и обязательную ручную сортировку глубины через setDepth(). Полученную сцену можно развивать: добавить статичный ландшафт и динамичные объекты (персонажей), сортируя их в едином порядке; реализовать укладку блоков разной высоты, учитывая её в формуле глубины; или применить шейдер для сложных визуальных эффектов на всей сетке.