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

Загрузка большого количества ресурсов — распространённая задача в играх, будь то уровни, предметы или персонажи. Пример из блога багов Phaser показывает, как организовать массовую загрузку тысяч 2x2 пиксельных изображений и отобразить их на сцене. Мы разберём этот код, чтобы понять, как эффективно управлять загрузкой, отслеживать прогресс и избегать типичных ошибок при работе с большими наборами данных. Это полезно для создания игр с процедурной генерацией, инвентарём или множеством визуальных эффектов.

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

Живой запуск

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

Исходный код


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

    create ()
    {
        this.add.text(10, 10, `Phaser v${Phaser.VERSION}\n2x2 TEST\nSelect quantity 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%20tiny.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%20tiny.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} 2x2 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);
        });


        let p = 1;

        for (let i = 0; i < this.quantity; i++)
        {
            this.load.image(`block${i}`, `https://labs.phaser.io/assets/tests/pixels/${p}.png`);

            p++;

            if (p === 33)
            {
                p = 1;
            }
        }
    }

    create ()
    {
        for (let i = 0; i < 512; i++)
        {
            const x = Phaser.Math.Between(0, 375);
            const y = Phaser.Math.Between(200, 300);
            const key = `block${Phaser.Math.Between(0, this.quantity - 1)}`;

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

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() создаётся информационный текст и четыре кнопки. Каждая кнопка — это объект текста (this.add.text), стилизованный под кнопку.

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

После создания кнопкам задаются отступы (setPadding), точка начала координат (setOrigin) и интерактивность (setInteractive). Это превращает текст в кликабельный элемент.

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

Обработчик события pointerdown для каждой кнопки использует метод this.scene.start для запуска основной сцены BigLoad с передачей параметра quantity. Ключевой момент — использование once вместо on, что гарантирует одноразовое срабатывание и предотвращает утечки памяти.

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

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

Приём параметров и инициализация

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

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

Параметр quantity сохраняется в свойстве сцены this.quantity, делая его доступным в других методах, таких как preload. Здесь же выводится информационное сообщение на экран и в консоль, что полезно для отладки.

Массовая загрузка и индикатор прогресса

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

const progress = this.add.graphics();

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

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

Затем в цикле for вызывается this.load.image для каждого из quantity файлов. Ключ (key) генерируется по шаблону block${i}, а URL — из набора тестовых 2x2 пиксельных изображений с labs.phaser.io. Переменная `p` обеспечивает циклическое использование 32 доступных изображений.

for (let i = 0; i < this.quantity; i++)
{
    this.load.image(`block${i}`, `https://labs.phaser.io/assets/tests/pixels/${p}.png`);
    p++;
    if (p === 33)
    {
        p = 1;
    }
}

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

Случайное отображение загруженных ресурсов

После завершения загрузки вызывается метод create. В нём демонстрируется использование загруженных изображений: 512 спрайтов размещаются в случайных позициях на ограниченной области экрана.

for (let i = 0; i < 512; i++)
{
    const x = Phaser.Math.Between(0, 375);
    const y = Phaser.Math.Between(200, 300);
    const key = `block${Phaser.Math.Between(0, this.quantity - 1)}`;
    this.add.image(x, y, key);
}

Phaser.Math.Between генерирует случайные координаты и индекс изображения. Ключ key выбирается случайно из всех загруженных. Это создаёт визуально шумный паттерн, который наглядно показывает, что все ресурсы загружены и готовы к использованию. Ограничение в 512 спрайтов для отображения предотвращает потенциальные проблемы с производительностью рендеринга.

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

Этот пример иллюстрирует ключевые принципы работы с большими наборами ресурсов в Phaser: передача данных между сценами, массовая загрузка с индикацией прогресса и случайное размещение объектов. Для экспериментов попробуйте изменить количество отображаемых спрайтов, использовать другие типы ресурсов (например, аудио или JSON) или реализовать отложенную загрузку по мере необходимости. Также стоит протестировать сборку атласов текстуры для сокращения количества HTTP-запросов.