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

Загрузка ресурсов — фундаментальный этап создания игры. Обычно мы указываем все изображения и звуки заранее в `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');
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath('assets/sprites/');

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

        this.load.image('atari130xe');
    }

    create ()
    {
    }

    showFile (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);

Подготовка очереди файлов

Вся логика находится в классе сцены Example. Первым делом мы создаём массив files, который будет нашей очередью на загрузку. В методе init() этот массив заполняется ключами (именами файлов без расширения) для 150+ спрайтов, которые доступны в публичном репозитории примеров Phaser.

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

    init ()
    {
        this.files.push('atari400');
        this.files.push('atari800');
        // ... и ещё более 150 имён
        this.files.push('zelda-life');
    }

Важно: init() выполняется до preload(), что гарантирует готовность списка к моменту начала загрузки. Массив выступает в роли стека — мы будем брать файлы с конца, используя метод .pop().

Настройка загрузчика и подписка на события

В методе preload() происходит ключевая настройка. Сначала задаётся базовый URL и путь к папке со спрайтами с помощью this.load.setBaseURL() и this.load.setPath(). Это избавляет от необходимости указывать полный путь для каждого файла.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.setPath('assets/sprites/');

Затем мы подписываемся на событие 'filecomplete' загрузчика. Каждый раз, когда любой файл завершает загрузку, будет вызвана функция showFile. Третий аргумент this обеспечивает правильный контекст (this внутри showFile будет ссылаться на сцену).

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

Наконец, мы вручную запускаем процесс, начав загрузку первого изображения — 'atari130xe'. Это и есть спусковой крючок для всей цепной реакции.

this.load.image('atari130xe');
}

Обработка завершения загрузки и запуск следующей

Сердце примера — метод showFile. Он выполняет две задачи: отображает загруженный спрайт и инициирует загрузку следующего.

Функция получает от события три параметра: key (ключ ресурса), type и texture. Мы используем только key.

showFile (key, type, texture)
{
    const x = Phaser.Math.Between(0, 800);
    const y = Phaser.Math.Between(0, 600);
    this.add.image(x, y, key);

Сначала генерируются случайные координаты в пределах сцены (800x600), и загруженное изображение добавляется на экран.

Затем мы извлекаем последний элемент из массива files:

const nextFile = this.files.pop();

Если элемент существует (массив не пуст), мы начинаем загрузку следующего спрайта с этим ключом. Загрузчик, получив новую задачу, впоследствии снова вызовет showFile по её завершении, создавая цикл.

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

Процесс продолжается, пока массив files не опустеет.

Почему это работает: событийная модель загрузчика

Ключ к пониманию — событийная модель Loader Plugin в Phaser 3. Метод this.load.image() не блокирует выполнение. Он ставит файл в очередь внутреннего менеджера загрузки и немедленно возвращает управление. Сама загрузка происходит асинхронно.

Событие 'filecomplete' — это сигнал от менеджера о том, что конкретный файл (изображение, аудио, JSON и т.д.) успешно загружен и добавлен в кэш текстуры (this.textures). Именно в этот момент ресурс готов к использованию методами вроде this.add.image().

Таким образом, пример создаёт управляемый конвейер: Запрос загрузки -> Ожидание события -> Обработка (отображение) -> Новый запрос. Это безопаснее, чем пытаться запускать множество параллельных загрузок this.load.image() в цикле, и даёт полный контроль над процессом.

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

Цепная загрузка через событие 'filecomplete' — это элегантный способ управлять потоком загрузки ресурсов в Phaser 3. Он идеально подходит для создания прогрессивных загрузочных экранов, где каждый загруженный ассет сразу становится частью интерфейса, или для ситуаций, когда следующий набор ресурсов зависит от предыдущего (например, загрузка уровня после его конфигурационного файла). **Идеи для экспериментов:** 1. Добавьте текстовый объект, который будет выводить key и текущий прогресс (осталось: ${this.files.length}). 2. Измените логику, чтобы спрайты появлялись не случайно, а выстраивались в сетку. 3. Комбинируйте типы ресурсов. Подпишитесь на событие 'complete' (завершение всей очереди), чтобы запустить игру после загрузки последнего файла. 4. Загружайте файлы не из стека, а из внешнего JSON-списка, полученного с сервера.