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

Когда в игре множество объектов, они могут перекрывать друг друга в неправильном порядке, ломая иллюзию глубины. Phaser 3 предоставляет простой механизм управления порядком отрисовки через свойство `depth`. Эта статья покажет, как использовать его для создания эффекта псевдо-3D и корректного отображения слоев в 2D-играх.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
        this.move = 0;
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('atlas', 'assets/tests/fruit/veg.png', 'assets/tests/fruit/veg.json');
        this.load.image('image', 'assets/sprites/mushroom2.png');
    }

    create ()
    {
        for (let i = 0; i < 2000; i++)
        {
            const image = this.add.image(100 + Math.random() * 600, 100 + Math.random() * 400, 'atlas', 'veg0' + Math.floor(1 + Math.random() * 9));
            image.depth = image.y;
        }

        this.mushroom0 = this.add.image(400, 300, 'image');
        this.mushroom1 = this.add.image(400, 300, 'image');
        this.mushroom2 = this.add.image(400, 300, 'image');
    }

    update ()
    {
        this.mushroom0.x = 400 + Math.cos(this.move) * 200;
        this.mushroom0.y = 300 + Math.sin(this.move) * 200;
        this.mushroom0.depth = this.mushroom0.y + this.mushroom0.height / 2;

        this.mushroom1.x = 400 + Math.sin(-this.move) * 200;
        this.mushroom1.y = 300 + Math.cos(-this.move) * 200;
        this.mushroom1.depth = this.mushroom1.y + this.mushroom1.height / 2;

        this.mushroom2.y = 300 + Math.sin(this.move) * 180;
        this.mushroom2.depth = this.mushroom2.y + this.mushroom2.height / 2;

        this.move += 0.01;
    }
}

const config = {
    type: Phaser.WEBGL,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Что такое свойство `depth`?

В Phaser 3 каждый игровой объект (например, Image или Sprite) имеет свойство depth. Оно определяет порядок, в котором рендерер будет отрисовывать объекты на холсте или WebGL-контексте. Объекты с большим значением depth рисуются поверх объектов с меньшим значением.

По умолчанию объекты рисуются в порядке их добавления на сцену с помощью методов типа this.add.image(). Свойство depth позволяет переопределить этот порядок динамически.

image.depth = 100;

Статическая сортировка по оси Y

Классический прием в 2D-играх — сортировка объектов по их координате Y, чтобы создавать иллюзию глубины: объекты, которые находятся «ниже» на экране (имеют большую координату Y), должны перекрывать те, что «выше». В примере это реализовано для 2000 случайно размещенных спрайтов овощей.

В методе create() каждому созданному изображению присваивается depth, равный его координате `y`. В результате спрайты, расположенные ниже по вертикали, будут отрисовываться поверх тех, что выше, создавая естественный вид.

for (let i = 0; i < 2000; i++) {
    const image = this.add.image(100 + Math.random() * 600, 100 + Math.random() * 400, 'atlas', 'veg0' + Math.floor(1 + Math.random() * 9));
    image.depth = image.y;
}

Динамическое обновление глубины

Для объектов, которые движутся, значение depth необходимо обновлять каждый кадр, чтобы порядок отрисовки оставался корректным. В примере три гриба (mushroom0, mushroom1, mushroom2) движутся по разным траекториям.

Их глубина пересчитывается в методе update() на основе текущей позиции. К координате `yприбавляется половина высоты спрайта (height / 2`). Это смещает точку расчета глубины к центру спрайта, что часто дает более точный визуальный результат, особенно для объектов с высокой «ножкой» или смещенным якорем.

this.mushroom0.depth = this.mushroom0.y + this.mushroom0.height / 2;
this.mushroom1.depth = this.mushroom1.y + this.mushroom1.height / 2;
this.mushroom2.depth = this.mushroom2.y + this.mushroom2.height / 2;

Поведение объектов с одинаковой глубиной

Что произойдет, если несколько объектов получат одинаковое значение depth? Phaser сохранит для них исходный порядок добавления на сцену (order of creation). Однако в динамической сцене, где объекты постоянно движутся и их координаты Y могут совпадать, лучше добавлять небольшие поправки к расчету, чтобы избежать мерцания (z-fighting) и обеспечить стабильный порядок.

Например, можно добавить уникальный идентификатор, умноженный на очень малое число:

// Пример для массива объектов
objects.forEach((obj, index) => {
    obj.depth = obj.y + (index * 0.0001);
});

Производительность и оптимизация

Постоянное обновление depth для тысяч объектов в update() может быть затратным. В примере статические овощи получают глубину один раз в create(), так как они не двигаются. Для движущихся объектов обновление неизбежно.

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

Также помните, что сортировка по depth выполняется рендерером Phaser. Чем больше уникальных значений глубины, тем больше работы для рендерера. В некоторых случаях можно группировать объекты в «слои» с фиксированными значениями глубины (например, фон: 0, игрок: 100, эффекты: 200, UI: 1000).

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

Свойство depth — это мощный и гибкий инструмент для управления визуальной иерархией объектов в Phaser 3. Используйте его для создания иллюзии глубины в 2D-играх, корректного отображения частиц и эффектов, а также организации сложных UI-слоев. **Идеи для экспериментов:** 1. Создайте платформер, где игрок корректно проходит за и перед фоновыми объектами. 2. Реализуйте систему погоды (дождь, снег), где частицы имеют динамическую глубину относительно игрока. 3. Сымитируйте изометрическую проекцию, вычисляя глубину как сумму координат x + y.