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

Работа с контейнерами (`Container`) и камерами (`Camera`) — ключевой навык при создании сложных интерфейсов, эффектов и управления видимостью в играх на Phaser 3. Этот пример демонстрирует, как трансформации объекта-спрайта внутри контейнера взаимодействуют с параметрами камеры, и как эти изменения можно отслеживать в реальном времени. Понимание этой механики позволяет точно позиционировать элементы игрового мира, создавать сложные анимации и реализовывать продвинутые эффекты камеры. В статье мы подробно разберем исходный код примера, объясним, как работает цепочка преобразований (спрайт → контейнер → камера) и как использовать инструменты отладки для визуализации и контроля этих процессов. Это знание пригодится при разработке любых проектов, где требуется точный контроль над положением и видимостью объектов.

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

Живой запуск

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

Исходный код


class OverLayConfig extends Phaser.Scene
{
    constructor ()
    {
        super({ key: "overlay" })
    }

    createOverlay ()
    {
        this.graphics = this.add.graphics();
    }

    updateOverlay ()
    {
        this.graphics.clear();

        this.graphics.lineStyle(2, 0x00ff00, 1);

        // if (result)
        // {
        //     var tl = sprite.getTopLeft(null, true);
        //     var tr = sprite.getTopRight(null, true);
        //     var bl = sprite.getBottomLeft(null, true);
        //     var br = sprite.getBottomRight(null, true);

        //     // cam1.getWorldPoint(tl.x, tl.y, tl);
        //     // cam1.getWorldPoint(tr.x, tr.y, tr);
        //     // cam1.getWorldPoint(bl.x, bl.y, bl);
        //     // cam1.getWorldPoint(br.x, br.y, br);

        //     graphics.lineStyle(2, 0x00ff00, 1);
        //     graphics.lineBetween(tl.x, tl.y, tr.x, tr.y);
        //     graphics.lineBetween(tl.x, tl.y, bl.x, bl.y);
        //     graphics.lineBetween(tr.x, tr.y, br.x, br.y);
        //     graphics.lineBetween(bl.x, bl.y, br.x, br.y);

        //     graphics.fillStyle(0x00ff00, 1);
        //     graphics.fillRect(tl.x, tl.y, 6, 6);

        //     graphics.fillStyle(0xff0000, 1);
        //     graphics.fillRect(tr.x, tr.y, 6, 6);

        //     graphics.fillStyle(0xff00ff, 1);
        //     graphics.fillRect(bl.x, bl.y, 6, 6);

        //     graphics.fillStyle(0x0000ff, 1);
        //     graphics.fillRect(br.x, br.y, 6, 6);

        // }
    }
}


class Example extends Phaser.Scene
{

    constructor ()
    {
        super({ key: "main" })
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
        this.load.image('grid', 'assets/pics/debug-grid-1920x1920.png');
    }



    create ()
    {
        const cam1 = this.cameras.main.setName('Camera 1');

        this.add.image(0, 0, 'grid').setOrigin(0).setAlpha(0.5);

        const container = this.add.container(200, 100);

        const sprite = this.add.sprite(100, 100, 'eye').setInteractive();

        container.add(sprite);

        sprite.setScale(2, 1);
        container.setScale(2);
        sprite.setAngle(20);

        const text = this.add.text(10, 10, 'Click on sprite', { font: '16px Courier', fill: '#00ff00' });

        sprite.on('pointerdown', function () {
            this.input.enableDebug(sprite);
        }, this);

        const gui = new dat.GUI();

        const p1 = gui.addFolder('Pointer');
        p1.add(this.input, 'x').listen();
        p1.add(this.input, 'y').listen();
        p1.open();

        const c1 = gui.addFolder('Camera');
        c1.add(cam1, 'x').listen();
        c1.add(cam1, 'y').listen();
        c1.add(cam1, 'scrollX').listen();
        c1.add(cam1, 'scrollY').listen();
        c1.add(cam1, 'rotation').min(0).step(0.01).listen();
        c1.add(cam1, 'zoom', 0.1, 2).step(0.1).listen();
        c1.open();
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: [Example, OverLayConfig]
};

const game = new Phaser.Game(config);

Структура сцены и настройка сцены-оверлея

В примере используются две сцены: основная (Example) и сцена для оверлея (OverLayConfig). Сцена-оверлей задумана для отрисовки вспомогательной графики, например, рамки вокруг объекта, но в данном примере её функционал не активирован.

В основной сцене в методе preload загружаются два изображения: текстура для спрайта и фоновая сетка для отладки.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
    this.load.image('grid', 'assets/pics/debug-grid-1920x1920.png');
}

