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

Работа с мультиатласами (multi-atlas) в Phaser 3 — отличный способ организовать игровые ассеты. Однако при использовании их с системами частиц (Particle Emitter) вы можете столкнуться с коварной ошибкой: частицы отображаются некорректно или вовсе не отображаются, если их кадр находится во втором или последующих файлах атласа. Эта статья разбирает конкретный пример такой ошибки, объясняет её причину и предлагает практические решения для её устранения. Понимание этой проблемы поможет вам избежать часов отладки и создавать стабильные визуальные эффекты.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.path = 'assets/atlas/';
        this.load.multiatlas('megaset', 'tp3-multi-atlas.json');
    }

    create ()
    {
        // Good - This frame from the 1st atlas would be displayed correctly
        this.add.image(40, 60, 'megaset', 'diamond')
        // Good - This frame from the 2nd atlas would be displayed correctly
        this.add.image(80, 60, 'megaset', 'gem')

        // Good - These particles of a frame from the 1st atlas would be displayed correctly
        this.add.particles(40, 200, 'megaset', { frame: 'diamond' });
        // Bad - These particles of a frame from the 2nd atlas would be displayed incorrectly
        this.add.particles(80, 200, 'megaset', { frame: 'gem' });
    }
}

new Phaser.Game({
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    scene: Example,
    parent: 'phaser-example'
});

Проблема: Частицы из второго атласа не работают

В примере загружается мультиатлас megaset, состоящий из нескольких файлов изображений (например, tp3-multi-atlas-0.png и tp3-multi-atlas-1.png) и одного JSON-файла с разметкой.

При создании сцены мы видим четыре объекта:

* Два статичных спрайта: один с кадром 'diamond' (из первого файла атласа), другой с кадром 'gem' (из второго файла атласа). Оба отображаются корректно. * Два эмиттера частиц: первый использует кадр 'diamond', второй — кадр 'gem'.

Здесь и кроется проблема. Частицы с кадром 'gem' отображаются некорректно (например, как пустой прямоугольник или с искажённой текстурой), в то время как частицы с кадром 'diamond' работают как надо.

// Работает корректно
this.add.particles(40, 200, 'megaset', { frame: 'diamond' });
// Отображается с ошибкой!
this.add.particles(80, 200, 'megaset', { frame: 'gem' });

Причина: Механика работы Particle Emitter с текстурами

Корень проблемы лежит в том, как система частиц (Particle Emitter Manager) получает доступ к текстурам. В отличие от обычного спрайта, который может корректно разрешать кадры из любого файла внутри мультиатласа, эмиттер частиц на момент инициализации часто обращается только к *первому* изображению (текстуре) из набора.

Когда вы создаёте эмиттер и указываете кадр 'gem', система находит его метаданные в JSON (координаты, размер), но пытается взять пиксельные данные из первой загруженной текстуры (где этого кадра нет). Это приводит к ошибке выборки текстуры (texture sampling error) на GPU.

Проще говоря: this.add.image() умеет «заглядывать» во все файлы атласа, а this.add.particles() в определённых условиях — только в первый.

Решение 1: Предварительная настройка текстур (Рекомендуется)

Самое надёжное решение — явно указать системе частиц, какую текстуру использовать. Для этого нужно «подготовить» (prep) текстуру, связанную с нужным кадром, до создания эмиттера.

Шаги: 1. Получить ссылку на объект текстуры (Texture) атласа по его ключу. 2. Найти в этой текстуре конкретный источник (Source), который содержит нужный кадр. 3. Использовать метод prep, чтобы система частиц могла с этим источником работать.

create() {
    const texture = this.textures.get('megaset');
    const source = texture.getSourceImage('gem'); // Находим источник для кадра 'gem'
    texture.prepare(source); // Критически важный шаг для Particle Emitter

    // Теперь частицы с кадром 'gem' будут работать
    this.add.particles(80, 200, 'megaset', { frame: 'gem' });
}

Метод getSourceImage автоматически находит, в каком из файлов мультиатласа лежит указанный кадр. Метод prep гарантирует, что этот источник данных будет корректно передан в систему рендеринга частиц.

Решение 2: Использование отдельных атласов

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

preload() {
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.path = 'assets/atlas/';
    // Загружаем два отдельных атласа
    this.load.atlas('atlas1', 'tp3-multi-atlas-0.png', 'tp3-multi-atlas.json');
    this.load.atlas('atlas2', 'tp3-multi-atlas-1.png', 'tp3-multi-atlas.json'); // Тот же JSON, но система выберет нужные фреймы
}

create() {
    // Частицы берутся из гарантированно правильного источника
    this.add.particles(40, 200, 'atlas1', { frame: 'diamond' });
    this.add.particles(80, 200, 'atlas2', { frame: 'gem' });
}

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

Проверка и отладка

Чтобы убедиться, в каком источнике находится ваш кадр, можно использовать методы объекта текстуры для отладки.

create() {
    const texture = this.textures.get('megaset');
    const frame = texture.getFrame('gem'); // Получаем объект кадра
    const sourceIndex = frame.sourceIndex; // Индекс источника (0, 1, 2...)
    console.log(`Кадр 'gem' находится в источнике с индексом ${sourceIndex}`);
}

Если sourceIndex больше 0, а вы не выполнили texture.prepare(), то с кадром 'gem' почти наверняка возникнут проблемы при использовании в частицах.

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

Ошибка с отображением частиц из мультиатласа — известный нюанс Phaser 3, связанный с оптимизацией работы текстур. Решение через texture.prepare() является каноническим и должно применяться для любого кадра, источник которого (изображение) отличается от источника первого кадра в атласе. **Идеи для экспериментов:** * Создайте эмиттер, который случайным образом выбирает кадры для частиц из разных источников мультиатласа, предварительно подготовив все необходимые текстуры. * Проверьте, влияет ли эта проблема на другие объекты, использующие текстуры динамически, например, на Render Texture или Tile Sprites. * Напишите вспомогательную функцию, которая автоматически готовит все источники текстуры при создании сцены, если используется мультиатлас.