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

Создание плавной анимации для большого количества объектов — классическая задача в игровой разработке. Обычно для этого используют циклы, но они могут нагружать процессор. В этой статье мы разберем пример, где 128 самолетов парят по экрану с помощью всего двух вызовов API. Вы узнаете, как использовать `Phaser.Actions` для эффективного управления группой спрайтов, что особенно полезно для создания фоновых эффектов, частиц или вражеских волн.

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

Живой запуск

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

Исходный код


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

        this.planes = [];
    }

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

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

        this.cameras.main.setBounds(0, 0, 800, 600);

        for (let i = 0; i < 128; i++)
        {
            const x = Phaser.Math.Between(0, 800);
            const y = Phaser.Math.Between(0, 600);

            this.planes.push(this.add.image(x, y, 'plane'));
        }
    }

    update ()
    {
        Phaser.Actions.IncY(this.planes, -1, -0.025);

        Phaser.Actions.WrapInRectangle(this.planes, this.cameras.main.getBounds(), 128);
    }
}

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

const game = new Phaser.Game(config);

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

Код начинается с создания класса сцены, наследующего от Phaser.Scene. В конструкторе инициализируется пустой массив this.planes, который будет хранить все спрайты самолетов.

class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
        this.planes = [];
    }

В методе preload загружаются два изображения: фон неба (bg) и спрайт самолета (plane). Базовый URL указывает на репозиторий с примерами Phaser.

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

Создание игровых объектов и настройка камеры

В методе create сначала добавляется фоновое изображение. Затем устанавливаются границы камеры с помощью this.cameras.main.setBounds. Это определяет виртуальное пространство, в котором будут двигаться объекты.

create ()
    {
        this.add.image(400, 300, 'bg');
        this.cameras.main.setBounds(0, 0, 800, 600);

Далее в цикле создаются 128 спрайтов самолета. Их начальные координаты задаются случайным образом в пределах границ камеры (от 0 до 800 по X и от 0 до 600 по Y) с помощью Phaser.Math.Between. Каждый созданный спрайт добавляется в массив this.planes.

for (let i = 0; i < 128; i++)
        {
            const x = Phaser.Math.Between(0, 800);
            const y = Phaser.Math.Between(0, 600);
            this.planes.push(this.add.image(x, y, 'plane'));
        }
    }

Анимация через Phaser.Actions: эффективное обновление

Сердце анимации находится в методе update, который вызывается на каждом кадре. Вместо ручного перебора массива this.planes в цикле, используются две мощные функции из модуля Phaser.Actions.

Первая функция — Phaser.Actions.IncY. Она увеличивает (или уменьшает) свойство `y` для каждого спрайта в переданном массиве. В данном случае все самолеты плавно поднимаются вверх.

update ()
    {
        Phaser.Actions.IncY(this.planes, -1, -0.025);

Параметры функции: 1. this.planes — массив объектов для изменения. 2. -1 — базовое значение, на которое изменяется координата Y. 3. -0.025 — случайное отклонение от базового значения (step). Итоговое изменение для каждого объекта вычисляется как value + (Math.random() * step). Это придает движению небольшой разброс и выглядит более естественно.

Обеспечение непрерывности движения с помощью WrapInRectangle

Если бы мы только двигали самолеты вверх, они бы вскоре улетели за верхнюю границу экрана и исчезли. Функция Phaser.Actions.WrapInRectangle решает эту проблему. Она проверяет положение каждого спрайта в массиве относительно заданного прямоугольника (границ камеры) и, если спрайт его покидает, "перебрасывает" его на противоположную сторону.

Phaser.Actions.WrapInRectangle(this.planes, this.cameras.main.getBounds(), 128);
    }
}
Параметры функции:
1.  `this.planes` — массив объектов для проверки.
2.  `this.cameras.main.getBounds()` — прямоугольник (границы камеры), внутри которого объекты должны оставаться.
3.  `128` — отступ (padding). Объект считается покинувшим зону только когда уйдет за ее границу более чем на 128 пикселей. Это создает плавный эффект: самолеты начинают появляться снизу еще до того, как последние скроются наверху, формируя бесконечный поток.

Конфигурация и запуск игры

Завершает пример стандартная конфигурация игры Phaser 3. В объекте config задается автоматический выбор рендерера, размеры холста, цвет фона, ID родительского HTML-элемента и класс основной сцены. После этого создается экземпляр игры new Phaser.Game(config), который запускает весь описанный цикл.

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

const game = new Phaser.Game(config);

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

Использование Phaser.Actions позволяет управлять сотнями объектов с минимальными затратами производительности, заменяя громоздкие циклы на одну строку кода. Для экспериментов попробуйте изменить параметры в IncY (например, двигать объекты по оси X с IncX), использовать другие действия из модуля (например, Rotate для вращения или SetTint для изменения цвета) или применить WrapInRectangle к объектам, которые движутся по диагонали, создавая более сложные паттерны движения.