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

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

Версия 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('sky', 'assets/skies/pixelsky.png');
        this.load.spritesheet('blocks', 'assets/sprites/heartstar32.png', { frameWidth: 32, frameHeight: 32 });
    }

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

        //  Create a series of sprites, with a block as the 'head'

        let head;
        const snake = [];

        for (let i = 0; i < 12; i++)
        {
            const part = this.add.image(64 + i * 32, 128, 'blocks', 1);

            part.setOrigin(0, 0);

            if (i === 11)
            {
                part.setFrame(0);

                head = part;
            }

            snake.push(part);
        }

        //  0 = left
        //  1 = right
        //  2 = up
        //  3 = down
        let direction = 3;
        let distance = Phaser.Math.Between(4, 8);

        //  Create a movement timer - every 100ms we'll move the 'snake'

        this.time.addEvent({ delay: 100, loop: true, callback: () => {

            let x = head.x;
            let y = head.y;

            if (direction === 0)
            {
                x = Phaser.Math.Wrap(x - 32, 0, 800);
            }
            else if (direction === 1)
            {
                x = Phaser.Math.Wrap(x + 32, 0, 800);
            }
            else if (direction === 2)
            {
                y = Phaser.Math.Wrap(y - 32, 0, 576);
            }
            else if (direction === 3)
            {
                y = Phaser.Math.Wrap(y + 32, 0, 576);
            }

            Phaser.Actions.ShiftPosition(snake, x, y);

            distance--;

            if (distance === 0)
            {
                if (direction <= 1)
                {
                    direction = Phaser.Math.Between(2, 3);
                }
                else
                {
                    direction = Phaser.Math.Between(0, 1);
                }

                distance = Phaser.Math.Between(4, 12);
            }

        }});
    }
}

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

const game = new Phaser.Game(config);

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

В методе preload загружаются необходимые ресурсы: фон и спрайтшит с блоками. В create сначала добавляется фон, а затем создается массив snake, который будет хранить все части змейки.

for (let i = 0; i < 12; i++)
{
    const part = this.add.image(64 + i * 32, 128, 'blocks', 1);
    part.setOrigin(0, 0);
    if (i === 11)
    {
        part.setFrame(0);
        head = part;
    }
    snake.push(part);
}

Цикл создает 12 спрайтов, выстроенных в линию. Каждый блок имеет размер 32x32 пикселя. Последнему блоку (голове) устанавливается кадр 0, чтобы визуально отличать его. Все блоки добавляются в массив snake для дальнейшего управления. Установка setOrigin(0, 0) смещает точку привязки в левый верхний угол, что упрощает расчеты позиций.

Организация движения с помощью таймера

Для реализации периодического движения используется this.time.addEvent. Это встроенный в Phaser таймер, который вызывает переданную функцию каждые 100 миллисекунд.

this.time.addEvent({ delay: 100, loop: true, callback: () => {
    // Логика движения здесь
}});

Внутри коллбэка вычисляется новая позиция для головы змейки на основе текущего направления (direction). Переменная distance определяет, сколько шагов змейка пройдет в текущем направлении перед поворотом. Использование таймера с фиксированным интервалом создает характерное пошаговое движение.

Расчет новой позиции и телепортация

Передвижение головы происходит по четырем направлениям. Для обеспечения бесшовного перемещения по игровому полю используется метод Phaser.Math.Wrap. Он "заворачивает" координату, если та выходит за указанные границы.

if (direction === 0)
{
    x = Phaser.Math.Wrap(x - 32, 0, 800);
}
else if (direction === 1)
{
    x = Phaser.Math.Wrap(x + 32, 0, 800);
}
// ... аналогично для направлений 2 (вверх) и 3 (вниз)

Например, если голова движется вправо (direction === 1) и ее координата `xстановится больше 800,Wrap` вернет значение в начале диапазона (около 0), создавая эффект выхода с одной стороны экрана и появления с другой.

Магия ShiftPosition: сдвиг всего массива

Ключевой метод в этом примере — Phaser.Actions.ShiftPosition. Он принимает массив игровых объектов и новые координаты `xиy`.

Phaser.Actions.ShiftPosition(snake, x, y);

Метод выполняет циклический сдвиг позиций всех объектов в массиве. Позиция первого элемента (snake[0]) принимает значение (x, y). Позиция второго элемента (snake[1]) становится равной старой позиции первого, и так далее. Последний элемент массива (голова) получает позицию предпоследнего. Таким образом, нам не нужно вручную перебирать массив и сохранять старые координаты — вся логика инкапсулирована в одном вызове API.

Случайные повороты и управление длиной шага

После каждого шага значение distance уменьшается. Когда оно достигает нуля, выбирается новое случайное направление и длина следующего отрезка пути.

distance--;
if (distance === 0)
{
    if (direction <= 1)
    {
        direction = Phaser.Math.Between(2, 3);
    }
    else
    {
        direction = Phaser.Math.Between(0, 1);
    }
    distance = Phaser.Math.Between(4, 12);
}

Условие if (direction <= 1) проверяет, двигалась ли змейка по горизонтали (0 — влево, 1 — вправо). Если да, то следующее направление выбирается вертикальным (2 — вверх, 3 — вниз), и наоборот. Это гарантирует, что змейка не развернется на 180 градусов и не пойдет "в себя". Phaser.Math.Between генерирует случайное число в заданном диапазоне.

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

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