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

В большинстве игр на Phaser загрузка ресурсов происходит автоматически в методе `preload`. Но что, если вам нужно загружать ассеты динамически, по требованию игрока или в ответ на события? В этой статье разберем продвинутый паттерн: ручной запуск загрузчика и пошаговая загрузка ресурсов. Этот подход идеально подходит для создания прогрессивной загрузки контента, мини-игр с постепенным открытием ассетов или экономии памяти на старте проекта.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    files = [];

    init ()
    {
        this.files.push('atari400');
        this.files.push('atari800');
        this.files.push('atari800xl');
        this.files.push('128x128');
        this.files.push('128x128-v2');
        this.files.push('a');
        this.files.push('advanced_wars_land');
        this.files.push('advanced_wars_tank');
        this.files.push('amiga-cursor');
        this.files.push('aqua_ball');
        this.files.push('arrow');
        this.files.push('arrows');
        this.files.push('asteroids_ship');
        this.files.push('asteroids_ship_white');
        this.files.push('asuna_by_vali233');
        this.files.push('atari1200xl');
        this.files.push('b');
        this.files.push('baddie_cat_1');
        this.files.push('balls');
        this.files.push('beball1');
        this.files.push('bikkuriman');
        this.files.push('block');
        this.files.push('blue_ball');
        this.files.push('bluebar');
        this.files.push('bluemetal_32x32x4');
        this.files.push('bobs-by-cleathley');
        this.files.push('bsquadron1');
        this.files.push('bsquadron2');
        this.files.push('bsquadron3');
        this.files.push('budbrain_chick');
        this.files.push('bullet');
        this.files.push('bunny');
        this.files.push('cakewalk');
        this.files.push('car');
        this.files.push('carrot');
        this.files.push('centroid');
        this.files.push('chain');
        this.files.push('chick');
        this.files.push('chunk');
        this.files.push('clown');
        this.files.push('coin');
        this.files.push('cokecan');
        this.files.push('columns-blue');
        this.files.push('columns-orange');
        this.files.push('columns-red');
        this.files.push('copy-that-floppy');
        this.files.push('crate');
        this.files.push('crate32');
        this.files.push('cursor-rotate');
        this.files.push('darkwing_crazy');
        this.files.push('default');
        this.files.push('diamond');
        this.files.push('dragcircle');
        this.files.push('drawcursor');
        this.files.push('dude');
        this.files.push('eggplant');
        this.files.push('elephant');
        this.files.push('enemy-bullet');
        this.files.push('exocet_spaceman');
        this.files.push('explosion');
        this.files.push('eyes');
        this.files.push('firstaid');
        this.files.push('flectrum');
        this.files.push('flectrum2');
        this.files.push('fork');
        this.files.push('fuji');
        this.files.push('gameboy_seize_color_40x60');
        this.files.push('gem');
        this.files.push('gem-blue-16x16x4');
        this.files.push('gem-green-16x16x4');
        this.files.push('gem-red-16x16x4');
        this.files.push('ghost');
        this.files.push('green_ball');
        this.files.push('healthbar');
        this.files.push('helix');
        this.files.push('hello');
        this.files.push('hotdog');
        this.files.push('humstar');
        this.files.push('ilkke');
        this.files.push('interference_ball_48x48');
        this.files.push('interference_tunnel');
        this.files.push('jets');
        this.files.push('kirito_by_vali233');
        this.files.push('lemming');
        this.files.push('loop');
        this.files.push('maggot');
        this.files.push('master');
        this.files.push('melon');
        this.files.push('mine');
        this.files.push('mouse_jim_sachs');
        this.files.push('mushroom');
        this.files.push('mushroom2');
        this.files.push('onion');
        this.files.push('orange-cat1');
        this.files.push('orange-cat2');
        this.files.push('orb-blue');
        this.files.push('orb-green');
        this.files.push('orb-red');
        this.files.push('oz_pov_melting_disk');
        this.files.push('palm-tree-left');
        this.files.push('palm-tree-right');
        this.files.push('pangball');
        this.files.push('parsec');
        this.files.push('particle1');
        this.files.push('pepper');
        this.files.push('phaser');
        this.files.push('phaser-dude');
        this.files.push('phaser-ship');
        this.files.push('phaser_tiny');
        this.files.push('phaser1');
        this.files.push('phaser2');
        this.files.push('pineapple');
        this.files.push('plane');
        this.files.push('platform');
        this.files.push('player');
        this.files.push('purple_ball');
        this.files.push('ra_dont_crack_under_pressure');
        this.files.push('rain');
        this.files.push('red_ball');
        this.files.push('rgblaser');
        this.files.push('saw');
        this.files.push('shinyball');
        this.files.push('ship');
        this.files.push('shmup-baddie');
        this.files.push('shmup-baddie-bullet');
        this.files.push('shmup-baddie2');
        this.files.push('shmup-baddie3');
        this.files.push('shmup-boom');
        this.files.push('shmup-bullet');
        this.files.push('shmup-ship');
        this.files.push('shmup-ship2');
        this.files.push('skull');
        this.files.push('snowflake-pixel');
        this.files.push('sonic');
        this.files.push('sonic_havok_sanity');
        this.files.push('soundtracker');
        this.files.push('space-baddie');
        this.files.push('space-baddie-purple');
        this.files.push('spaceman');
        this.files.push('speakers');
        this.files.push('spikedball');
        this.files.push('spinObj_01');
        this.files.push('spinObj_02');
        this.files.push('spinObj_03');
        this.files.push('spinObj_04');
        this.files.push('spinObj_05');
        this.files.push('spinObj_06');
        this.files.push('spinObj_07');
        this.files.push('spinObj_08');
        this.files.push('splat');
        this.files.push('steelbox');
        this.files.push('strip1');
        this.files.push('strip2');
        this.files.push('tetrisblock1');
        this.files.push('tetrisblock2');
        this.files.push('tetrisblock3');
        this.files.push('thrust_ship');
        this.files.push('thrust_ship2');
        this.files.push('tinycar');
        this.files.push('tomato');
        this.files.push('treasure_trap');
        this.files.push('tree-european');
        this.files.push('ufo');
        this.files.push('vu');
        this.files.push('wabbit');
        this.files.push('wasp');
        this.files.push('wizball');
        this.files.push('x2kship');
        this.files.push('xenon2_bomb');
        this.files.push('xenon2_ship');
        this.files.push('yellow_ball');
        this.files.push('zelda-hearts');
        this.files.push('zelda-life');
    }

    create ()
    {
        const text = this.add.text(10, 10, 'Click to start the loader', { font: '16px Courier', fill: '#00ff00' });

        this.input.once('pointerup', function ()
        {

            text.setVisible(false);

            this.load.setPath('assets/sprites/');

            this.load.on('filecomplete', this.addNextFile, this);

            //  It needs _something_ in the queue, or `start` will just exit immediately.
            this.load.image('atari130xe');

            this.load.start();

        }, this);
    }

    addNextFile (key, type, texture)
    {
        const x = Phaser.Math.Between(0, 800);
        const y = Phaser.Math.Between(0, 600);

        this.add.image(x, y, key);

        const nextFile = this.files.pop();

        if (nextFile)
        {
            this.load.image(nextFile);
        }
    }
}

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

