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

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

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/skies/deepblue.png');
        this.load.atlas('cards', 'assets/atlas/cards.png', 'assets/atlas/cards.json');
    }

    create ()
    {
        this.add.image(400, 300, 'bg');

        const frames = this.textures.get('cards').getFrameNames();

        const cards = [];

        //  Create 8 cards and push them into an array

        for (var i = 0; i < 16; i++)
        {
            cards.push(this.add.sprite(0, 0, 'cards', Phaser.Math.RND.pick(frames)));
        }

        //  The cards are 140x190 in size

        //  Let's lay them out in a 8x2 grid, except we'll use a smaller cell size, so that
        //  the cards overlap horizontally

        Phaser.Actions.GridAlign(cards, {
            width: 8,
            height: 2,
            cellWidth: 80,
            cellHeight: 220,
            x: 50,
            y: 80
        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и загрузка ассетов

Как и в любой сцене Phaser, работа начинается с методов preload и create. В preload мы загружаем фоновое изображение и атлас спрайтов — набор карт. Использование атласа (atlas) эффективнее, чем загрузка множества отдельных изображений, так как уменьшает количество HTTP-запросов и оптимизирует использование памяти.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('bg', 'assets/skies/deepblue.png');
    this.load.atlas('cards', 'assets/atlas/cards.png', 'assets/atlas/cards.json');
}

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

create ()
{
    this.add.image(400, 300, 'bg');
    const frames = this.textures.get('cards').getFrameNames();
}

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

Для удобства манипуляций с группой объектов их нужно собрать в массив. Мы создаем пустой массив cards и в цикле создаем 16 спрайтов. Каждый спрайт изначально помещается в точку (0, 0), а его текстура и кадр задаются как cards и случайный фрейм из полученного ранее списка.

Ключевой момент: метод Phaser.Math.RND.pick(frames) случайным образом выбирает имя фрейма из массива frames. Это встроенный генератор случайных чисел Phaser.

const cards = [];
for (var i = 0; i < 16; i++)
{
    cards.push(this.add.sprite(0, 0, 'cards', Phaser.Math.RND.pick(frames)));
}

Магия GridAlign: настройка перекрытия

Самый важный этап — использование Phaser.Actions.GridAlign. Этот метод принимает массив игровых объектов и объект конфигурации, а затем автоматически перераспределяет их координаты, выравнивая по сетке.

Стандартный подход предполагает, что cellWidth и cellHeight равны или больше размера объекта, чтобы избежать наложения. Однако для эффекта перекрытия (как в веерной колоде карт) мы задаем cellWidth меньше реальной ширины карты (140px). В примере используется cellWidth: 80. Это означает, что горизонтальное расстояние между центрами соседних карт будет всего 80 пикселей, что заставляет их заходить друг на друга.

Параметры width: 8 и height: 2 задают размер сетки (8 колонок, 2 строки). Параметры x: 50 и y: 80 определяют стартовую точку (верхний левый угол) для размещения всей сетки на сцене.

Phaser.Actions.GridAlign(cards, {
    width: 8,
    height: 2,
    cellWidth: 80,
    cellHeight: 220,
    x: 50,
    y: 80
});

Обратите внимание, что cellHeight (220) больше высоты карты (190), поэтому карты в разных строках не перекрываются по вертикали, создавая аккуратный двурядный макет.

Конфигурация игры

Код инициализации игры стандартен. В конфигурационном объекте мы задаем тип рендерера, размеры холста, цвет фона (который будет виден до загрузки ассетов), ID родительского HTML-элемента и класс нашей сцены.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};
const game = new Phaser.Game(config);

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

Метод Phaser.Actions.GridAlign — это мощный и гибкий инструмент для автоматического размещения объектов. Как мы увидели, его можно использовать не только для строгих сеток, но и для создания визуальных эффектов с наложением, меняя размеры ячеек. Для экспериментов попробуйте: изменить параметры cellWidth и cellHeight на динамические, зависящие от размера экрана; анимировать процесс раскладки карт с помощью Tweens; или применить GridAlign не к спрайтам, а к контейнерам с более сложной иерархией объектов.