О чем этот пример
Создание плавного и эффективного перемещения цепочки объектов — частая задача в играх, будь то змейка, следы или движущиеся платформы. В этом примере мы разберем, как использовать метод `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 — это мощный инструмент для управления группой объектов с минимальным кодом. Он идеально подходит не только для змеек, но и для создания следов, цепочек частиц или движущихся платформ. Для экспериментов попробуйте: изменить интервал таймера для регулировки скорости, добавить управление с клавиатуры, реализовать рост змейки при сборе предметов или применить этот подход к массиву спрайтов с разными текстурами.
