О чем этот пример
В этой статье разберем ключевой этап разработки игры «Змейка» — реализацию движения и управления. Мы построим логику перемещения змейки по игровому полю с циклическим переходом через границы и интеллектуальным управлением, которое предотвращает разворот «в себя». Это фундамент для любой пошаговой или клеточной игры, где объект управляется игроком и имеет «хвост». Вы научитесь использовать классы Phaser, группы объектов и математические утилиты для создания плавной и предсказуемой игровой механики.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
var config = {
type: Phaser.WEBGL,
width: 640,
height: 480,
backgroundColor: '#bfcc00',
parent: 'phaser-example',
scene: {
preload: preload,
create: create,
update: update
}
};
var snake;
var food;
var cursors;
// Direction consts
var UP = 0;
var DOWN = 1;
var LEFT = 2;
var RIGHT = 3;
var game = new Phaser.Game(config);
function preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('food', 'assets/games/snake/food.png');
this.load.image('body', 'assets/games/snake/body.png');
}
function create ()
{
var Food = new Phaser.Class({
Extends: Phaser.GameObjects.Image,
initialize:
function Food (scene, x, y)
{
Phaser.GameObjects.Image.call(this, scene)
this.setTexture('food');
this.setPosition(x * 16, y * 16);
this.setOrigin(0);
this.total = 0;
scene.children.add(this);
}
});
var Snake = new Phaser.Class({
initialize:
function Snake (scene, x, y)
{
this.headPosition = new Phaser.Math.Vector2(x, y);
this.body = scene.add.group();
this.head = this.body.create(x * 16, y * 16, 'body');
this.head.setOrigin(0);
this.alive = true;
this.speed = 100;
this.moveTime = 0;
this.tail = new Phaser.Math.Vector2(x, y);
this.heading = RIGHT;
this.direction = RIGHT;
},
update: function (time)
{
if (time >= this.moveTime)
{
return this.move(time);
}
},
faceLeft: function ()
{
if (this.direction === UP || this.direction === DOWN)
{
this.heading = LEFT;
}
},
faceRight: function ()
{
if (this.direction === UP || this.direction === DOWN)
{
this.heading = RIGHT;
}
},
faceUp: function ()
{
if (this.direction === LEFT || this.direction === RIGHT)
{
this.heading = UP;
}
},
faceDown: function ()
{
if (this.direction === LEFT || this.direction === RIGHT)
{
this.heading = DOWN;
}
},
move: function (time)
{
/**
* Based on the heading property (which is the direction the pgroup pressed)
* we update the headPosition value accordingly.
*
* The Math.wrap call allow the snake to wrap around the screen, so when
* it goes off any of the sides it re-appears on the other.
*/
switch (this.heading)
{
case LEFT:
this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x - 1, 0, 40);
break;
case RIGHT:
this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x + 1, 0, 40);
break;
case UP:
this.headPosition.y = Phaser.Math.Wrap(this.headPosition.y - 1, 0, 30);
break;
case DOWN:
this.headPosition.y = Phaser.Math.Wrap(this.headPosition.y + 1, 0, 30);
break;
}
this.direction = this.heading;
// Update the body segments and place the last coordinate into this.tail
Phaser.Actions.ShiftPosition(this.body.getChildren(), this.headPosition.x * 16, this.headPosition.y * 16, 1, this.tail);
// Update the timer ready for the next movement
this.moveTime = time + this.speed;
return true;
}
});
food = new Food(this, 3, 4);
snake = new Snake(this, 8, 8);
// Create our keyboard controls
cursors = this.input.keyboard.createCursorKeys();
}
function update (time, delta)
{
if (!snake.alive)
{
return;
}
/**
* Check which key is pressed, and then change the direction the snake
* is heading based on that. The checks ensure you don't double-back
* on yourself, for example if you're moving to the right and you press
* the LEFT cursor, it ignores it, because the only valid directions you
* can move in at that time is up and down.
*/
if (cursors.left.isDown)
{
snake.faceLeft();
}
else if (cursors.right.isDown)
{
snake.faceRight();
}
else if (cursors.up.isDown)
{
snake.faceUp();
}
else if (cursors.down.isDown)
{
snake.faceDown();
}
snake.update(time);
}
Структура классов: Еда и Змейка
Вместо использования спрайтов напрямую, код создает два пользовательских класса с помощью Phaser.Class. Это позволяет инкапсулировать логику и состояние объектов.
Класс Food наследуется от Phaser.GameObjects.Image. Он просто отображает изображение еды в заданной позиции на сетке (каждая клетка — 16 пикселей).
var Food = new Phaser.Class({
Extends: Phaser.GameObjects.Image,
initialize:
function Food (scene, x, y)
{
Phaser.GameObjects.Image.call(this, scene)
this.setTexture('food');
this.setPosition(x * 16, y * 16);
this.setOrigin(0);
scene.children.add(this);
}
});
Класс Snake не наследуется от игрового объекта, а является композицией. Он хранит состояние змейки: позицию головы на сетке (headPosition), группу сегментов тела (body), флаг жизни, скорость, время следующего хода и направление движения.
var Snake = new Phaser.Class({
initialize:
function Snake (scene, x, y)
{
this.headPosition = new Phaser.Math.Vector2(x, y);
this.body = scene.add.group();
this.head = this.body.create(x * 16, y * 16, 'body');
this.head.setOrigin(0);
this.alive = true;
this.speed = 100;
this.moveTime = 0;
this.heading = RIGHT;
this.direction = RIGHT;
}
});
Логика движения и телепортация
Сердце механики — метод move в классе Snake. Он вызывается не каждый кадр, а по таймеру, заданному свойством speed (в данном случае, каждые 100 мс). Это создает пошаговое, дискретное движение.
Ключевая задача — обновить позицию головы на сетке (40x30 клеток) в зависимости от текущего heading. Здесь на помощь приходит Phaser.Math.Wrap. Эта функция автоматически «перебрасывает» координату, если она выходит за заданные границы, создавая эффект телепортации с одной стороны экрана на другую.
switch (this.heading)
{
case LEFT:
this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x - 1, 0, 40);
break;
case RIGHT:
this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x + 1, 0, 40);
break;
// ... аналогично для UP и DOWN
}
После обновления позиции головы, нужно сдвинуть все сегменты тела. Для этого используется мощный хелпер Phaser.Actions.ShiftPosition. Он берет массив детей из группы this.body и сдвигает их позиции, передавая последнюю позицию (координаты отброшенного хвоста) в переменную this.tail. Это критически важно для будущей механики поедания еды и роста змейки.
Phaser.Actions.ShiftPosition(this.body.getChildren(), this.headPosition.x * 16, this.headPosition.y * 16, 1, this.tail);
Наконец, метод обновляет this.moveTime, чтобы следующий ход произошел через заданный интервал.
Управление с клавиатуры и ограничение ввода
Управление реализовано через this.input.keyboard.createCursorKeys() в функции create. В функции update проверяется состояние клавиш-стрелок.
Важный нюанс: змейка не может мгновенно развернуться на 180 градусов. Методы faceLeft, faceRight, faceUp и faceDown содержат простую, но эффективную проверку. Они меняют намеченное направление (heading) только если текущее фактическое направление (direction) не является противоположным. Например, при движении вправо (RIGHT) можно повернуть только вверх (UP) или вниз (DOWN).
faceLeft: function ()
{
if (this.direction === UP || this.direction === DOWN)
{
this.heading = LEFT;
}
}
В главном цикле update проверка клавиш и вызов snake.update(time) происходят только если змейка жива (snake.alive).
if (cursors.left.isDown) { snake.faceLeft(); }
else if (cursors.right.isDown) { snake.faceRight(); }
// ... проверки для up и down
snake.update(time);
Метод update змейки сам решает, настало ли время для движения, сравнивая переданное time с внутренним this.moveTime.
update: function (time)
{
if (time >= this.moveTime)
{
return this.move(time);
}
}
Что попробовать дальше
Мы разобрали каркас игровой механики «Змейки» в Phaser, сфокусировавшись на движении и умном управлении. Основные инструменты — пользовательские классы, группы объектов, Phaser.Actions.ShiftPosition для анимации хвоста и Phaser.Math.Wrap для циклического мира. Для экспериментов попробуйте
- Изменить
this.speedдля ускорения или замедления игры - Заменить телепортацию на столкновение со стеной и завершение игры
- Добавить визуальный эффект (например, мигание) в момент «перехода» через границу экрана
- Реализовать управление с помощью мыши или сенсорных жестов, конвертируя координаты в направление
