О чем этот пример
В большинстве игр на 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, или комбинировать загрузку разных типов ресурсов (спрайт + звук для объекта) в одной цепочке.