const game = new Phaser.Game(config);

Инициализация: готовим пул файлов

Вместо предзагрузки всех файлов в preload, мы создаем массив с именами будущих текстур. Эти имена соответствуют файлам в папке assets/sprites/. Ключевой момент: загрузчик Phaser не начинает работу сам по себе, пока мы явно не вызовем this.load.start().

init ()
{
    this.files.push('atari400');
    this.files.push('atari800');
    // ... длинный список файлов
}

Метод init выполняется до create и идеально подходит для подготовки данных. Массив this.files становится нашей очередью для динамической загрузки.

Старт загрузки по событию игрока

В методе `create` мы создаем текстовую подсказку и ждем клика мыши. По клику происходит несколько важных действий:
1. Подсказка скрывается.
2. Устанавливается базовый путь для загрузки через `this.load.setPath`.
3. На событие `filecomplete` вешается обработчик `addNextFile`.
4. В очередь загрузки добавляется один файл-«затравка».
5. Вызывается `this.load.start()` для ручного запуска процесса.
this.input.once('pointerup', function ()
{
    text.setVisible(false);
    this.load.setPath('assets/sprites/');
    this.load.on('filecomplete', this.addNextFile, this);
    this.load.image('atari130xe');
    this.load.start();
}, this);

Без хотя бы одного файла в очереди (atari130xe) вызов start завершится мгновенно, не запустив загрузчик.

Цепная реакция: загрузка по одному файлу

Обработчик `addNextFile` срабатывает каждый раз, когда файл успешно загружен. Внутри него:
1. Случайным образом определяется позиция на экране.
2. Создается спрайт с только что загруженной текстурой.
3. Из массива `this.files` извлекается следующий ключ.
4. Если ключ существует, он добавляется в очередь загрузки через `this.load.image(nextFile)`.
addNextFile (key, type, texture)
{
    const x = Phaser.Math.Between(0, 800);
    const y = Phaser.Math.Between(0, 600);
    this.add.image(x, y, key);
    const nextFile = this.files.pop();
    if (nextFile)
    {
        this.load.image(nextFile);
    }
}

Это создает цепную реакцию: загрузка первого файла завершается, добавляется второй, его загрузка завершается, добавляется третий, и так пока массив не опустеет.

Зачем это нужно? Практические сценарии

Такой подход не просто академический пример. Вот где он пригодится: * **Прогрессивная загрузка:** Пока игрок смотрит заставку или читает диалоги, фоном подгружаются ресурсы для следующего уровня. * **Экономия оперативной памяти:** Не загружать все 150 спрайтов сразу, а только те, которые нужны для текущего игрового момента. * **Динамический контент:** Если игра генерирует уровни на основе данных с сервера, можно загружать только необходимые для этого набора ассеты. * **Интерактивные мини-игры:** Клик игрока может запускать загрузку конкретного набора ресурсов для мини-игры, не нагружая память заранее.

Ключевое API здесь — это событие filecomplete и метод load.start().

Важные нюансы и подводные камни

1. **Контекст обработчика:** Обратите внимание, что в this.load.on третьим аргументом передается this. Это гарантирует, что внутри addNextFile контекст (this) будет указывать на экземпляр сцены, а не на загрузчик. 2. **Путь к файлам:** Метод setPath задает путь для всех последующих вызовов load.image. Убедитесь, что имена в массиве files точно соответствуют именам файлов (без расширения) в этой директории. 3. **Типы ресурсов:** В примере загружаются только изображения (load.image), но аналогично можно работать со звуками (load.audio), атласами (load.atlas) или JSON-данными, используя соответствующие методы API загрузчика. 4. **Обработка ошибок:** В реальном проекте стоит также подписаться на событие loaderror, чтобы обрабатывать случаи, когда файл не найден или поврежден, и не обрывать цепочку загрузки.

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

Ручное управление загрузчиком Phaser открывает путь к созданию гибких и оптимизированных игр. Вы получаете полный контроль над потоком загрузки ресурсов. Для экспериментов попробуйте: изменить логику очереди (например, загружать не по одному, а пачками по 5 файлов), добавить прогресс-бар, реагирующий на filecomplete, или комбинировать загрузку разных типов ресурсов (спрайт + звук для объекта) в одной цепочке.