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

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

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

Живой запуск

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

Исходный код


class MainScene extends Phaser.Scene
{

    constructor()
    {
        super({ key: "MainScene" });
    }

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('mushroom', 'assets/sprites/mushroom2.png');
        this.load.image('block', 'assets/sprites/block.png');
    }

    create ()
    {
        // render out the player health bar
        const playerMonsterName = this.add.text(30, 20, 'IGUANIGNITE', {
            color: '#7E3D3F',
            fontSize: '32px',
        });
        this.add.container(556, 318, [
            this.add.image(0, 0, BATTLE_ASSET_KEYS.HEALTH_BAR_BACKGROUND).setOrigin(0),
            playerMonsterName,
            this.add.text(30, 55, 'HP', { // this line
                color: '#FF6505',
                fontSize: '24px',
                fontStyle: 'italic',
            }),
            this.add.text(playerMonsterName.width + 35, 23, 'L5', {
                color: '#ED474B',
                fontSize: '28px',
            }),
            this.add
                .text(443, 80, '25/25', {
                    color: '#7E3D3F',
                    fontSize: '16px',
                })
                .setOrigin(1, 0),
        ]);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: 0xFFFFFF,
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    scene: [ MainScene ]
};

const game = new Phaser.Game(config);

Проблема: "уплывающий" текст

В данном примере создаётся сцена с контейнером, который должен отображать полоску здоровья и связанные с ней текстовые метки. Контейнер позиционируется в точке (556, 318). Внутрь него добавляются фон полоски здоровья (HEALTH_BAR_BACKGROUND) и несколько объектов текста.

Ключевая проблема заключается в том, что объект playerMonsterName создаётся и добавляется на сцену *до* создания контейнера, в точке (30, 20) относительно глобальных координат сцены. После этого тот же самый текстовый объект добавляется в массив дочерних элементов контейнера. Это приводит к конфликту систем координат. Текст начинает учитывать позицию контейнера, но его исходная позиция (30, 20) задана в мировых координатах, что даёт неверный финальный результат на экране.

Анализ кода: где скрывается ошибка

Давайте внимательно посмотрим на метод create. Первый текстовый объект создаётся с помощью this.add.text.

const playerMonsterName = this.add.text(30, 20, 'IGUANIGNITE', {
    color: '#7E3D3F',
    fontSize: '32px',
});

Этим вызовом объект сразу добавляется в дисплейный список сцены с координатами X=30, Y=20. Затем создаётся контейнер в точке (556, 318). Вторым элементом в его массив передаётся уже существующий объект playerMonsterName.

this.add.container(556, 318, [
    this.add.image(0, 0, BATTLE_ASSET_KEYS.HEALTH_BAR_BACKGROUND).setOrigin(0),
    playerMonsterName, // <-- Проблемный объект, уже добавленный на сцену
    this.add.text(30, 55, 'HP', {
        color: '#FF6505',
        fontSize: '24px',
        fontStyle: 'italic',
    }),
    // ... другие элементы
]);

В отличие от него, текстовый объект 'HP' создаётся непосредственно внутри вызова container. Его координаты (30, 55) будут корректно интерпретированы как локальные относительно контейнера, а не сцены.

Решение: единый источник координат

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

**Подход 1: Создавать все элементы внутри контейнера.** Это самый чистый и рекомендуемый способ. Координаты каждого text или image будут отсчитываться от левого верхнего угла контейнера.

create ()
{
    const containerX = 556;
    const containerY = 318;

    const container = this.add.container(containerX, containerY);

    // Все элементы создаются как дочерние для сцены, но сразу передаются в контейнер
    const background = this.add.image(0, 0, BATTLE_ASSET_KEYS.HEALTH_BAR_BACKGROUND).setOrigin(0);
    const playerMonsterName = this.add.text(30, 20, 'IGUANIGNITE', {
        color: '#7E3D3F',
        fontSize: '32px',
    });
    const hpLabel = this.add.text(30, 55, 'HP', {
        color: '#FF6505',
        fontSize: '24px',
        fontStyle: 'italic',
    });

    container.add([background, playerMonsterName, hpLabel]);
}

**Подход 2: Явно задавать позицию после добавления в контейнер.** Если объект уже был создан, можно сбросить его позицию после включения в контейнер, используя локальные координаты.

// ... playerMonsterName создан ранее ...
container.add(playerMonsterName);
playerMonsterName.setPosition(30, 20); // Теперь это локальные координаты контейнера

Важное правило работы с Container

Объект в Phaser может иметь только одного родителя в дисплейном списке. Когда вы передаёте уже существующий на сцене объект (как playerMonsterName) в метод container.add() или в конструктор контейнера, он автоматически удаляется со своего старого места в списке отображения сцены и становится дочерним элементом контейнера. Однако его свойства `xиy` при этом не сбрасываются и продолжают хранить старые значения в мировых координатах, что и приводит к визуальному "сдвигу".

Всегда помните: **координаты объектов внутри контейнера должны быть локальными относительно его позиции (0,0).** Если вам нужно преобразовать мировые координаты в локальные, можно использовать методы container.getLocalTransformMatrix() или просто пересчитывать позицию вручную, вычитая позицию контейнера.

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

Ошибка рендеринга текста в примере наглядно демонстрирует важность понимания иерархии и систем координат в Phaser. Контейнеры — это не просто визуальные группы, а полноценные родительские объекты, меняющие точку отсчёта для своих детей. Для экспериментов попробуйте

  1. Создать интерфейс с несколькими вложенными контейнерами и проследить цепочку координат
  2. Написать вспомогательную функцию, которая автоматически корректирует позицию существующего объекта при его добавлении в новый контейнер
  3. Использовать отладку, выводя в консоль мировые (`x,y) и локальные (getLocalPoint`) координаты проблемных элементов