О чем этот пример
В сложных игровых проектах часто возникает необходимость показывать игроку несколько видов одновременно: основное поле, карту, инвентарь или камеру противника. Phaser позволяет создавать несколько камер, но управлять их сложной анимацией из разных сцен неудобно. В этой статье разберем паттерн «Контроллер», который выносит управление камерами в отдельную служебную сцену. Это делает код чище, анимации — синхронизированными, а логику — переиспользуемой. Вы научитесь централизованно управлять движением, зумом и перестановкой нескольких камер, создавая профессиональные многоэкранные композиции.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Controller extends Phaser.Scene
{
constructor ()
{
super({ key: 'Controller', active: true });
this.camera1;
this.camera2;
this.camera3;
this.camera4;
}
create ()
{
this.camera1 = this.scene.get('DemoA').cameras.main;
this.camera2 = this.scene.get('DemoB').cameras.main;
this.camera3 = this.scene.get('DemoC').cameras.main;
this.camera4 = this.scene.get('DemoD').cameras.main;
this.run();
}
run ()
{
this.slideLeft(2000, 3000);
this.slideUp(2000, 6000);
this.slideRight(2000, 9000);
this.slideCenter(2000, 12000);
this.slideTopLeft(2000, 16000);
this.zoomOut(2000, 20000);
this.exchange1(2000, 23000);
this.exchange2(2000, 26000);
this.exchange3(2000, 29000);
this.zoomIn(2000, 32000);
this.time.addEvent({ delay: 34000, callback: this.run, callbackScope: this, repeat: -1 });
}
slideLeft (speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
x: '-=800',
ease: 'Power1',
duration: speed,
delay: delay
});
}
slideRight(speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
x: '+=800',
ease: 'Power1',
duration: speed,
delay: delay
});
}
slideUp(speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
y: '-=600',
ease: 'Power1',
duration: speed,
delay: delay
});
}
slideDown(speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
y: '+=600',
ease: 'Power1',
duration: speed,
delay: delay
});
}
slideCenter(speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
x: '-=400',
y: '+=300',
ease: 'Power1',
duration: speed,
delay: delay
});
}
slideTopLeft(speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
x: '+=400',
y: '+=300',
ease: 'Power1',
duration: speed,
delay: delay
});
}
exchange1(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera1,
x: 400,
y: 300,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera4,
x: 0,
y: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
}
exchange2(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera2,
x: 0,
y: 300,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera3,
x: 400,
y: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
}
exchange3(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera1,
x: 0,
y: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera2,
x: 400,
y: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera3,
x: 0,
y: 300,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera4,
x: 400,
y: 300,
ease: 'Power1',
duration: speed,
delay: delay
});
}
zoomOut(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera1,
x: 0,
y: 0,
zoom: 0.5,
width: 400,
height: 300,
scrollX: 200,
scrollY: 150,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera2,
x: 400,
y: 0,
zoom: 0.5,
width: 400,
height: 300,
scrollX: 200,
scrollY: 150,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera3,
x: 0,
y: 300,
zoom: 0.5,
width: 400,
height: 300,
scrollX: 200,
scrollY: 150,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera4,
x: 400,
y: 300,
zoom: 0.5,
width: 400,
height: 300,
scrollX: 200,
scrollY: 150,
ease: 'Power1',
duration: speed,
delay: delay
});
}
zoomIn(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera1,
x: 0,
y: 0,
zoom: 1,
width: 800,
height: 600,
scrollX: 0,
scrollY: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera2,
x: 800,
y: 0,
zoom: 1,
width: 800,
height: 600,
scrollX: 0,
scrollY: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera3,
x: 0,
y: 600,
zoom: 1,
width: 800,
height: 600,
scrollX: 0,
scrollY: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
var tween = this.tweens.add({
targets: this.camera4,
x: 800,
y: 600,
zoom: 1,
width: 800,
height: 600,
scrollX: 0,
scrollY: 0,
ease: 'Power1',
duration: speed,
delay: delay
});
}
setCameraFull(cam, x, y)
{
cam.zoom = 1;
cam.x = x;
cam.y = y;
cam.width = 800;
cam.height = 600;
cam.scrollX = 0;
cam.scrollY = 0;
}
}
Архитектура контроллера: зачем отдельная сцена?
Исходный пример предполагает наличие пяти сцен: четыре демо-сцены (DemoA, DemoB, DemoC, DemoD) и одна управляющая (Controller). Демо-сцены отрисовывают свой контент, а контроллер — дирижирует их камерами.
Ключевая идея: камеры — это объекты, доступные из любой сцены. Метод this.scene.get('ключСцены').cameras.main возвращает главную камеру указанной сцены. Сохранив ссылки на эти камеры, мы можем управлять ими извне, как любыми другими объектами Phaser с помощью твинов.
Контроллер создан с флагом active: true, чтобы запуститься сразу и работать в фоне, не мешая другим сценам.
constructor ()
{
super({ key: 'Controller', active: true });
this.camera1;
this.camera2;
this.camera3;
this.camera4;
}
create ()
{
this.camera1 = this.scene.get('DemoA').cameras.main;
this.camera2 = this.scene.get('DemoB').cameras.main;
this.camera3 = this.scene.get('DemoC').cameras.main;
this.camera4 = this.scene.get('DemoD').cameras.main;
this.run();
}
Оркестровка твинов: создание сложной последовательности
Метод run() в контроллере — это партитура для нашей анимации. Он последовательно вызывает методы, отвечающие за разные движения, передавая им два параметра: длительность (speed) и задержку перед началом (delay). Все задержки суммируются, создавая единую временную шкалу.
Фишка подхода в использовании this.time.addEvent. После завершения последней анимации (через 34000 мс) событие по таймеру с параметром repeat: -1 запускает всю последовательность заново, создавая бесконечный цикл.
run ()
{
this.slideLeft(2000, 3000);
this.slideUp(2000, 6000);
// ... другие вызовы
this.zoomIn(2000, 32000);
this.time.addEvent({ delay: 34000, callback: this.run, callbackScope: this, repeat: -1 });
}
Групповое движение камер
Простейшие движения — сдвиг всех камер в одном направлении. Обратите внимание, что твин создается для массива targets. Phaser анимирует все объекты в этом массиве одновременно с одинаковыми параметрами. Это мощный инструмент для синхронных действий.
Свойства `xиyкамеры определяют её положение на конечном холсте (Viewport). Изменяя их, мы двигаем «окно» камеры по экрану. Операторы'-=800'и'+=800'` задают относительное изменение.
slideLeft (speed, delay)
{
var tween = this.tweens.add({
targets: [ this.camera1, this.camera2, this.camera3, this.camera4 ],
x: '-=800',
ease: 'Power1',
duration: speed,
delay: delay
});
}
Сложные обмены и индивидуальное управление
Методы exchange1, exchange2, exchange3 демонстрируют переход камер на новые позиции на экране. Здесь твины применяются к камерам по отдельности, но с одинаковой задержкой, создавая эффект их "перестановки" местами.
Важно: координаты (x: 400, y: 300) — это не координаты в игровом мире, а позиция viewport камеры на общем экране. Таким образом, каждая из четырех камер изначально занимает свой квадрант экрана 800x600, а в процессе обмена они меняются этими квадрантами.
exchange1(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera1,
x: 400,
y: 300,
ease: 'Power1',
duration: speed,
delay: delay
});
// ... твин для camera4
}
Анимация зума и скролла: меняем область видимости
Самые эффектные преобразования — zoomOut и zoomIn. Они меняют не только позицию viewport, но и его внутренние свойства.
* zoom: Масштаб камеры. Значение 0.5 уменьшает отображаемую область в два раза.
* width/height: Физический размер viewport камеры на экране. При зуме он уменьшается.
* scrollX/scrollY: Смещение камеры внутри игрового мира. Когда viewport уменьшается, чтобы сохранить центр обзора, нужно сдвинуть точку, за которой следит камера.
В примере при зуме камера уменьшается до размера 400x300, а её скролл сдвигается на (200, 150), что эффективно центрирует уменьшенный вид на той же точке мира.
zoomOut(speed, delay)
{
var tween = this.tweens.add({
targets: this.camera1,
x: 0,
y: 0,
zoom: 0.5,
width: 400,
height: 300,
scrollX: 200,
scrollY: 150,
ease: 'Power1',
duration: speed,
delay: delay
});
// ... твины для других камер
}
Что попробовать дальше
Паттерн «Контроллер» для камер — это чистый и мощный способ управления сложной визуальной логикой. Вы можете адаптировать его для создания split-screen режимов, динамических интро, переходов между меню и геймплеем или системы наблюдения за несколькими юнитами. Для экспериментов попробуйте: 1. Привязать анимации камер к событиям геймплея (например, зум при получении урона). 2. Создать плавное переключение между одиночной камерой и split-screen видом. 3. Реализовать камеру-«пипку» (picture-in-picture), следующую за второстепенным персонажем. 4. Управлять камерами не по временной шкале, а в ответ на ввод игрока.
