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

При работе с контейнерами (`Container`) в Phaser разработчики часто используют метод `getBounds()` для получения габаритов объекта. Однако вложенность контейнеров может привести к неожиданным результатам. Эта статья разбирает пример, демонстрирующий тонкость вычисления границ у контейнеров после изменения их позиции, и объясняет, как избежать ошибок при позиционировании сложных иерархий объектов.

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

Живой запуск

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

Исходный код


var config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: {
        create: create
    }
};

var game = new Phaser.Game(config);

function create ()
{
  // Create a test container
  var parentContainer = this.add.container(0,0);

  // Create a nested container
  var nestedContainer = this.add.container(0,0);

  // Create a child object
  var testObject = this.add.rectangle(0, 0, 20, 20).setOrigin(0);

  // Add object to nested container and this to the parent
  nestedContainer.add(testObject);
  parentContainer.add(nestedContainer);

  // At this point the containter is at 0x0 with a height and width of 20.
  console.log(parentContainer.getBounds());

  parentContainer.setX(100);
  parentContainer.setY(100);

  // Ths container should have moved, but not changed in size.
  console.log(parentContainer.getBounds());
}

Разбираем пример: иерархия контейнеров

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

Сначала инициализируется игра с конфигурацией, указывающей функцию create как точку создания сцены.

var config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: {
        create: create
    }
};

var game = new Phaser.Game(config);

Создание иерархии объектов

В функции create строится древовидная структура: родительский контейнер, внутри него — вложенный контейнер, а в том — прямоугольник (Rectangle). Важно отметить, что прямоугольник создается с координатами (0,0) относительно своего родителя и сбросом точки трансформации (setOrigin(0)).

function create ()
{
  // Create a test container
  var parentContainer = this.add.container(0,0);

  // Create a nested container
  var nestedContainer = this.add.container(0,0);

  // Create a child object
  var testObject = this.add.rectangle(0, 0, 20, 20).setOrigin(0);

  // Add object to nested container and this to the parent
  nestedContainer.add(testObject);
  parentContainer.add(nestedContainer);

Поведение метода `getBounds()`

Метод container.getBounds() вычисляет габаритный прямоугольник (bounding box), который включает всех видимых потомков контейнера. При первом вызове, когда вся иерархия находится в точке (0,0), метод корректно возвращает прямоугольник размером 20x20.

// At this point the containter is at 0x0 with a height and width of 20.
  console.log(parentContainer.getBounds());

Затем родительский контейнер перемещается в точку (100,100) с помощью методов setX и setY. Логично ожидать, что границы теперь будут иметь те же размеры (20x20), но смещенные координаты. Однако в этом примере демонстрируется проблема (баг #4644), из-за которой getBounds() может не учесть трансформации родителя при определенных условиях, если вычисления были кэшированы или произведены до перемещения. Второй вызов console.log призван это проверить.

parentContainer.setX(100);
  parentContainer.setY(100);

  // Ths container should have moved, but not changed in size.
  console.log(parentContainer.getBounds());
}

На практике, если getBounds() возвращает старые значения, это может сломать логику, зависящую от актуального положения объектов (например, проверки выхода за пределы экрана, расчетов столкновений).

Практические рекомендации

Чтобы избежать проблем с актуальностью границ контейнера: 1. **Принудительный пересчет.** Если вы изменили позицию, масштаб или поворот контейнера и сразу нужны актуальные границы, можно попробовать сбросить кэш вычислений, вызвав container.updateBounds() перед getBounds(). Однако в данном конкретном примере бага это может не сработать, так как проблема глубже. 2. **Осознание вложенности.** Помните, что getBounds() для контейнера вычисляется относительно мировых координат (world), а не локальных. Изменение позиции любого родителя в цепочке влияет на мировые координаты всех потомков. 3. **Альтернативный подход.** Для проверки положения конкретного дисплейного объекта (например, прямоугольника testObject) иногда надежнее использовать его собственные свойства `x,yиdisplayWidth`/displayHeight, учитывая цепочку родителей.

// Получение мировых координат дочернего объекта
let worldX = testObject.x; // Локальные координаты относительно nestedContainer
let worldY = testObject.y;
// Для мировых координат может потребоваться учет матриц трансформации родителей

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

Работа с Container.getBounds() требует понимания, что результат может кэшироваться и не всегда мгновенно отражает изменения в иерархии объектов, особенно при наличии багов в конкретных версиях движка. Всегда проверяйте актуальность возвращаемых данных в отладчике. Для экспериментов попробуйте изменить пример: добавьте поворот (setRotation) контейнеру и снова вызовите getBounds(), или создайте более глубокую вложенность из 3-4 контейнеров, чтобы увидеть, как накапливается погрешность вычислений.