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

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

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

Живой запуск

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

Исходный код


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

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

        const button1 = this.add.text(10, 100, '10', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });
        const button2 = this.add.text(10, 200, '50', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });
        const button3 = this.add.text(10, 300, '100', { fontFamily: 'Arial', fontSize: '24px', color: '#ffffff', align: 'center', fixedWidth: 260, backgroundColor: '#0000cc' });
        const button4 = this.add.text(10, 400, '250', { 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: 10 });
        });

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

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

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

        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%201024.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%201024.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} 1024px files`, { font: '16px 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/pics/sao-sinon.png`);
        }
    }

    create ()
    {
        this.add.image(0, 200, 'block0').setOrigin(0).setScale(0.25);
        this.add.image(128, 328, `block${this.quantity - 1}`).setOrigin(0).setScale(0.25);
    }
}

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

const game = new Phaser.Game(config);

Структура сцен: разделение ответственности

Пример построен на двух сценах Phaser 3. Это классический подход для разделения логики: одна сцена отвечает за выбор параметров, другая — за выполнение основной задачи.

Класс SetQuantity — это сцена-меню. Его задача — позволить пользователю выбрать, сколько ресурсов будет загружено в следующей сцене. Это имитирует выбор уровня сложности или размера игрового мира.

class SetQuantity extends Phaser.Scene
{
    constructor()
    {
        super(); // Вызов родительского конструктора Phaser.Scene
    }

Класс Demo (переименованный в BigLoad в конструкторе) — это основная рабочая сцена. Она принимает выбранное количество через параметр, отображает прогресс загрузки и показывает результат.

class Demo extends Phaser.Scene
{
    constructor()
    {
        super('BigLoad'); // Регистрация сцены с ключом 'BigLoad'
    }

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

scene: [ SetQuantity, Demo ]

Создание интерактивного меню выбора

В методе create сцены SetQuantity создаются текстовые кнопки. Каждая кнопка — это объект Text с настроенным стилем и интерактивностью.

Ключевые шаги: 1. **Создание текста:** Используется this.add.text() с координатами, надписью и объектом стиля. 2. **Настройка внешнего вида:** Методы setPadding() и setOrigin(0) задают внутренние отступы и привязывают точку трансформации к левому верхнему углу для удобного позиционирования. 3. **Добавление интерактивности:** setInteractive() делает объект чувствительным к событиям ввода.

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

Обработка нажатия реализована через событие 'pointerdown'. Важно использовать once вместо on, чтобы подписка сработала только один раз и избежать утечек памяти при перезапуске сцены.

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

Метод this.scene.start() запускает сцену с ключом 'BigLoad' и передает ей данные — объект { quantity: 10 }. Эти данные будут получены в методе init целевой сцены.

Прием данных и подготовка к загрузке

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

init (data)
{
    this.quantity = data.quantity; // Сохраняем количество в свойство сцены
    this.add.text(10, 10, `Loading ${this.quantity} 1024px files`, { font: '16px Courier', fill: '#00ff00' });
    console.log('Quantity:', this.quantity);
}

Сохранив quantity в свойстве this.quantity, мы делаем эту переменную доступной в последующих методах жизненного цикла сцены, таких как preload и create.

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

Ядро примера — метод preload. Здесь происходит массовая загрузка изображений и отрисовка индикатора прогресса.

1. **Создание графики для прогресс-бара:** Объект Graphics используется для отрисовки простых фигур.

const progress = this.add.graphics();

2. **Подписка на событие прогресса:** Менеджер загрузки Phaser (this.load) генерирует событие 'progress' по мере загрузки файлов. Его значение — число от 0 до 1.

this.load.on('progress', value =>
    {
        progress.clear(); // Очищаем предыдущий кадр
        progress.fillStyle(0xffff00, 1); // Задаем желтый цвет
        progress.fillRect(0, 100, 375 * value, 60); // Рисуем прямоугольник, ширина зависит от прогресса
    });

3. **Цикл загрузки:** На основе переданного quantity в цикле формируются уникальные ключи и запросы на загрузку одного и того же изображения с сервера Phaser Labs.

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

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

Отображение результата

После завершения загрузки выполняется метод create. В данном примере для демонстрации отображаются только первое и последнее загруженное изображение.

create ()
{
    this.add.image(0, 200, 'block0').setOrigin(0).setScale(0.25);
    this.add.image(128, 328, `block${this.quantity - 1}`).setOrigin(0).setScale(0.25);
}

- 'block0' — ключ первого изображения, загруженного в цикле. - `` block${this.quantity - 1} `— ключ последнего изображения. Шаблонная строка динамически формирует имя, например,block249дляquantity: 250`. - setOrigin(0) устанавливает точку привязки изображения в его левый верхний угол для точного позиционирования по координатам. - setScale(0.25) уменьшает изображение, так как оригинал имеет размер 1024x1024 пикселя.

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

Пример 'Big Load 1024' — это отличный шаблон для организации загрузки ресурсов по требованию. Вы можете адаптировать его для загрузки разных наборов ассетов (например, текстур для уровня 'Пустыня' или 'Лес'), предзагрузки дополнительного контента после главного меню или создания стресс-тестов для вашей игры. Для экспериментов попробуйте загружать разные типы ресурсов (аудио, атласы), реализовать отмену загрузки или добавить более сложный анимированный индикатор прогресса.