О чем этот пример
Создание логики персонажа — ключевой этап в разработке игры. В этой статье мы разберем, как реализовать класс `Snake` для игры «Змейка» на 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 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 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;
},
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
Phaser.Actions.ShiftPosition(this.body.getChildren(), this.headPosition.x * 16, this.headPosition.y * 16, 1);
// Update the timer ready for the next movement
this.moveTime = time + this.speed;
return true;
}
});
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);
}
Структура класса Snake
Класс Snake инкапсулирует всю логику змейки: её положение, состояние и поведение. Он создаётся внутри сцены и управляется через методы update и move.
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;
}
});
- headPosition: вектор для хранения позиции головы в сетке (не в пикселях).
- body: группа Phaser, содержащая сегменты змейки. Она упрощает управление несколькими спрайтами.
- head: первый спрайт в группе, созданный из изображения body. setOrigin(0) задаёт точку отсчёта в левом верхнем углу для точного позиционирования по сетке.
- alive: флаг жизненного состояния.
- speed: скорость движения в миллисекундах (чем меньше значение, тем быстрее змейка).
- moveTime: временная метка для контроля следующего шага.
- heading и direction: направление, в которое змейка нацелилась и фактически движется. Это предотвращает мгновенный разворот на 180 градусов.
Управление направлением
Направление змейки меняется через методы faceLeft, faceRight, faceUp и faceDown. Они проверяют, чтобы новое направление не было противоположным текущему, что исключает возможность «самоубийства» змейки.
faceLeft: function () {
if (this.direction === UP || this.direction === DOWN) {
this.heading = LEFT;
}
}
Каждый метод проверяет this.direction. Например, если змейка движется вверх или вниз, она может повернуть налево. this.heading обновляется только при выполнении условия, а фактическое изменение направления (this.direction) происходит в методе move. Это разделение позволяет обрабатывать ввод плавно, без конфликтов.
В главном цикле update сцены эти методы вызываются в ответ на нажатия клавиш:
if (cursors.left.isDown) {
snake.faceLeft();
}
Логика движения и обновления
Движение змейки происходит в методе move, который вызывается из update класса Snake на основе временных меток.
update: function (time) {
if (time >= this.moveTime) {
return this.move(time);
}
}
time — это общее время игры в миллисекундах, переданное из основного цикла Phaser. Если текущее время превышает this.moveTime, змейка делает шаг.
В move обновляется headPosition с учётом heading и обеспечивается «заворачивание» через Phaser.Math.Wrap:
switch (this.heading) {
case LEFT:
this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x - 1, 0, 40);
break;
}
Phaser.Math.Wrap(value, min, max) «переносит» значение за границы диапазона. Например, при движении влево за границу x=0, позиция станет x=39 (так как сетка 40x30). Это создаёт эффект бесконечного поля.
После сдвига головы, все сегменты тела обновляются с помощью Phaser.Actions.ShiftPosition:
Phaser.Actions.ShiftPosition(this.body.getChildren(), this.headPosition.x * 16, this.headPosition.y * 16, 1);
Этот метод сдвигает позиции всех спрайтов в группе, начиная с конца, и устанавливает новую позицию для головы. Аргумент `1` — это шаг, который здесь не критичен, так как мы работаем с одной позицией. Умножение на 16 переводит сеточные координаты в пиксели (размер спрайта).
Наконец, this.moveTime обновляется для следующего шага: this.moveTime = time + this.speed;.
Обработка ввода в главном цикле
Управление змейкой привязано к стрелкам клавиатуры. В create создаётся объект cursors:
cursors = this.input.keyboard.createCursorKeys();
В update сцены проверяются состояния клавиш и вызываются соответствующие методы класса Snake:
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();
}
Цепочка else if гарантирует, что за один кадр будет обработано только одно нажатие. Важно, что проверка происходит каждый кадр, но фактическое движение — только по таймеру. Это обеспечивает отзывчивое управление без «пропусков» поворотов.
Перед обработкой ввода идёт проверка if (!snake.alive), которая может остановить обновление при «смерти» змейки (в этом примере alive всегда true, но это задел для будущего).
В конце вызывается snake.update(time), который делегирует обновление внутренней логики классу Snake.
Что попробовать дальше
Вы реализовали основу управления змейкой в Phaser, используя класс для инкапсуляции логики, временные интервалы для движения и обработку ввода с клавиатуры. Этот паттерн можно применять для любых пошаговых или плавных перемещений.
Идеи для экспериментов:
- Измените speed, чтобы змейка двигалась быстрее или медленнее.
- Добавьте еду и механику роста змейки, расширяя группу body.
- Реализуйте столкновения с телом, изменив флаг alive.
- Замените Phaser.Math.Wrap на столкновения со стенами, используя this.headPosition.x < 0.
