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

В этой статье мы рассмотрим, как легко организовать непрерывное движение игровых объектов по сцене с помощью Phaser Actions. Часто в играх требуется, чтобы объекты, выходя за пределы экрана, появлялись с другой стороны, создавая бесконечный цикл. Механика «оборачивания» (wrapping) идеально подходит для фоновых элементов, врагов или космических кораблей, движущихся в открытом пространстве. Мы разберем практический пример, который использует встроенные методы Phaser для эффективного управления группой анимированных спрайтов.

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

Живой запуск

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

Исходный код


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

        this.wrapRect;
        this.robots = [];
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('floor', 'assets/demoscene/checker-floor.png');
        this.load.atlas('robot', 'assets/animations/robo.png', 'assets/animations/robo.json');
    }

    create ()
    {
        this.add.image(400, 600, 'floor').setScale(1.25).setOrigin(0.5, 1);

        //  Create an animation
        this.anims.create({
            key: 'run',
            frames: this.anims.generateFrameNames('robot', { prefix: 'Running_', start: 0, end: 14, zeroPad: 3 }),
            frameRate: 18,
            repeat: -1
        });

        //  Create 6 sprites, spaced out horizontally
        for (let i = 0; i < 6; i++)
        {
            const sprite = this.add.sprite(i * 200, 380).play('run');

            this.robots.push(sprite);
        }

        //  This is our 'wrapping rectangle'
        //  When a sprite leaves this, it'll be wrapped around
        this.wrapRect = new Phaser.Geom.Rectangle(0, 0, 800, 600);
    }

    update ()
    {
        //  Move all the sprites forward 2 pixels
        Phaser.Actions.IncX(this.robots, 2);

        //  Wrap the sprites within our wrapping rectangle, with a 200px buffer
        Phaser.Actions.WrapInRectangle(this.robots, this.wrapRect, 200);
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и анимации

Класс сцены инициализирует массив для хранения спрайтов и геометрический прямоугольник для определения области оборачивания.

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

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('floor', 'assets/demoscene/checker-floor.png');
this.load.atlas('robot', 'assets/animations/robo.png', 'assets/animations/robo.json');

В методе create сначала добавляется статичный фон. Затем создается анимация с ключом 'run' из кадров атласа. Метод generateFrameNames автоматически генерирует имена кадров по заданному шаблону.

this.anims.create({
    key: 'run',
    frames: this.anims.generateFrameNames('robot', { prefix: 'Running_', start: 0, end: 14, zeroPad: 3 }),
    frameRate: 18,
    repeat: -1
});

Далее в цикле создается шесть спрайтов, каждый из которых сразу проигрывает анимацию 'run', и добавляется в массив this.robots.

for (let i = 0; i < 6; i++)
{
    const sprite = this.add.sprite(i * 200, 380).play('run');
    this.robots.push(sprite);
}

Наконец, определяется прямоугольник this.wrapRect, который задает границы для оборачивания. Его размеры обычно совпадают с видимой областью сцены или игровым миром.

Логика обновления и перемещения объектов

Вся активная логика происходит в методе update, который вызывается на каждом кадре игры.

Первым делом все спрайты в массиве this.robots сдвигаются по оси X на 2 пикселя с помощью статического метода Phaser.Actions.IncX. Это эффективный способ применить одно изменение к целой группе объектов.

Phaser.Actions.IncX(this.robots, 2);

Без дополнительных проверок спрайты просто улетели бы за правый край экрана и исчезли из виду. Чтобы создать эффект бесконечного пространства, нам нужно их вернуть.

Механика оборачивания с помощью WrapInRectangle

Ключевой метод Phaser.Actions.WrapInRectangle решает задачу «телепортации» объектов. Он проверяет положение каждого спрайта в переданном массиве относительно заданного прямоугольника (this.wrapRect).

Phaser.Actions.WrapInRectangle(this.robots, this.wrapRect, 200);

Третий аргумент — padding (отступ), равный 200 пикселям. Это важный параметр, который определяет поведение оборачивания: - Если спрайт покидает прямоугольник **справа** более чем на 200 пикселей, он мгновенно перемещается **влево** за пределы прямоугольника на те же 200 пикселей. - Аналогично, если спрайт покидает прямоугольник **слева** более чем на 200 пикселей, он перемещается **вправо**.

Отступ предотвращает резкое, заметное глазу «мерцание» или скачок спрайта, когда он только-только пересек границу. Объект сначала полностью скрывается за пределами видимой зоны (на 200 пикселей), а только потом незаметно для игрока переносится на противоположную сторону. Это создает плавную и непрерывную иллюзию движения.

Настройка игры и конфигурация

Инициализация игры происходит стандартно, через объект конфигурации и создание экземпляра Phaser.Game.

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

const game = new Phaser.Game(config);

Обратите внимание, что размеры сцены (width: 800, height: 600) совпадают с размерами прямоугольника оборачивания this.wrapRect, созданного в методе create. Это значит, что область оборачивания равна всей видимой области игры. Вы можете сделать wrapRect меньше или больше, чтобы контролировать, в какой именно зоне происходит «телепортация» объектов.

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

Методы Phaser.Actions.IncX и Phaser.Actions.WrapInRectangle предоставляют мощный и оптимизированный инструмент для управления движением множества объектов. Для экспериментов попробуйте: изменить значение отступа (padding) на 0 или очень большое число; применить оборачивание по вертикали с помощью Phaser.Actions.IncY; использовать другую геометрическую область, например Phaser.Geom.Circle, в сочетании с кастомной логикой оборачивания; или добавить случайную скорость каждому спрайту для более динамичной картины.