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

Одна из частых задач в 2D-играх — корректное отображение объектов по глубине (z-index), чтобы элементы, находящиеся "ближе" к игроку, перекрывали те, что "дальше". Phaser предоставляет несколько способов управления глубиной. В этой статье мы разберем пример использования `Layer` — специального контейнера, который позволяет централизованно управлять видимостью и, что самое важное, автоматически сортировать своих дочерних объектов по свойству `depth`. Мы увидим, как это работает на практике с движущимися спрайтами.

Версия 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('mushroom', 'assets/sprites/mushroom2.png');
    }

    create ()
    {
        const layer = this.add.layer();

        console.log(layer);

        for (let i = 0; i < 1024; i++)
        {
            let x = Phaser.Math.Between(100, 700);
            let y = Phaser.Math.Between(100, 500);
            let f = Phaser.Math.Between(1, 9);

            const image = layer.add(this.make.image({ x, y, key: 'atlas', frame: 'veg0' + f }, false));

            image.depth = image.y;
        }

        this.mushroom0 = layer.add(this.make.image({ x: 400, y: 300, key: 'mushroom' }, false));
        this.mushroom1 = layer.add(this.make.image({ x: 400, y: 300, key: 'mushroom' }, false));
        this.mushroom2 = layer.add(this.make.image({ x: 400, y: 300, key: 'mushroom' }, false));

        this.input.on('pointerdown', () => {

            layer.visible = !layer.visible;

        });
    }

    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.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Что такое Layer и зачем он нужен

Layer в Phaser — это игровой объект-контейнер, который может включать в себя другие объекты (спрайты, изображения, текст). Его ключевые особенности: - Он сам является дочерним элементом Scene и управляет списком своих детей. - Позволяет применять действия (например, скрытие, перемещение) сразу ко всем своим дочерним объектам. - Может автоматически сортировать своих детей по значению свойства depth каждого объекта. Эта сортировка происходит на каждом кадре (preRender), что критически важно для динамически движущихся объектов.

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

Создание Layer и наполнение его объектами

В методе create() сцены создается экземпляр Layer. Затем в цикле создается множество изображений овощей из атласа и добавляются в этот слой.

const layer = this.add.layer();

for (let i = 0; i < 1024; i++) {
    let x = Phaser.Math.Between(100, 700);
    let y = Phaser.Math.Between(100, 500);
    let f = Phaser.Math.Between(1, 9);

    const image = layer.add(this.make.image({ x, y, key: 'atlas', frame: 'veg0' + f }, false));
    image.depth = image.y;
}

Обратите внимание на два важных момента: 1. Объект создается через фабрику this.make.image, но на сцену он попадает не через this.add, а через метод layer.add(). Второй аргумент false означает, что объект не должен добавляться в список обновления сцены, так как обновлением будет заниматься слой. 2. Сразу после создания объекту устанавливается свойство depth. В данном случае оно равно координате `y. Это основа для сортировки: объекты, расположенные ниже на экране (с большим значениемy), будут иметь большуюdepthи, следовательно, будут отрисованы поверх объектов с меньшейdepth` (если сорт включен).

Затем в этот же слой добавляются три спрайта гриба.

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

Сортировка внутри Layer работает, только если у его дочерних объектов меняется свойство depth. В статичной сцене достаточно установить depth один раз при создании. Но если объекты движутся, их глубина должна пересчитываться каждый кадр.

В методе update() примера три гриба движутся по разным траекториям. После расчета новых координат (`x,y) для каждого гриба немедленно пересчитывается егоdepth`.

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;

Здесь используется формула глубина = y + halfHeight. Зачем прибавляется половина высоты? Это небольшой, но важный трюк. Поскольку точка `yспрайта — это его верхний левый угол, а для восприятия глубины важна его середина (или "ноги"), добавлениеheight / 2` делает сортировку более естественной. Без этого грибы начинали бы перекрывать друг друга в моменты, когда их верхние края находятся на одной высоте, что выглядело бы некорректно.

Слой автоматически, на каждом кадре, перед отрисовкой отсортирует все 1024 овоща и 3 гриба по их актуальному значению depth.

Управление видимостью всего слоя

Одно из преимуществ использования Layer — возможность массовых операций. В примере показано, как по клику мыши можно скрыть или показать сразу все объекты слоя.

this.input.on('pointerdown', () => {
    layer.visible = !layer.visible;
});

Установка свойства visible в false для слоя делает невидимыми все его дочерние объекты, независимо от их индивидуальных настроек видимости. Это гораздо эффективнее, чем перебирать все объекты вручную.

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

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