О чем этот пример
Одна из ключевых задач при разработке игр — управление игровым циклом и корректный перезапуск сцен с физикой. Этот пример демонстрирует, как правильно организовать переходы между сценами (Preloader, MainMenu, Game, GameOver) в Phaser 3 с использованием физического движка Matter.js. Вы научитесь создавать изолированные состояния игры, где физический мир полностью перезапускается без утечек памяти и артефактов, что критически важно для аркадных игр, платформеров и любых проектов с повторяющимися игровыми сессиями.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Preloader extends Phaser.Scene
{
constructor ()
{
super({ key: 'preloader' });
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('buttonBG', 'assets/sprites/button-bg.png');
this.load.image('buttonText', 'assets/sprites/button-text.png');
this.load.image('ayu', 'assets/pics/ayu.png');
this.load.image('ball', 'assets/sprites/pangball.png');
}
create ()
{
console.log('%c Preloader ', 'background: green; color: white; display: block;');
this.scene.start('mainmenu');
}
}
class MainMenu extends Phaser.Scene
{
constructor ()
{
super({ key: 'mainmenu' });
window.MENU = this;
}
create ()
{
console.log('%c MainMenu ', 'background: green; color: white; display: block;');
const bg = this.add.image(0, 0, 'buttonBG');
const text = this.add.image(0, 0, 'buttonText');
const container = this.add.container(400, 300, [ bg, text ]);
bg.setInteractive();
bg.once('pointerup', function ()
{
this.scene.start('game');
}, this);
}
}
class Game extends Phaser.Scene
{
constructor ()
{
super({ key: 'game' });
window.GAME = this;
this.controls;
}
create ()
{
console.log('%c Game ', 'background: green; color: white; display: block;');
this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);
// Add in a stack of balls
for (let i = 0; i < 64; i++)
{
const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');
ball.setCircle();
ball.setFriction(0.005);
ball.setBounce(1);
}
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
this.add.text(0, 0, 'Use Cursors to scroll camera.\nClick to Quit', { font: '18px Courier', fill: '#00ff00' }).setScrollFactor(0);
this.input.once('pointerup', function ()
{
this.scene.start('gameover');
}, this);
}
update (time, delta)
{
this.controls.update(delta);
}
}
class GameOver extends Phaser.Scene
{
constructor ()
{
super({ key: 'gameover' });
window.OVER = this;
}
create ()
{
console.log('%c GameOver ', 'background: green; color: white; display: block;');
this.add.sprite(400, 300, 'ayu');
this.add.text(300, 500, 'Game Over - Click to start restart', { font: '16px Courier', fill: '#00ff00' });
this.input.once('pointerup', function (event)
{
this.scene.start('mainmenu');
}, this);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
physics: {
default: 'matter',
matter: {
gravity: {
y: 0.3
}
}
},
scene: [ Preloader, MainMenu, Game, GameOver ]
};
const game = new Phaser.Game(config);
Структура сцен и их назначение
Пример построен на четырёх сценах, которые последовательно запускают друг друга, образуя полный игровой цикл: загрузка, меню, игра, завершение.
Каждая сцена — это отдельный класс, расширяющий Phaser.Scene. Ключ сцены (key), указанный в конструкторе, используется для её идентификации при переходах.
class Preloader extends Phaser.Scene
{
constructor ()
{
super({ key: 'preloader' });
}
Сцена Preloader загружает необходимые изображения. Важно, что метод preload() выполняется автоматически фреймворком. После загрузки в create() мы вручную запускаем следующую сцену — MainMenu.
create ()
{
console.log('%c Preloader ', 'background: green; color: white; display: block;');
this.scene.start('mainmenu');
}
Метод this.scene.start() останавливает текущую сцену и запускает новую. Это основа для управления потоком игры.
Запуск игры и настройка физического мира
Сцена Game — это ядро примера. Здесь создаётся и настраивается физический мир Matter.js.
Внутри метода create() мы сначала задаём границы мира. Параметры setBounds определяют, где действуют физические законы. Последние булевые параметры, особенно true для нижней границы, создают «пол», от которого будут отскакивать мячи.
this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);
Далее создаётся 64 физических тела (мяча) с помощью this.matter.add.image. Ключевой момент: каждому телу назначаются физические свойства.
const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');
ball.setCircle();
ball.setFriction(0.005);
ball.setBounce(1);
- setCircle() задаёт тело в форме круга, что критически важно для корректных столкновений.
- setFriction(0.005) устанавливает очень низкое трение, позволяя мячам долго катиться.
- setBounce(1) делает мячи абсолютно упругими (коэффициент restitution = 1), поэтому они не теряют энергию при отскоке.
Инициализация тел *над* видимой областью (y: Phaser.Math.Between(-600, 0)) создаёт эффект их падения сверху в начале игры.
Управление камерой и обработка ввода
Для динамичного обзора игрового поля в примере реализовано сглаженное управление камерой с клавиатуры.
Сначала создаётся объект конфигурации controlConfig. Он связывает клавиши курсора и клавиши `Q`/`E` с действиями камеры: перемещением и зумом.
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
На основе этой конфигурации создаётся экземпляр Phaser.Cameras.Controls.SmoothedKeyControl. Этот контроллер обеспечивает плавное, инерционное движение камеры, а не мгновенные «телепортации».
Чтобы контроллер работал, необходимо вызывать его метод update каждый кадр. Это делается в методе update сцены, куда передаётся delta-время для независимой от фреймрейта скорости.
update (time, delta)
{
this.controls.update(delta);
}
Обработчик клика мыши (pointerup) в этой сцене служит триггером для завершения игры и перехода к сцене GameOver.
Полный перезапуск: Завершение и возврат в меню
Сцена GameOver показывает финальный экран. По клику мыши игра возвращается в главное меню.
this.input.once('pointerup', function (event)
{
this.scene.start('mainmenu');
}, this);
Здесь происходит самое важное. Когда мы из меню снова запускаем сцену Game (this.scene.start('game') в MainMenu), Phaser не просто показывает старую сцену. Он **полностью останавливает текущий экземпляр сцены Game и создаёт новый**.
Это означает, что:
1. Старый физический мир Matter.js уничтожается.
2. Все созданные в нём тела (ball) удаляются.
3. Запускается заново метод create() сцены Game, который создаёт абсолютно новый мир с новым набором из 64 мячей.
Такой подход гарантирует чистый, детерминированный перезапуск игры без накопления состояния или утечек памяти. В конфигурации игры активирован Matter.js с лёгкой гравитацией по оси Y.
physics: {
default: 'matter',
matter: {
gravity: {
y: 0.3
}
}
},
Что попробовать дальше
Этот пример наглядно показывает парадигму управления состоянием в Phaser через систему сцен. Каждая игровая сессия живёт в собственном, изолированном экземпляре сцены, что делает перезапуск игры простым и безопасным. Для экспериментов попробуйте изменить физические параметры (гравитацию, упругость), количество или тип тел, добавить статические препятствия в мир или реализовать счётчик перезапусков, передавая данные между сценами через this.scene.start(key, data).