Метод create — это сердце примера. Здесь происходит вся инициализация. Сначала мы получаем ссылку на основную камеру, даём ей имя 'Camera 1' и добавляем фоновую сетку с полупрозрачностью.

Создание контейнера и работа с трансформациями

Контейнер (Container) — это специальный игровой объект, который группирует другие объекты, позволяя применять к ним общие трансформации (позицию, масштаб, поворот) как к единому целому.

const container = this.add.container(200, 100);
const sprite = this.add.sprite(100, 100, 'eye').setInteractive();
container.add(sprite);

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

Далее применяются трансформации к обоим объектам:

sprite.setScale(2, 1);
container.setScale(2);

Сначала спрайт растягивается по оси X в 2 раза. Затем весь контейнер (вместе со спрайтом внутри) также масштабируется в 2 раза. Эти преобразования складываются. Итоговый масштаб спрайта будет (2 * 2 = 4) по оси X и (1 * 2 = 2) по оси Y.

sprite.setAngle(20);

Спрайт внутри контейнера поворачивается на 20 градусов. Если бы мы повернули сам контейнер, повернулись бы все его дочерние элементы.

Отладка и визуализация с помощью dat.GUI

Одна из самых полезных частей примера — интеграция библиотеки dat.GUI для создания панели отладки. Она позволяет в реальном времени отслеживать и изменять параметры.

Панель разделена на две секции. В секции 'Pointer' выводятся текущие координаты курсора мыши (this.input.x, this.input.y). Эти значения обновляются автоматически благодаря методу .listen().

const p1 = gui.addFolder('Pointer');
p1.add(this.input, 'x').listen();

Секция 'Camera' гораздо информативнее. Здесь можно отслеживать и изменять ключевые параметры камеры cam1: - `x,y` — позиция камеры в мире. - scrollX, scrollY — смещение (скролл) камеры. - rotation — угол поворота камеры. - zoom — уровень масштабирования (зум) камеры.

c1.add(cam1, 'zoom', 0.1, 2).step(0.1).listen();

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

Взаимодействие и цепочка преобразований

Итоговое положение и вид спрайта на экране определяются цепочкой преобразований: 1. **Локальные трансформации спрайта** (позиция, масштаб, угол) внутри своего родителя — контейнера. 2. **Глобальные трансформации контейнера**, которые применяются ко всем его детям. 3. **Преобразования камеры** (позиция, зум, поворот), которые проецируют мировые координаты в координаты отображения (viewport).

Когда вы кликаете на спрайт, срабатывает обработчик события pointerdown. Он активирует встроенный режим отладки Phaser для этого спрайта с помощью this.input.enableDebug(sprite). На экране появится зелёная рамка с информацией о спрайте, что помогает визуализировать его реальные границы после всех преобразований.

sprite.on('pointerdown', function () {
    this.input.enableDebug(sprite);
}, this);

Задача закомментированного кода в сцене OverLayConfig — нарисовать поверх всего эту самую рамку и углы спрайта вручную, используя методы getTopLeft, getTopRight и другие. Эти методы возвращают мировые координаты углов спрайта, которые затем можно было бы преобразовать в координаты камеры с помощью cam1.getWorldPoint. Это низкоуровневый, но полный контроль за отрисовкой отладочной информации.

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

Этот пример наглядно показывает мощь системы трансформаций Phaser 3, построенной на принципах иерархии (контейнеры) и проекции (камеры). Понимая, как локальные преобразования объекта становятся мировыми и как камера их отображает, вы получаете полный контроль над визуальной частью игры. **Идеи для экспериментов:** 1. В панели dat.GUI добавьте управление масштабом и углом поворота самого контейнера. Сравните, как это влияет на спрайт внутри. 2. Раскомментируйте и доработайте код в updateOverlay, чтобы рисовать рамку вокруг спрайта, которая будет корректно отображаться при любом зуме и повороте камеры. 3. Создайте вторую камеру (this.cameras.add) и настройте её на отображение только определённой области. Попробуйте привязать к ней свой dat.GUI для управления двумя камерами одновременно.