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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('veg01', 'assets/tests/fruit/veg01.png');
        this.load.image('veg02', 'assets/tests/fruit/veg02.png');
        this.load.image('veg03', 'assets/tests/fruit/veg03.png');
        this.load.image('veg04', 'assets/tests/fruit/veg04.png');
        this.load.image('veg05', 'assets/tests/fruit/veg05.png');
        this.load.image('veg06', 'assets/tests/fruit/veg06.png');
        this.load.image('veg07', 'assets/tests/fruit/veg07.png');
        this.load.image('veg08', 'assets/tests/fruit/veg08.png');
        this.load.image('veg09', 'assets/tests/fruit/veg09.png');
        this.load.image('veg10', 'assets/tests/fruit/veg10.png');
        this.load.image('veg11', 'assets/tests/fruit/veg11.png');
        this.load.image('veg12', 'assets/tests/fruit/veg12.png');
        this.load.image('veg13', 'assets/tests/fruit/veg13.png');
        this.load.image('veg14', 'assets/tests/fruit/veg14.png');
        this.load.image('veg15', 'assets/tests/fruit/veg15.png');
        this.load.image('veg16', 'assets/tests/fruit/veg16.png');
        this.load.image('veg17', 'assets/tests/fruit/veg17.png');
        this.load.image('veg18', 'assets/tests/fruit/veg18.png');
        this.load.image('veg19', 'assets/tests/fruit/veg19.png');
        this.load.image('veg20', 'assets/tests/fruit/veg20.png');
        this.load.image('veg21', 'assets/tests/fruit/veg21.png');
        this.load.image('veg22', 'assets/tests/fruit/veg22.png');
        this.load.image('veg23', 'assets/tests/fruit/veg23.png');
        this.load.image('veg24', 'assets/tests/fruit/veg24.png');
        this.load.image('veg25', 'assets/tests/fruit/veg25.png');
        this.load.image('veg26', 'assets/tests/fruit/veg26.png');
        this.load.image('veg27', 'assets/tests/fruit/veg27.png');
        this.load.image('veg28', 'assets/tests/fruit/veg28.png');
        this.load.image('veg29', 'assets/tests/fruit/veg29.png');
        this.load.image('veg30', 'assets/tests/fruit/veg30.png');
        this.load.image('veg31', 'assets/tests/fruit/veg31.png');
        this.load.image('veg32', 'assets/tests/fruit/veg32.png');
        this.load.image('veg33', 'assets/tests/fruit/veg33.png');
        this.load.image('veg34', 'assets/tests/fruit/veg34.png');
        this.load.image('veg35', 'assets/tests/fruit/veg35.png');
        this.load.image('veg36', 'assets/tests/fruit/veg36.png');
        this.load.image('veg37', 'assets/tests/fruit/veg37.png');
    }

    create ()
    {
        const fruit = [];

        const test1 = 8;
        const test2 = 16;
        const test3 = 32;
        const test4 = 37;
        const test5 = 17;
        const test6 = 31;

        for (let i = 1; i < test3 + 1; i++)
        {
            fruit.push(this.add.sprite(0, 0, `veg${Phaser.Utils.String.Pad(i, 2, '0', 1)}`));
        }

        Phaser.Actions.GridAlign(fruit, {
            width: 8,
            height: 8,
            cellWidth: 64,
            cellHeight: 64,
            x: 100,
            y: 100
        });
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка текстур в Phaser

Первый шаг — загрузка ресурсов. В методе preload() класса сцены (Scene) используется загрузчик (Loader).

Здесь мы загружаем 37 изображений с фруктами и овощами. Ключевой момент — структура их ключей (идентификаторов). Все они имеют формат vegXX, где XX — двузначный номер. Это важно для последующего создания спрайтов в цикле.

Метод this.load.setBaseURL() задает базовый URL, от которого будут строиться пути к файлам. Это удобно, если все ресурсы лежат в одной папке.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('veg01', 'assets/tests/fruit/veg01.png');
// ... загрузка остальных изображений вплоть до veg37

Создание и хранение спрайтов

В методе create() происходит инициализация игровых объектов. Сначала создается пустой массив fruit, который будет хранить все наши спрайты.

Затем объявляются несколько констант (test1, test2 и т.д.). В данном примере активно используется только test3, равная 32. Она определяет, сколько спрайтов мы создадим.

Цикл for создает спрайты и помещает их в массив. Ключевая деталь — формирование ключа текстуры. Поскольку в ключах используются двузначные числа (01, 02, ...), а наш счетчик `i— обычное число, нам нужно добавить ведущий ноль. Для этого используется утилитаPhaser.Utils.String.Pad()`.

const fruit = [];
const test3 = 32;

for (let i = 1; i < test3 + 1; i++)
{
    fruit.push(this.add.sprite(0, 0, `veg${Phaser.Utils.String.Pad(i, 2, '0', 1)}`));
}

Метод this.add.sprite() создает новый спрайт. Первые два аргумента (0, 0) — это временные координаты, которые будут переопределены на следующем шаге.

Автоматическое выравнивание по сетке

После того как все спрайты созданы и собраны в массив, их нужно красиво расположить. Phaser предоставляет для этого мощный метод Phaser.Actions.GridAlign().

Этот метод принимает массив игровых объектов (в нашем случае — fruit) и объект конфигурации. Он автоматически расставляет объекты по сетке, обновляя их свойства `xиy`.

Разберем параметры конфигурации: * width и height — количество столбцов и строк в сетке. * cellWidth и cellHeight — размер каждой ячейки в пикселях. * `xиy` — координаты верхнего левого угла всей сетки.

Phaser.Actions.GridAlign(fruit, {
    width: 8,
    height: 8,
    cellWidth: 64,
    cellHeight: 64,
    x: 100,
    y: 100
});

В результате 32 спрайта будут расположены в сетку 8x4 (так как 32 / 8 = 4 строки) с отступом в 100 пикселей от левого и верхнего края окна.

Настройка игры и запуск

Финальная часть кода — создание экземпляра игры (Game). Конфигурационный объект config определяет основные параметры: * type: Phaser.WEBGL указывает на использование WebGL-рендерера (можно также использовать Phaser.CANVAS). * parent: ID HTML-элемента, в который будет встроен canvas игры. * width и height: размеры области отрисовки. * scene: класс сцены, которая будет запущена первой.

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

После создания экземпляра Phaser.Game автоматически запускается жизненный цикл сцены: preload(), create(), а затем update() на каждом кадре.

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

Пример демонстрирует два важных паттерна в Phaser: пакетную загрузку ресурсов с предсказуемыми ключами и использование действий (Actions) для управления группами объектов. Это фундамент для создания сложных интерфейсов и уровней. Для экспериментов попробуйте: 1. Изменить параметры width и height в GridAlign, чтобы получить другую форму сетки. 2. Использовать не все 37 загруженных текстур, а, например, только 17 (test5) или 31 (test6), изменив условие в цикле. 3. Добавить спрайтам физические тела и сделать их интерактивными по клику.