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

В динамичных 2D-играх камера часто движется, вращается и зумирует, следуя за игроком. Но элементы интерфейса (HUD), такие как шкалы здоровья или индикаторы, должны оставаться стабильными на экране. В этой статье разберем, как использовать объект `Stamp` в Phaser для создания HUD, который полностью игнорирует преобразования камеры и всегда отображается в одном месте экрана. Это практичный подход к разделению игрового мира и пользовательского интерфейса.

Версия 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('space3', 'assets/skies/space3.png');
        this.load.atlas('hud', 'assets/ui/dark-ui.png', 'assets/ui/dark-ui.json');
        this.load.atlas('atlas', 'assets/atlas/megaset-1.png', 'assets/atlas/megaset-1.json');
    }

    create ()
    {
        const halfWidth = this.scale.width / 2;
        const halfHeight = this.scale.height / 2;

        // Create a background image that won't move with the camera,
        // but will still rotate with perspective.
        this.add.image(halfWidth, halfHeight, 'space3').setScale(2).setScrollFactor(0.05);

        const total = 10;
        for (let i = 0; i < total; i++)
        {
            const depth = 1 / (total - i);
            this.add.image(Phaser.Math.Between(0, this.scale.width), Phaser.Math.Between(0, this.scale.height), 'atlas', 'titan-mech')
            .setScale(depth)
            .setScrollFactor(depth);
        }
      
        // Add a HUD using Stamps which don't react to Camera transforms at all.
        const barHeight = this.scale.height - 32;
        this.add.stamp(halfWidth, barHeight, 'hud', 'track-empty');
        this.bar = this.add.stamp(halfWidth, barHeight, 'hud', 'track-red');
    }

    update (time, delta)
    {
        this.bar.scaleX = Math.sin(time / 200);

        this.cameras.main
        .setScroll(Math.cos(time / 1000) * 400, Math.sin(time / 1000) * 200)
        .setRotation(Math.sin(time / 500) * 0.1)
        .setZoom(1 + Math.sin(time / 2000) * 0.1);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

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

Stamp — это специальный игровой объект в Phaser, который не подвержен влиянию преобразований камеры. В отличие от обычных Image или Sprite, которые могут двигаться, вращаться и масштабироваться вместе с камерой, Stamp всегда отображается в фиксированных координатах экрана. Это делает его идеальным для элементов HUD, которые должны быть постоянно видны игроку.

В исходном примере мы создаем фоновое изображение, которое немного движется с камерой (параллакс-эффект), и несколько спрайтов, которые полностью следуют за камерой. А для HUD используем Stamp, чтобы панель оставалась на месте.

Настройка сцены и загрузка ресурсов

В методе preload() загружаются необходимые ресурсы: фон, атлас для интерфейса и атлас для игровых объектов. Атласы позволяют эффективно хранить множество текстур в одном изображении.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('space3', 'assets/skies/space3.png');
    this.load.atlas('hud', 'assets/ui/dark-ui.png', 'assets/ui/dark-ui.json');
    this.load.atlas('atlas', 'assets/atlas/megaset-1.png', 'assets/atlas/megaset-1.json');
}

Создание игрового мира с параллакс-эффектом

В create() сначала добавляем фоновое изображение. Важный момент: мы устанавливаем setScrollFactor(0.05). Это означает, что фон будет двигаться с камерой, но только на 5% от её перемещения, создавая эффект глубины (параллакс).

Затем в цикле создаем несколько спрайтов мехов. Каждому присваивается свой depth (глубина), который влияет на масштаб и scrollFactor. Объекты с меньшим depth (дальние) меньше масштабированы и меньше двигаются с камерой, что усиливает ощущение 3D-пространства.

const total = 10;
for (let i = 0; i < total; i++)
{
    const depth = 1 / (total - i);
    this.add.image(Phaser.Math.Between(0, this.scale.width), Phaser.Math.Between(0, this.scale.height), 'atlas', 'titan-mech')
    .setScale(depth)
    .setScrollFactor(depth);
}

Создание статичного HUD с помощью Stamp

Здесь происходит ключевое действие. Мы создаем два объекта Stamp для HUD: пустую дорожку и заполняющую её красную полосу. Они добавляются методом this.add.stamp(). Координаты рассчитываются так, чтобы HUD был закреплен внизу экрана.

Поскольку это Stamp, они полностью игнорируют любые будущие преобразования камеры (прокрутку, вращение, зум) и всегда остаются на одном месте экрана. Красная полоса сохраняется в свойстве this.bar для дальнейшей анимации.

const barHeight = this.scale.height - 32;
this.add.stamp(halfWidth, barHeight, 'hud', 'track-empty');
this.bar = this.add.stamp(halfWidth, barHeight, 'hud', 'track-red');

Анимация камеры и HUD в update()

В методе update() происходят две ключевые анимации. Во-первых, масштаб красной полосы HUD по оси X (this.bar.scaleX) меняется по синусоиде, создавая эффект пульсации. Это демонстрирует, что Stamp можно анимировать независимо от камеры.

Во-вторых, камера активно преобразуется: она движется по кругу (setScroll), вращается (setRotation) и слегка зумирует (setZoom). Все эти изменения затрагивают фон и спрайты мехов, но абсолютно не влияют на Stamp-объекты HUD.

update (time, delta)
{
    this.bar.scaleX = Math.sin(time / 200);

    this.cameras.main
    .setScroll(Math.cos(time / 1000) * 400, Math.sin(time / 1000) * 200)
    .setRotation(Math.sin(time / 500) * 0.1)
    .setZoom(1 + Math.sin(time / 2000) * 0.1);
}

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

Использование Stamp — это чистый и эффективный способ создать неподвижный HUD в Phaser. Он полностью изолирован от хаотичного движения игровой камеры, что критически важно для удобства игрока. Для экспериментов попробуйте: создать сложный HUD из нескольких Stamp-объектов (иконки, кнопки), добавить им интерактивность через setInteractive, или использовать этот подход для статичных элементов меню паузы, которые должны появляться поверх всего.