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

Использование DOM-элементов внутри игрового мира Phaser открывает новые возможности для создания гибридных интерфейсов, где стандартный HTML/CSS сочетается с игровой графикой и физикой. В этой статье мы разберем, как создавать динамические DOM-объекты, анимировать их в 3D-пространстве и реализовать плавное управление камерой для навигации по большой сцене. Этот подход полезен для создания интерактивных меню, HUD-элементов с современными CSS-эффектами или нестандартных визуальных представлений прямо внутри игрового контекста.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    controls;

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

    create ()
    {
        const smileys = [ '😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','️🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾' ];

        //  Create a bunch of random divs all over the place

        let sf = 0.5;
        let px = 64;

        for (let i = 1; i <= 100; i++)
        {
            const x = Phaser.Math.Between(100, 2000);
            const y = Phaser.Math.Between(100, 2000);

            const element = this.add.dom(x, y, 'div', `font-size: ${px}px`, Phaser.Utils.Array.GetRandom(smileys)).setScrollFactor(sf);

            element.setPerspective(800);

            element.rotate3d.set(Math.random(), Math.random(), Math.random(), 0);

            this.tweens.add({
                targets: element.rotate3d,
                w: 180,
                duration: 2000,
                ease: 'Sine.easeInOut',
                loop: -1,
                yoyo: true
            });

            if (i % 50 === 0)
            {
                sf += 0.1;
                px += 32;
            }
        }

        this.cameras.main.setBounds(0, 0, 4000, 4000);

        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);
    }

    update (time, delta)
    {
        this.controls.update(delta);
    }
}

const config = {
    type: Phaser.AUTO,
    backgroundColor: '#2d2d2d',
    scale: {
        _mode: Phaser.Scale.FIT,
        parent: 'phaser-example',
        width: 800,
        height: 600
    },
    dom: {
        createContainer: true
    },
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и загрузка ресурсов

Класс Example расширяет Phaser.Scene. В методе preload() устанавливается базовый URL для загрузки и загружается одно изображение, хотя в данном примере оно не используется — вся графика создается через DOM.

Важный момент: для работы с DOM-элементами в конфигурации игры необходимо включить соответствующую опцию.

dom: {
    createContainer: true
}

Без этого контейнера DOM-элементы не будут добавлены на страницу.

Создание массива DOM-элементов

В методе create() определяется массив смайликов — это обычные текстовые эмодзи. Далее в цикле создается 100 DOM-объектов.

Ключевой метод для добавления DOM-ноды — this.add.dom(x, y, element, style, content). Он принимает координаты, имя HTML-элемента (например, 'div'), строку стилей и содержимое.

const element = this.add.dom(x, y, 'div', `font-size: ${px}px`, Phaser.Utils.Array.GetRandom(smileys)).setScrollFactor(sf);

Здесь Phaser.Utils.Array.GetRandom(smileys) случайным образом выбирает смайлик из массива. Метод .setScrollFactor(sf) определяет, насколько элемент будет привязан к движению камеры. Фактор 0.5 означает, что элемент будет двигаться вполовину медленнее камеры, создавая эффект параллакса.

Каждые 50 элементов фактор прокрутки (sf) и размер шрифта (px) увеличиваются, создавая градацию размеров и скоростей.

3D-анимация элементов

Phaser позволяет применять к DOM-элементам простые 3D-трансформации через CSS. Метод setPerspective(800) устанавливает точку схода для 3D-преобразований.

У каждого элемента есть свойство rotate3d — это объект Phaser.Math.Vector4, представляющий ось вращения (x, y, z) и угол (w). Изначально ось задается случайным вектором, а угол равен 0.

element.rotate3d.set(Math.random(), Math.random(), Math.random(), 0);

Для анимации используется система твинов Phaser. Твин нацелен на свойство `w(угол) вектораrotate3d` и плавно меняет его от 0 до 180 градусов и обратно, создавая бесконечное покачивание элемента.

this.tweens.add({
    targets: element.rotate3d,
    w: 180,
    duration: 2000,
    ease: 'Sine.easeInOut',
    loop: -1,
    yoyo: true
});

Phaser автоматически преобразует изменение вектора rotate3d в CSS-свойство transform: rotate3d().

Настройка камеры и границ мира

Поскольку элементы разбросаны по большой области (до 2000 пикселей), необходимо задать границы для камеры, чтобы она могла по ним перемещаться.

this.cameras.main.setBounds(0, 0, 4000, 4000);

Это определяет прямоугольную область мира размером 4000x4000 пикселей, за пределы которой камера выйти не сможет.

Плавное управление камерой с клавиатуры

Для плавного, инерционного управления камерой используется встроенный класс Phaser.Cameras.Controls.SmoothedKeyControl. Сначала создается объект конфигурации, который связывает клавиши с действиями камеры.

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
};

Клавиши стрелок отвечают за движение, Q и E — за зум. Параметры acceleration (ускорение), drag (сопротивление) и maxSpeed (максимальная скорость) тонко настраивают 'физику' движения камеры, делая его приятным и нерезким.

Экземпляр контрола создается и сохраняется в свойстве сцены, а в методе update() каждый кадр вызывается его метод update(delta), передающий дельту времени для плавных расчетов движения.

this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

update (time, delta)
{
    this.controls.update(delta);
}

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

Комбинация DOM-элементов с игровым движком позволяет легко интегрировать веб-технологии в игровой процесс. Вы можете экспериментировать: замените смайлики на сложные HTML-виджеты с CSS-анимациями, привяжите вращение элементов к положению камеры или скорости игрока, создайте интерактивный DOM-объект, реагирующий на клики. Используйте SmoothedKeyControl не только для камеры, но и для управления объектами, где требуется плавный, инерционный разгон и торможение.