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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    text;
    i = 0;
    hsv;
    group;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.spritesheet('invader', 'assets/tests/invaders/invader3.png', { frameWidth: 48, frameHeight: 32 });
    }

    create ()
    {
        this.hsv = Phaser.Display.Color.HSVColorWheel();

        //  Create some invaders
        this.group = this.add.group({ key: 'invader', frame: 0, repeat: 53 });

        //  And some debug text
        this.text = this.add.text(10, 10, 'Invaders: 54', { font: '16px Courier', fill: '#00ff00' });

        const invaders = this.group.getChildren();

        //  Align them in a grid
        Phaser.Actions.GridAlign(this.group.getChildren(), { width: 9, cellWidth: 58, cellHeight: 48, x: 132, y: 148 });

        //  On each click, kill a sprite
        this.input.on('pointerup', event =>
        {

            //  Get a random sprite from the local array
            const invader = Phaser.Utils.Array.RemoveRandomElement(invaders);

            if (invader)
            {
                //  Then destroy it. This will fire a 'destroy' event that the Group will hear
                //  and then it'll automatically remove itself from the Group.
                invader.destroy();
            }

            console.log(this.group.getChildren());

        }, this);
    }

    update ()
    {
        this.text.setText(`Invaders: ${this.group.getLength()}`);

        const tint = this.hsv[this.i];

        Phaser.Actions.SetTint(this.group.getChildren(), tint.color);

        this.i++;

        if (this.i === this.hsv.length)
        {
            this.i = 0;
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Создание группы и начальная настройка

В примере создается группа (Group) спрайтов 'invader' с помощью метода this.add.group. Группа — это мощный контейнер для управления множеством однотипных объектов. Она автоматически обновляется, рендерится и может применять действия ко всем своим членам.

this.group = this.add.group({ key: 'invader', frame: 0, repeat: 53 });

Ключевые параметры: - key — ключ загруженного спрайта. - frame — кадр анимации (индекс). - repeat — количество дополнительных объектов (всего создастся 54: 1 + 53).

Сразу после создания все спрайты находятся в одной точке. Чтобы расположить их в сетке, используется утилита Phaser.Actions.GridAlign.

Phaser.Actions.GridAlign(this.group.getChildren(), { width: 9, cellWidth: 58, cellHeight: 48, x: 132, y: 148 });

Обратите внимание: this.group.getChildren() возвращает обычный массив с дочерними объектами группы. Этот массив используется для первоначального выравнивания, но далее с ним нужно работать аккуратно.

Подготовка данных и обработка клика

Для работы с цветами создается HSV-палитра (Phaser.Display.Color.HSVColorWheel()). Она будет использоваться в цикле обновления для анимации оттенков спрайтов.

Логика удаления привязана к событию клика (pointerup). Важный момент: для выбора объекта на удаление используется не сама группа, а ее локальная копия массива invaders, созданная в create().

const invaders = this.group.getChildren();

В обработчике события Phaser.Utils.Array.RemoveRandomElement берет случайный элемент из этого массива и одновременно удаляет его из массива. Это гарантирует, что один и тот же спрайт не будет выбран дважды.

const invader = Phaser.Utils.Array.RemoveRandomElement(invaders);

Корректное уничтожение объекта

После получения случайного спрайта вызывается его метод destroy(). Это правильный способ удаления игровых объектов в Phaser.

if (invader)
{
    invader.destroy();
}

Что происходит при вызове destroy()? 1. Объект помечается для уничтожения. 2. Генерируется событие destroy. 3. Группа, будучи слушателем событий своих детей, автоматически получает это событие. 4. Группа удаляет уничтоженный объект из своего внутреннего списка.

**Почему это важно?** Не нужно вручную вызывать this.group.remove(invader). Группа сама синхронизирует свое состояние благодаря событийной модели. Проверить текущее количество объектов в группе можно методом this.group.getLength().

Обновление состояния и визуальные эффекты

В методе update() происходит две важные вещи: обновление текста с количеством 'захватчиков' и анимация их цвета.

this.text.setText(`Invaders: ${this.group.getLength()}`);

Использование this.group.getLength() критично, так как этот метод всегда возвращает актуальное количество живых объектов в группе после всех уничтожений.

Циклическое изменение оттенка применяется ко всем детям группы с помощью Phaser.Actions.SetTint.

const tint = this.hsv[this.i];
Phaser.Actions.SetTint(this.group.getChildren(), tint.color);

Обратите внимание, что this.group.getChildren() здесь вызывается каждый кадр и возвращает уже обновленный актуальный список объектов. Индекс `i` циклически проходит по всей HSV-палитре.

Конфигурация игры и запуск

Сцена регистрируется в конфигурации игры. Обратите внимание на параметр parent, который указывает ID HTML-элемента для встраивания canvas.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};

Создание экземпляра игры запускает весь жизненный цикл.

const game = new Phaser.Game(config);

Ключевые выводы и типичные ошибки

1. **Группа управляет своими детьми через события.** Вызывайте object.destroy(), а не пытайтесь удалять объекты из группы вручную. 2. **this.group.getChildren() возвращает копию массива.** Изменения в этом массиве (например, удаление элемента) не влияют на внутреннее состояние группы. Группа обновляет свой список только через события destroy. 3. **Для подсчета объектов используйте getLength().** Не полагайтесь на длину массива, полученного через getChildren(). 4. **Утилиты Phaser.Actions работают с любыми массивами.** Они не привязаны к группам, что дает гибкость.

Пример ошибки: прямая манипуляция массивом группы.

// НЕПРАВИЛЬНО: группа не узнает об удалении
const children = this.group.getChildren();
children.pop();

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

Работа с группами в Phaser эффективна, когда вы доверяете встроенной событийной системе. Метод destroy() — это правильный способ удаления объектов, который гарантирует корректное обновление всех связанных структур. Для экспериментов попробуйте

  1. Уничтожать объекты не по клику, а при столкновении с пулей
  2. Создавать анимацию 'взрыва' спрайта перед вызовом destroy()
  3. Реализовать пул объектов на основе группы, где уничтоженные объекты не удаляются, а деактивируются и используются повторно