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

При разработке игр часто возникает задача разделения интерфейса и игрового мира. Например, нужно чтобы элементы HUD (здоровье, очки) не вращались вместе с камерой или не увеличивались при зуме. Phaser предлагает мощный механизм для такого разделения - фильтры камер (`cameraFilter`). Эта статья покажет, как использовать свойство `cameraFilter` игровых объектов и метод `setCamera()` для тонкого управления тем, в каких камерах отображаются объекты. Вы научитесь создавать независимые UI-камеры и назначать объекты только конкретным камерам.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


var UI_CAM_1;
var UI_CAM_2;

var UIText1;
var UIText2;
var UIText3;

class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('einstein', 'assets/pics/ra-einstein.png');
    }

    create ()
    {
        const image = this.add.image(400, 300, 'einstein');

        UIText1 = this.add.text(0, 32, '0');
        UIText2 = this.add.text(0, 64, '0');
        UIText3 = this.add.text(500, 64, '0');

        UI_CAM_1 = this.cameras.add();
        UI_CAM_2 = this.cameras.add();

        //list of all cameras
        this.camlist = this.cameras.cameras;

        //exclude gameobject to some camera
        UIText1.cameraFilter = this.setCamera(UI_CAM_1);
        UIText2.cameraFilter = this.setCamera(UI_CAM_1);
        UIText3.cameraFilter = this.setCamera(UI_CAM_2);

        image.cameraFilter = this.setCamera(this.cameras.main);
    }

    update ()
    {
        UIText1.setText("UI Camera 1");
        UIText2.setText("Main camera rotation: " + this.cameras.main.rotation);
        UIText3.setText("UI Camera 2");

        UI_CAM_1.scrollY = Math.sin(this.time.now / 100) * 10;
        UI_CAM_2.scrollX = Math.sin(this.time.now / 100) * 10;

        this.cameras.main.setZoom(Math.abs(Math.sin(this.cameras.main.rotation)) * 0.5 + 1);
        this.cameras.main.rotation += 0.01;
    }

    setCamera(cam)
    {
        let l = (1 << this.camlist.length) - 1;
        return l & ~cam.id;
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Что такое cameraFilter и зачем он нужен

Каждый игровой объект в Phaser (наследник GameObject) имеет свойство cameraFilter. Это 32-битное число, где каждый бит соответствует конкретной камере в игре. Если бит установлен в 0 - объект будет отображаться в этой камере, если в 1 - будет скрыт.

По умолчанию все объекты отображаются во всех камерах. Это удобно для большинства случаев, но становится проблемой при создании сложных сцен с несколькими камерами для разных целей (например, основная камера для мира + UI-камера для интерфейса).

// Пример: объект будет виден только в камере с id=0
UIText1.cameraFilter = 0b11111110; // Все биты кроме нулевого = 1

Создание и настройка дополнительных камер

В Phaser можно создавать любое количество камер через this.cameras.add(). Каждая новая камера получает уникальный идентификатор, который используется в фильтрах.

В нашем примере создаются две дополнительные камеры для UI элементов и используется основная камера (this.cameras.main) для фонового изображения.

// Создание дополнительных камер
UI_CAM_1 = this.cameras.add();
UI_CAM_2 = this.cameras.add();

// Получение списка всех камер сцены
this.camlist = this.cameras.cameras;

Все камеры по умолчанию имеют одинаковые размеры и положение, но их можно независимо настраивать - двигать, вращать, зумировать.

Метод setCamera() и работа с битовыми масками

Ключевой метод в примере - setCamera(cam), который вычисляет правильную битовую маску для фильтра камер. Он работает по принципу: "отображать объект во всех камерах, КРОМЕ указанной".

Давайте разберем логику метода:

setCamera(cam) {
    // Создаем маску где все биты установлены в 1
    // Например, для 3 камер: (1 << 3) - 1 = 0b111
    let l = (1 << this.camlist.length) - 1;
    
    // Инвертируем бит конкретной камеры
    // Например, для камеры с id=1: 0b111 & ~1 = 0b111 & 0b110 = 0b110
    return l & ~cam.id;
}

В битовой маске 0b110 (в двоичном виде): - Бит 0 = 0 → объект виден в камере с id=0 - Бит 1 = 1 → объект скрыт в камере с id=1 - Бит 2 = 0 → объект виден в камере с id=2

Этот метод вызывается для каждого объекта, чтобы назначить его конкретным камерам:

UIText1.cameraFilter = this.setCamera(UI_CAM_1);
image.cameraFilter = this.setCamera(this.cameras.main);

Динамическое обновление камер в update()

В методе update() демонстрируется независимое управление разными камерами. Каждая камера может иметь свои собственные трансформации, которые не влияют на объекты в других камерах.

update() {
    // UI_CAM_1 двигается по вертикали
    UI_CAM_1.scrollY = Math.sin(this.time.now / 100) * 10;
    
    // UI_CAM_2 двигается по горизонтали
    UI_CAM_2.scrollX = Math.sin(this.time.now / 100) * 10;
    
    // Основная камера вращается и зумируется
    this.cameras.main.setZoom(Math.abs(Math.sin(this.cameras.main.rotation)) * 0.5 + 1);
    this.cameras.main.rotation += 0.01;
}

Важно: хотя основная камера вращается, тексты UIText1 и UIText2 остаются неподвижными, потому что они назначены только UI_CAM_1. Текст UIText3 двигается горизонтально, так как он в UI_CAM_2. Изображение Эйнштейна вращается и зумируется вместе с основной камерой.

Практическое применение в играх

Техника фильтров камер особенно полезна в следующих сценариях:

1. **Разделение UI и игрового мира** - элементы интерфейса не должны вращаться/зумироваться с основной камерой 2. **Миникарты** - создание отдельной камеры для отображения карты в углу экрана 3. **Сплит-скрин** - несколько игроков на одном экране с разными камерами 4. **Эффекты пост-обработки** - применение шейдеров только к определенным камерам

Пример настройки HUD камеры:

// Создаем камеру для HUD
const hudCamera = this.cameras.add(0, 0, 800, 600);
hudCamera.setScroll(0, 0); // Фиксируем положение

// Назначаем элементы HUD только этой камере
healthBar.cameraFilter = this.setCamera(hudCamera);
scoreText.cameraFilter = this.setCamera(hudCamera);

Таким образом, при любых трансформациях основной камеры (дрожание, зум, вращение в босс-битвах) элементы интерфейса останутся стабильными.

Что попробовать дальше

Фильтры камер в Phaser 3 предоставляют мощный инструмент для управления видимостью объектов в многокамерных сценах. Используя битовые маски и свойство cameraFilter, вы можете создавать сложные композиции с независимыми слоями отображения. Для экспериментов попробуйте: 1. Создать систему из 3-4 камер с разными эффектами 2. Реализовать динамическое переключение фильтров во время игровых событий 3. Комбинировать фильтры камер с системами глубины (depth) для сложных UI-композиций 4. Создать камеру только для эффектов частиц, которые должны игнорировать зум основной камеры