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

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

Версия 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 служит меню выбора, вторая Demo (с ключом 'BigLoad') выполняет основную работу по загрузке и отображению. Такой подход разделяет ответственность: интерфейс выбора и сам процесс загрузки логически изолированы.

Ключевой момент — передача данных из одной сцены в другую через параметр data метода scene.start(). Это стандартный и эффективный способ коммуникации между сценами в Phaser.

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

В методе create() создаются текстовые элементы, которые выступают в роли кнопок. Каждая кнопка имеет фиксированную ширину (fixedWidth), отступы (setPadding) и интерактивность (setInteractive).

Обратите внимание на использование метода once() вместо on() для обработки события pointerdown. Это гарантирует, что обработчик сработает лишь один раз, что логично для кнопки, запускающей переход на другую сцену.

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

Особенность примера — динамическое создание кнопки для переключения между версиями Phaser 3.55.2 и 3.61. Это демонстрация условного создания игровых объектов на основе внешних условий (в данном случае версии движка).

Инициализация и массовая загрузка (Demo)

Метод init(data) сцены Demo принимает переданные данные и сохраняет выбранное количество (this.quantity) в свойство сцены. Это делает значение доступным в последующих методах жизненного цикла, таких как preload() и create().

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

В preload() происходит основная магия. Создается объект Graphics для отрисовки индикатора прогресса. Событие progress генератора загрузки (this.load) используется для обновления этого индикатора.

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

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

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}`);

Выбор ключей (block0, block${half}, block${last}) демонстрирует доступ к первому, среднему и последнему загруженному ресурсу, что является простой проверкой целостности загрузки.

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

Пример наглядно показывает, как Phaser управляет массовой асинхронной загрузкой и предоставляет инструменты для визуализации процесса. Для экспериментов попробуйте

  1. заменить загрузку изображений на другие типы ресурсов (аудио, атласы)
  2. реализовать отмену загрузки
  3. добавить более детальный прогресс-бар с текстовым процентом
  4. протестировать производительность с включенным кэшированием (this.load.setBaseURL) и без него