О чем этот пример
Переключение сцен — частое действие в играх, но резкий «скачок» может нарушить погружение. В этом примере показана техника плавного визуального перехода с использованием встроенных фильтров камеры и твинов Phaser 3. Вы научитесь создавать стильный эффект «пикселизации» для скрытия загрузки и смены контента, что сделает вашу игру более профессиональной и кинематографичной. Мы разберем, как применить фильтр `Pixelate` к основной камере и анимировать его параметры для входа в сцену и выхода из нее.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class SceneB extends Phaser.Scene
{
constructor ()
{
super({
key: 'SceneB'
});
}
init ()
{
this.cameras.main.fadeIn(100);
const fxCamera = this.cameras.main.filters.external.addPixelate(40);
this.add.tween({
targets: fxCamera,
duration: 700,
amount: -1,
});
}
create ()
{
const bg = this.add.image(0, 0, "bg1")
.setScale(.5)
.setOrigin(0);
const planet = this.add.image(this.sys.scale.width / 2, this.sys.scale.height / 2, "planet")
.setScale(.3);
// Planet rotation
this.add.tween({
targets: planet,
duration: 10000,
angle: 360,
repeat: -1
});
// FX
const pixelated = this.cameras.main.filters.external.addPixelate(-1);
// Create button
const buttonBox = this.add.rectangle(this.sys.scale.width / 2, this.sys.scale.height - 100, 290, 50, 0x000000, 1);
buttonBox.setInteractive();
const buttonText = this.add.text(this.sys.scale.width / 2, this.sys.scale.height - 100, "Click to Change Scene").setOrigin(0.5);
// Click to change scene
buttonBox.on('pointerdown', () => {
// Transition to next scene
this.add.tween({
targets: pixelated,
duration: 700,
amount: 40,
onComplete: () => {
this.cameras.main.fadeOut(100);
this.scene.start('SceneA');
}
})
});
// Hover button properties
buttonBox.on('pointerover', () => {
buttonBox.setFillStyle(0x222222, 1);
this.input.setDefaultCursor('pointer');
});
buttonBox.on('pointerout', () => {
buttonBox.setFillStyle(0x000000, 1);
this.input.setDefaultCursor('default');
});
}
}
class SceneA extends Phaser.Scene
{
ship;
flame;
constructor ()
{
super({ key: 'SceneA' });
}
init ()
{
this.cameras.main.fadeIn(100);
const fxCamera = this.cameras.main.filters.external.addPixelate(40);
this.add.tween({
targets: fxCamera,
duration: 700,
amount: -1,
});
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.setPath('assets/');
this.load.image("bg1", "skies/pixelsky.png");
this.load.image("bg2", "skies/space3.png");
this.load.image("ship", "sprites/x2kship.png");
this.load.atlas('flares', '/particles/flares.png', '/particles/flares.json');
this.load.image("planet", "tests/space/blue-planet.png");
}
create ()
{
const bg = this.add.image(0, 0, "bg2")
.setOrigin(0);
this.ship = this.add.image(200, 100, "ship")
.setScale(1.5);
// FX
const pixelated = this.cameras.main.filters.external.addPixelate(-1);
// Create button
const buttonBox = this.add.rectangle(this.sys.scale.width / 2, this.sys.scale.height - 100, 290, 50, 0x000000, 1);
buttonBox.setInteractive();
const buttonText = this.add.text(this.sys.scale.width / 2, this.sys.scale.height - 100, "Click to Change Scene").setOrigin(0.5);
// Click to change scene
buttonBox.on('pointerdown', () => {
// Transition to next scene
this.add.tween({
targets: pixelated,
duration: 700,
amount: 40,
onComplete: () => {
this.cameras.main.fadeOut(100);
this.scene.start('SceneB');
}
})
});
// Hover button properties
buttonBox.on('pointerover', () => {
buttonBox.setFillStyle(0x222222, 1);
this.input.setDefaultCursor('pointer');
});
buttonBox.on('pointerout', () => {
buttonBox.setFillStyle(0x000000, 1);
this.input.setDefaultCursor('default');
});
this.flame = this.add.particles(this.ship.x -65, this.ship.y, 'flares',
{
frame: 'white',
color: [ 0xfacc22, 0xf89800, 0xf83600, 0x9f0404 ],
colorEase: 'quad.out',
lifespan: 1000,
angle: { min: 175, max: 185 },
scale: { start: 0.40, end: 0, ease: 'sine.out' },
speed: 200,
advance: 2000,
blendMode: 'ADD'
});
}
update ()
{
// Wrap ship
this.ship.x = Phaser.Math.Wrap(this.ship.x + 1, 1, this.sys.scale.width + 50);
this.flame.setPosition(this.ship.x -65, this.ship.y);
}
}
const config = {
type: Phaser.AUTO,
width: 700,
height: 500,
pixelArt: true,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [SceneA, SceneB]
};
const game = new Phaser.Game(config);
Идея перехода: скрыть смену контента
Вместо мгновенного переключения между сценами (scene.start) мы можем визуально подготовить игрока. Идея в том, чтобы в начале сцены быстро "собрать" изображение из пикселей, а в конце — наоборот, "разобрать" его. Это скрывает потенциальные задержки и выглядит как осмысленный эффект.
Фильтр Pixelate из пространства имен this.cameras.main.filters.external идеально подходит для этой задачи. Он имеет параметр amount, который контролирует размер пикселей. Ключевая магия заключается в двух значениях:
- amount: 40 — сильная, заметная пикселизация.
- amount: -1 — фильтр отключен (исходное изображение).
Анимируя этот параметр с помощью tween, мы создаем плавный переход.
Структура сцены: init, create и фильтр
Каждая сцена (SceneA, SceneB) построена по единому шаблону, что делает код предсказуемым.
Метод init() выполняется до create() и идеально подходит для инициализации перехода. Здесь мы сразу применяем фильтр с сильной пикселизацией и запускаем твин, который за 700 миллисекунд убирает эффект, «собирая» картинку на глазах у игрока.
init ()
{
this.cameras.main.fadeIn(100);
const fxCamera = this.cameras.main.filters.external.addPixelate(40);
this.add.tween({
targets: fxCamera,
duration: 700,
amount: -1,
});
}
В методе create() мы настраиваем основной игровой контент (фон, объекты) и, что важно, создаем экземпляр фильтра с параметром -1. Этот экземпляр мы сохраняем в переменную (например, pixelated), чтобы позднее анимировать его для обратного перехода. Фильтр уже добавлен в камеру, но не активен.
const pixelated = this.cameras.main.filters.external.addPixelate(-1);
Механика перехода по клику
В каждой сцене создается интерактивная кнопка. Её логика — сердце перехода. При клике (pointerdown) мы анимируем ранее созданный фильтр pixelated, увеличивая его amount с -1 до 40. Это создает эффект «разложения» изображения на пиксели.
buttonBox.on('pointerdown', () => {
this.add.tween({
targets: pixelated,
duration: 700,
amount: 40,
onComplete: () => {
this.cameras.main.fadeOut(100);
this.scene.start('SceneA'); // или 'SceneB'
}
})
});
Обратите внимание на колбэк onComplete. Как только анимация пикселизации завершена, мы запускаем короткое затемнение камеры (fadeOut) и только затем переключаем сцену командой this.scene.start. Это добавляет ещё один слой плавности: сцена меняется уже затемнённой, что полностью скрывает любые артефакты переключения.
Важные детали реализации
Для корректной работы фильтров и общего визуального стиля в конфигурации игры выставлена критически важная настройка:
pixelArt: true,
Она отключает линейную интерполяцию текстур при масштабировании, что сохраняет чёткие пиксельные границы. Без этого настройки фильтр Pixelate может выглядеть размытым.
Фильтры применяются к объекту this.cameras.main.filters.external. Это специальный контейнер для пост-обработки, который рендерится после отрисовки всей сцены. Добавление фильтра через .addPixelate() автоматически включает этот конвейер.
Также в примере используется простое, но эффективное создание кнопки из прямоугольника (add.rectangle) и текста (add.text). Прямоугольнику назначаются обработчики pointerover и pointerout для изменения цвета и курсора, что обеспечивает базовую, но достаточную интерактивность.
buttonBox.on('pointerover', () => {
buttonBox.setFillStyle(0x222222, 1);
this.input.setDefaultCursor('pointer');
});
Дополнительные эффекты: частицы и движение
Пример не ограничивается переходами. В SceneA демонстрируются другие возможности Phaser для оживления сцены.
Система частиц создаёт пламя позади корабля. Частицы привязаны к позиции корабля в методе update(), создавая иллюзию работающего двигателя.
this.flame = this.add.particles(this.ship.x -65, this.ship.y, 'flares',
{
frame: 'white',
color: [ 0xfacc22, 0xf89800, 0xf83600, 0x9f0404 ],
// ... другие параметры
blendMode: 'ADD'
});
Движение корабля реализовано через Phaser.Math.Wrap в update(). Это заставляет корабль плавно перемещаться по горизонтали и, достигнув правой границы, появляться слева.
this.ship.x = Phaser.Math.Wrap(this.ship.x + 1, 1, this.sys.scale.width + 50);
Эти элементы показывают, как активная, живая сцена сочетается с кинематографичными переходами.
Что попробовать дальше
Использование фильтров камеры для переходов — мощный и элегантный приём в Phaser 3. Вы можете адаптировать эту технику, меняя фильтр (например, на размытие Blur или сепию ColorMatrix) или комбинируя несколько эффектов. Экспериментируйте с длительностью твинов, добавляйте звуковые эффекты в колбэки onComplete или связывайте силу пикселизации со скоростью нажатия на кнопку для создания более динамичных переходов. Этот подход универсален и значительно улучшит пользовательский опыт в вашем проекте.
