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

При создании игр часто возникает задача: объект вышел за границы экрана — что делать? Можно уничтожить его, можно заставить отскочить, а можно плавно переместить на противоположную сторону, создавая эффект бесконечного пространства. Этот подход идеально подходит для космических симуляторов, аркадных гонок или казуальных игр с парящими элементами. В статье разберем, как использовать встроенный в Phaser 3 метод `Phaser.Actions.WrapInRectangle()` для реализации такого «телепорта» объектов. Мы наглядно изучим его работу, параметры и интегрируем в игровой цикл, чтобы ваши спрайты никогда не терялись из виду.

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

Живой запуск

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

Исходный код


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

    create ()
    {
        this.graphics = this.add.graphics();

        this.shapes = new Array(15).fill(null).map(
            () => new Phaser.Geom.Circle(Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 600), Phaser.Math.Between(25, 75))
        );

        this.rect = Phaser.Geom.Rectangle.Clone(this.cameras.main);
    }

    update ()
    {
        this.shapes.forEach(function (shape, i) {
            shape.x += (1 + 0.1 * i);
            shape.y += (1 + 0.1 * i);
        });

        Phaser.Actions.WrapInRectangle(this.shapes, this.rect, 72);

        this.draw();
    }

    // Locals methods, they are not part of Phaser.scene
    color (i)
    {
        return 0x001100 * (i % 15) + 0x000033 * (i % 5);
    }

    draw ()
    {
        this.graphics.clear();

        this.shapes.forEach((shape, i) => {
            this.graphics
            .fillStyle(this.color(i), 0.5)
            .fillCircleShape(shape);
        }, this);
    }
}

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

const game = new Phaser.Game(config);

Суть метода WrapInRectangle

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

Представьте шарик, улетающий вправо за пределы экрана: он мгновенно появится слева, сохранив свою скорость и направление по оси Y. Это создает иллюзию замкнутого, циклического мира.

Phaser.Actions.WrapInRectangle(this.shapes, this.rect, 72);
В этом вызове:
- `this.shapes` — массив объектов для обработки (в нашем случае — геометрические круги).
- `this.rect` — прямоугольник (`Phaser.Geom.Rectangle`), в границах которого происходит проверка.
- `72` — необязательный радиус (padding). Метод считает, что объект имеет размер. Если координата объекта + этот радиус выходят за границу, срабатывает телепорт.

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

Вся магия начинается в методе create(). Здесь мы подготавливаем холст для рисования, массив движущихся фигур и прямоугольник-границу.

Сначала создается графический объект (Graphics) для отрисовки примитивов в каждом кадре.

this.graphics = this.add.graphics();

Затем мы создаем массив из 15 кругов (Phaser.Geom.Circle) со случайными позициями и радиусами. Это наши «игровые объекты» для демонстрации.

this.shapes = new Array(15).fill(null).map(
    () => new Phaser.Geom.Circle(Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 600), Phaser.Math.Between(25, 75))
);

Ключевой момент — создание прямоугольника-границы. Мы клонируем прямоугольник основной камеры (this.cameras.main). Это удобно, так как границами для обертки становятся текущие видимые границы экрана (viewport).

this.rect = Phaser.Geom.Rectangle.Clone(this.cameras.main);

Игровой цикл: движение и обертка

Логика работы метода раскрывается в update(), который выполняется каждый кадр.

На каждом кадре мы сначала двигаем все круги. Чтобы сделать движение наглядным, скорость каждого следующего круга в массиве немного увеличивается.

this.shapes.forEach(function (shape, i) {
    shape.x += (1 + 0.1 * i);
    shape.y += (1 + 0.1 * i);
});

После обновления позиций вызывается WrapInRectangle. Он проверяет каждый круг из массива this.shapes. Если круг (с учетом его «радиуса» в 72 пикселя) покинул пределы прямоугольника this.rect, его координаты корректируются для появления с другой стороны.

Phaser.Actions.WrapInRectangle(this.shapes, this.rect, 72);

Важно: метод модифицирует координаты (`x,y`) объектов в переданном массиве напрямую. После этой строки все круги гарантированно находятся в видимой области (или только что в нее вернулись).

Визуализация и кастомизация

После всех расчетов вызывается пользовательский метод draw() для отрисовки состояния. Он очищает холст и заново рисует все круги с уникальными цветами.

draw ()
{
    this.graphics.clear();
    this.shapes.forEach((shape, i) => {
        this.graphics
        .fillStyle(this.color(i), 0.5)
        .fillCircleShape(shape);
    }, this);
}

Вспомогательный метод color() генерирует цвет на основе индекса круга. Прозрачность (0.5) позволяет видеть наложения.

Практический совет: вместо радиуса в 72 пикселя вы можете использовать реальный радиус вашего спрайта или игрового объекта. Например, если у вас есть спрайт с физическим телом:

// Предположим, this.asteroids — массив спрайтов
let bounds = this.cameras.main;
Phaser.Actions.WrapInRectangle(this.asteroids, bounds, asteroid.width / 2);

Это обеспечит точную обертку по границам спрайта, а не по абстрактному значению.

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

Phaser.Actions.WrapInRectangle — это элегантное и производительное решение для создания эффекта циклического или бесконечного игрового пространства. Оно избавляет от необходимости писать велосипеды с проверками границ и расчетами переноса. Для экспериментов попробуйте: 1. Применить метод к группе спрайтов (Phaser.GameObjects.Group) вместо массива геометрических объектов. 2. Использовать в качестве границ не прямоугольник камеры, а произвольную зону на карте, создав new Phaser.Geom.Rectangle(x, y, width, height). 3. Комбинировать обертку с другими действиями из модуля Phaser.Actions, например, с Call() для запуска анимации или звука в момент «телепорта».