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

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

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

Живой запуск

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

Исходный код


class SetQuantity extends Phaser.Scene
{
    constructor()
    {
        super();
    }

    create ()
    {
        this.add.text(10, 10, `Phaser v${Phaser.VERSION}\n\nSelect quantity\nof files to load`, { font: '16px Courier', fill: '#00ff00' });

        const button1 = this.add.text(10, 100, '2500', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });
        const button2 = this.add.text(10, 200, '5000', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });
        const button3 = this.add.text(10, 300, '7500', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });
        const button4 = this.add.text(10, 400, '10000', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });

        button1.setPadding(16).setOrigin(0).setInteractive();
        button2.setPadding(16).setOrigin(0).setInteractive();
        button3.setPadding(16).setOrigin(0).setInteractive();
        button4.setPadding(16).setOrigin(0).setInteractive();

        button1.once('pointerdown', () => {
            this.scene.start('BigLoad', { quantity: 2500 });
        });

        button2.once('pointerdown', () => {
            this.scene.start('BigLoad', { quantity: 5000 });
        });

        button3.once('pointerdown', () => {
            this.scene.start('BigLoad', { quantity: 7500 });
        });

        button4.once('pointerdown', () => {
            this.scene.start('BigLoad', { quantity: 10000 });
        });

        if (Phaser.VERSION === '3.55.2')
        {
            const button5 = this.add.text(10, 500, 'Swap to 3.61', { fontFamily: 'Arial', fontSize: '24px', color: '#000000', align: 'center', fixedWidth: 260, backgroundColor: '#ffffff' });

            button5.setPadding(16).setOrigin(0).setInteractive();

            button5.once('pointerdown', () => {
                window.location.href = 'https://labs.phaser.io/view.html?src=src/bugs/0000%20big%20load.js&v=live';
            });
        }
        else
        {
            const button5 = this.add.text(10, 500, 'Swap to 3.55', { fontFamily: 'Arial', fontSize: '24px', color: '#000000', align: 'center', fixedWidth: 260, backgroundColor: '#ffffff' });

            button5.setPadding(16).setOrigin(0).setInteractive();

            button5.once('pointerdown', () => {
                window.location.href = 'https://labs.phaser.io/view.html?src=src/bugs/0000%20big%20load.js&v=3.55.2';
            });
        }
    }
}

class Demo extends Phaser.Scene
{
    constructor()
    {
        super('BigLoad');
    }

    init (data)
    {
        this.quantity = data.quantity;

        this.add.text(10, 10, `Loading ${this.quantity} files`, { font: '20px Courier', fill: '#00ff00' });

        console.log('Quantity:', this.quantity);
    }

    preload ()
    {
        // this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
        const progress = this.add.graphics();

        this.load.on('progress', value =>
        {
            progress.clear();
            progress.fillStyle(0xffff00, 1);
            progress.fillRect(0, 100, 375 * value, 60);
        });


        for (let i = 0; i < this.quantity; i++)
        {
            this.load.image(`block${i}`, 'https://labs.phaser.io/assets/sprites/128x128-v2.png');
        }
    }

    create ()
    {
        const half = Math.floor(this.quantity / 2);
        const last = this.quantity - 1;

        this.add.sprite(100, 250, 'block0');
        this.add.sprite(100, 350, `block${half}`);
        this.add.sprite(100, 450, `block${last}`);
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 375,
    height: 667,
    backgroundColor: '#000000',
    scene: [ SetQuantity, Demo ]
};

const game = new Phaser.Game(config);

Структура сцены выбора количества

Первая сцена SetQuantity служит меню для выбора количества файлов, которые будут загружены в следующей сцене. Это полезно для тестирования поведения игры при разной нагрузке.

В методе create() создается текстовое описание и четыре кнопки с вариантами количества: 2500, 5000, 7500 и 10000 файлов. Каждая кнопка — это объект текста, стилизованный под кнопку.

const button1 = this.add.text(10, 100, '2500', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });

После создания кнопкам задается отступ (setPadding), точка начала координат (setOrigin) и интерактивность (setInteractive). Обратите внимание, что обработчик события pointerdown использует метод once, а не on. Это гарантирует, что событие сработает только один раз, даже если пользователь будет кликать несколько раз.

button1.once('pointerdown', () => {
    this.scene.start('BigLoad', { quantity: 2500 });
});

При клике запускается сцена BigLoad с передачей параметра quantity. Динамическая пятая кнопка позволяет переключиться между версиями Phaser 3.55.2 и 3.61 для сравнения поведения.

Передача данных между сценами и инициализация загрузки

Сцена Demo (с ключом BigLoad) получает переданное количество файлов через метод init(). Этот метод выполняется до preload() и идеально подходит для подготовки данных на основе параметров.

init (data)
{
    this.quantity = data.quantity;
    this.add.text(10, 10, `Loading ${this.quantity} files`, { font: '20px Courier', fill: '#00ff00' });
}

Переменная this.quantity сохраняется для использования в следующем методе preload(). Важно понимать, что init() вызывается только один раз при запуске сцены, что делает его надежным местом для инициализации параметров.

Организация массовой загрузки и отслеживание прогресса

В методе preload() происходит основная работа по загрузке файлов. Сначала создается графический объект progress для визуализации прогресса.

const progress = this.add.graphics();

Затем на событие progress менеджера загрузки this.load вешается обработчик, который отрисовывает желтый прямоугольник, ширина которого зависит от значения прогресса (от 0 до 1).

this.load.on('progress', value =>
{
    progress.clear();
    progress.fillStyle(0xffff00, 1);
    progress.fillRect(0, 100, 375 * value, 60);
});

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

for (let i = 0; i < this.quantity; i++)
{
    this.load.image(`block${i}`, 'https://labs.phaser.io/assets/sprites/128x128-v2.png');
}

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

Использование загруженных ассетов

После завершения загрузки вызывается метод create(). В нем демонстрируется, что все файлы действительно загружены и доступны через свои ключи.

const half = Math.floor(this.quantity / 2);
const last = this.quantity - 1;

this.add.sprite(100, 250, 'block0');
this.add.sprite(100, 350, `block${half}`);
this.add.sprite(100, 450, `block${last}`);

Отображаются три спрайта: первый (block0), средний (например, block1250 при количестве 2500) и последний. Это подтверждает, что загрузка прошла успешно для всего диапазона ключей. На практике такой массовый подход к загрузке редко используется в рабочих проектах из-за высоких требований к сети и памяти.

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

Пример наглядно показывает механизм загрузки тысяч файлов в Phaser и потенциальные риски такого подхода. Для реальных проектов рекомендуется объединять ресурсы в атласы, использовать форматы вроде JSON Hash или Array для анимаций, и загружать ассеты порциями по мере необходимости. Вы можете поэкспериментировать с этим кодом: попробуйте добавить обработку ошибок загрузки, реализовать отмену загрузки при смене сцены или разделить загрузку на несколько этапов с помощью this.load.pack.