О чем этот пример
В этой части мы разберем, как оживить нашу игру «Змейка» на движке Phaser. Вы научитесь реализовывать плавное движение змейки по сетке, управление с клавиатуры, поедание пищи и рост хвоста. Этот пример демонстрирует ключевые концепции игровой логики, работы с группами объектов (`Group`) и управления временем (`time`), которые пригодятся при создании любых пошаговых или аркадных игр.
Версия 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);
},
eat: function ()
{
this.total++;
var x = Phaser.Math.Between(0, 39);
var y = Phaser.Math.Between(0, 29);
this.setPosition(x * 16, y * 16);
}
});
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;
},
grow: function ()
{
var newPart = this.body.create(this.tail.x, this.tail.y, 'body');
newPart.setOrigin(0);
},
collideWithFood: function (food)
{
if (this.head.x === food.x && this.head.y === food.y)
{
this.grow();
food.eat();
return true;
}
else
{
return false;
}
}
});
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();
}
if (snake.update(time))
{
// If the snake updated, we need to check for collision against food
snake.collideWithFood(food);
}
}
Структура и инициализация классов
Исходный код разделен на два основных класса: Snake для самой змейки и Food для еды. Они создаются внутри функции create, что позволяет инкапсулировать логику каждого объекта.
Класс Food расширяет стандартный Phaser.GameObjects.Image. Это позволяет нам легко управлять его положением и текстурой. Конструктор принимает координаты в сетке (x, y) и преобразует их в пиксельные, умножая на 16 (размер одного сегмента). Метод eat вызывается, когда змейка съедает пищу: он увеличивает счетчик и перемещает еду на новую случайную позицию на поле.
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);
},
eat: function () {
this.total++;
var x = Phaser.Math.Between(0, 39);
var y = Phaser.Math.Between(0, 29);
this.setPosition(x * 16, y * 16);
}
});
Класс Snake не наследуется от стандартного класса Phaser, а является пользовательской структурой. В нем хранятся состояние змейки (жива ли она, направление движения), ее тело как группа (Group) сегментов и логика перемещения.
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;
},
// ... остальные методы
});
Логика движения и управления
Движение змейки реализовано на основе времени игры (time). Метод update класса Snake проверяет, настало ли время для следующего шага, сравнивая текущее игровое время с сохраненным значением this.moveTime. Если время пришло, вызывается метод move.
В методе move в зависимости от текущего направления (this.heading) обновляется вектор позиции головы headPosition. Ключевой момент — использование Phaser.Math.Wrap, которое обеспечивает «заворачивание» змейки по краям экрана: выйдя за правую границу, она появляется слева, и наоборот.
move: function (time) {
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;
Phaser.Actions.ShiftPosition(this.body.getChildren(), this.headPosition.x * 16, this.headPosition.y * 16, 1, this.tail);
this.moveTime = time + this.speed;
return true;
}
Для перемещения всех сегментов тела используется мощный метод Phaser.Actions.ShiftPosition. Он сдвигает всю группу сегментов на новую позицию головы, а последнюю (хвостовую) позицию сохраняет в this.tail. Это нужно для последующего роста змейки.
Управление направлением осуществляется через методы faceLeft, faceRight, faceUp, faceDown. Они проверяют, что змейка не может развернуться на 180 градусов мгновенно (например, при движении вправо нельзя сразу повернуть налево). Это классическое правило игры.
faceLeft: function () {
if (this.direction === UP || this.direction === DOWN) {
this.heading = LEFT;
}
}
Взаимодействие с едой и рост змейки
Столкновение с едой проверяется в основном цикле игры (update), после каждого успешного обновления позиции змейки (snake.update(time) возвращает true). Метод collideWithFood сравнивает координаты головы змейки (this.head.x, this.head.y) с координатами еды (food.x, food.y). Так как оба объекта являются Image и их положение задано в пикселях, сравнение проходит корректно.
collideWithFood: function (food) {
if (this.head.x === food.x && this.head.y === food.y) {
this.grow();
food.eat();
return true;
} else {
return false;
}
}
При обнаружении столкновения вызывается метод grow. Он создает новый сегмент тела (Image) в позиции this.tail, которая была сохранена методом ShiftPosition при последнем движении. Таким образом, новый сегмент появляется там, где был конец хвоста до перемещения, что визуально удлиняет змейку.
grow: function () {
var newPart = this.body.create(this.tail.x, this.tail.y, 'body');
newPart.setOrigin(0);
}
Метод eat класса Food перемещает еду на новую случайную позицию, используя Phaser.Math.Between. Параметры 0-39 и 0-29 соответствуют размерам игрового поля в клетках (40x30 при размере клетки 16 пикселей).
Главный игровой цикл и управление
Функция update является сердцем игры. Она вызывается Phaser на каждом кадре. В ней происходит:
1. Проверка, жива ли змейка (на данный момент флаг alive всегда true, но его можно использовать для обработки смерти).
2. Обработка ввода с клавиатуры через объект cursors. Нажатия проверяются по порядку, и вызываются соответствующие методы изменения направления змейки.
3. Если змейка совершила движение (snake.update(time) вернул true), проверяется столкновение с едой.
function update (time, delta) {
if (!snake.alive) {
return;
}
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();
}
if (snake.update(time)) {
snake.collideWithFood(food);
}
}
Обратите внимание на использование параметра time. Это накопленное игровое время в миллисекундах, которое передается в snake.update для реализации пошагового движения с фиксированной скоростью (this.speed = 100 означает шаг каждые 100 мс).
Что попробовать дальше
Мы разобрали рабочую реализацию классической «Змейки» на Phaser, которая включает движение по сетке, управление, сбор предметов и рост. Код хорошо демонстрирует использование Class для создания объектов, Group для управления связанными элементами и работу с игровым временем для контроля скорости.
**Идеи для экспериментов:**
1. Добавьте столкновение змейки с самой собой (проверка координат головы с координатами любого сегмента тела) и завершение игры.
2. Реализуйте увеличение скорости змейки с каждым съеденным предметом (уменьшайте значение this.speed).
3. Добавьте разные типы пищи с разными эффектами (например, временное ускорение или дополнительные очки).
4. Замените «заворачивание» границ (Wrap) на столкновение со стенами и окончание игры.
5. Реализуйте систему очков, отображающую счет (this.total из класса Food) на экране с помощью текстового объекта (this.add.text).
