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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    sprite2;
    sprite1;

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

    create ()
    {
        this.sprite1 = this.add.image(600, 100, 'mushroom');
        this.physics.world.enable([ this.sprite1 ]);
        this.sprite1.body.setVelocity(-100, 200).setBounce(1, 1).setCollideWorldBounds(true);

        this.sprite2 = this.add.image(0, 0, 'mushroom');
        this.physics.world.enable([ this.sprite2 ]);
        this.sprite2.body.setVelocity(100, 200).setBounce(1, 1).setCollideWorldBounds(true);

        const container = this.add.container(200, 50, [ this.sprite2 ]);
    }

    update ()
    {
        this.physics.world.collide(this.sprite1, this.sprite2);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#010101',
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 200 },
            debug: true
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Что делает контейнер в Phaser?

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

Однако важно понимать, что сам контейнер по умолчанию не имеет физического тела. Физика в примере добавляется не к контейнеру, а к помещенному в него спрайту sprite2. Давайте посмотрим на ключевой код создания контейнера:

const container = this.add.container(200, 50, [ this.sprite2 ]);

Здесь контейнер создается в координатах (200, 50) на сцене, и в него сразу помещается массив дочерних объектов — в нашем случае это sprite2. Спрайт sprite2, будучи внутри контейнера, будет отсчитывать свои позиции относительно позиции контейнера.

Настройка физики Arcade для объектов

В примере используется физический движок Arcade. Он включается для отдельных спрайтов через метод this.physics.world.enable(). Этот метод добавляет физическое тело (body) к объекту, после чего с ним можно работать — задавать скорость, ускорение, отскок и проверять столкновения.

Обратите внимание: физика включается для спрайтов ДО их добавления в контейнер. Вот как это сделано для sprite2:

this.sprite2 = this.add.image(0, 0, 'mushroom');
this.physics.world.enable([ this.sprite2 ]);
this.sprite2.body.setVelocity(100, 200).setBounce(1, 1).setCollideWorldBounds(true);

Метод setVelocity() задает начальную скорость тела по осям X и Y. setBounce(1, 1) определяет коэффициент упругости (1 — идеальный отскок). setCollideWorldBounds(true) включает столкновения с границами игрового мира. Аналогичные настройки применены и к sprite1, который не помещен в контейнер и служит для сравнения.

Важный момент: гравитация, заданная в конфигурации (gravity: { y: 200 }), будет действовать на оба спрайта, так как они оба имеют физические тела.

Особенности столкновений и позиционирования

В методе update() происходит проверка столкновения между двумя спрайтами:

this.physics.world.collide(this.sprite1, this.sprite2);

Функция collide() движка Arcade автоматически обрабатывает столкновение двух физических тел, рассчитывая их отскок и изменение векторов скорости согласно их свойствам (массе, отскоку).

Но как насчет позиции sprite2, который находится внутри контейнера? Его локальные координаты внутри контейнера — (0, 0). Однако контейнер расположен в точке (200, 50) на сцене. Поэтому в мировых координатах (тех, которые использует физический движок для расчетов столкновений) позиция sprite2 будет (200, 50) плюс любые смещения, вызванные физикой. Движок Arcade корректно работает с мировыми координатами, поэтому столкновения рассчитываются правильно, несмотря на то что спрайт является дочерним объектом.

Если бы мы попытались включить физику для самого контейнера (через this.physics.world.enable(container)), это не сработало бы, так как контейнер по умолчанию не имеет свойства body, необходимого для движка Arcade. Физика всегда настраивается для дочерних спрайтов, имеющих текстуру.

Конфигурация игры и движка физики

Для корректной работы физики Arcade ее необходимо предварительно настроить в конфигурации игры (config). Это делается в объекте physics:

physics: {
    default: 'arcade',
    arcade: {
        gravity: { y: 200 },
        debug: true
    }
}

Параметр default: 'arcade' указывает, что по умолчанию будет использоваться именно этот физический движок. Внутри arcade задаются его настройки: gravity определяет вектор гравитации (в данном случае только по оси Y), а debug: true включает отладочный режим, при котором физические тела будут отображаться с контурами (hitboxes). Это крайне полезно при разработке для визуальной проверки столкновений.

Инициализация игры с этой конфигурацией происходит так:

const game = new Phaser.Game(config);

После этого движок Arcade автоматически обновляет состояния всех включенных физических тел в каждом кадре, применяя гравитацию, скорость и рассчитывая новые позиции.

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

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

  1. Добавить в контейнер несколько спрайтов с физикой и посмотреть, как они взаимодействуют друг с другом и с внешними объектами
  2. Анимировать позицию или вращение самого контейнера (container.x += 1) и наблюдать, как это влияет на траекторию движения дочерних физических тел
  3. Отключить debug-режим в конфигурации и добавить визуальные эффекты при столкновениях