О чем этот пример

В сложных игровых проектах часто возникает необходимость показывать игроку несколько видов одновременно: основное поле, карту, инвентарь или камеру противника. 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. Управлять камерами не по временной шкале, а в ответ на ввод игрока.