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

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

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

Живой запуск

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

Исходный код


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

var game = new Phaser.Game(config);

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.atlas('walker', 'assets/animations/walker.png', 'assets/animations/walker.json');
}

function create ()
{
    const animConfig = {
        key: 'walk',
        frames: 'walker',
        frameRate: 60,
        repeat: -1
    };

    this.anims.create(animConfig);

    const sprite = this.add.sprite(0, 0, 'walker', 'frame_0000');

    // const sprite = new Phaser.GameObjects.Sprite(this, 0, 0, 'walker', 'frame_0000');

    sprite.play('walk');

    sprite.destroy();

    // var container = this.add.container(400, 300);

    // container.add(sprite);

    console.log(this);
}

Настройка сцены и загрузка ассетов

Вся логика примера разбита на две стандартные функции сцены Phaser: preload и create. В preload мы настраиваем базовый URL для загрузки и используем метод this.load.atlas для загрузки атласа анимации.

function preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.atlas('walker', 'assets/animations/walker.png', 'assets/animations/walker.json');
}

Метод setBaseURL задает корневую папку для всех последующих загрузок. load.atlas загружает изображение-атлас (walker.png) и соответствующий JSON-файл с данными о кадрах (walker.json). Ключ 'walker' будет использоваться для доступа к этому атласу в коде.

Создание анимации и спрайта

В функции create происходит основная работа. Сначала создается конфигурационный объект для анимации, который передается в this.anims.create. Затем создается спрайт.

function create ()
{
    const animConfig = {
        key: 'walk',
        frames: 'walker',
        frameRate: 60,
        repeat: -1
    };
    this.anims.create(animConfig);
    const sprite = this.add.sprite(0, 0, 'walker', 'frame_0000');
}

Конфигурация animConfig определяет анимацию с ключом 'walk', которая использует все кадры из атласа 'walker', проигрывается со скоростью 60 кадров в секунду и повторяется бесконечно (repeat: -1).

Спрайт создается через фабричный метод сцены this.add.sprite. Ему передаются координаты (0, 0), ключ текстуры 'walker' и имя стартового кадра 'frame_0000'. Важно, что this.add.sprite не только создает объект Phaser.GameObjects.Sprite, но и автоматически добавляет его в список отображения текущей сцены.

Проблема: воспроизведение и немедленное уничтожение

В исходном коде после создания спрайта на нем запускается анимация, а затем он сразу уничтожается. Из-за этого игрок ничего не увидит на экране.

sprite.play('walk');
    sprite.destroy();

Метод sprite.play('walk') начинает проигрывать анимацию, созданную ранее. Однако следующий же вызов sprite.destroy() немедленно удаляет спрайт из сцены, освобождает его текстуру и останавливает все связанные с ним процессы, включая анимацию. Поскольку это происходит в одном кадре игры, спрайт не успевает отобразиться.

В закомментированном коде показана альтернатива — создание спрайта через конструктор new Phaser.GameObjects.Sprite. Такой спрайт не будет автоматически добавлен в сцену, и вызов play() на нем может привести к ошибкам, так как для работы анимации ему нужен родительский контекст сцены.

Как это исправить: отложенное уничтожение и контейнеры

Чтобы увидеть анимацию, нужно либо убрать вызов destroy(), либо отложить его. Phaser предоставляет несколько способов для управления временем. Самый простой — использовать встроенные события сцены или таймеры.

// Уничтожить спрайт через 2 секунды
this.time.delayedCall(2000, () => {
    sprite.destroy();
});

Еще один подход, показанный в комментариях, — использование Container. Контейнеры полезны для группировки объектов.

// Создание контейнера и добавление в него спрайта
var container = this.add.container(400, 300);
container.add(sprite);

В этом случае спрайт будет отрисован относительно позиции контейнера (400, 300). Уничтожение контейнера автоматически уничтожит все его дочерние элементы, включая наш спрайт.

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

Ключевой вывод: спрайт должен быть частью списка отображения сцены (через this.add или добавление в контейнер) для корректного отображения и работы анимации. Немедленный вызов destroy() после play() не дает анимации ни единого шанса. Для экспериментов попробуйте

  1. Запустить анимацию и перемещать спрайт с помощью this.tweens
  2. Создать несколько спрайтов с одной анимацией и уничтожать их по клику мыши
  3. Использовать sprite.on('animationcomplete', callback) для выполнения кода после окончания анимации