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

Визуальные эффекты, созданные с помощью шейдеров, могут оживить любую игру. Но что, если совместить их с физическим движком? Этот пример демонстрирует, как превратить шейдер из статичной картинки в полноценный физический объект, который может отскакивать от границ мира и сталкиваться с другими объектами. Вы научитесь создавать интерактивные визуальные элементы, которые не просто украшают сцену, а являются её активной частью.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.glsl('Marble', 'assets/shaders/marble.frag');
        this.load.glsl('Plasma', 'assets/shaders/plasma2.frag');
    }

    create ()
    {
        //  Our dynamic shader that will bounce around
        const block = this.add.shader({
            name: 'Marble',
            fragmentKey: 'Marble',
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            }
        }, 0, 0, 32 * 4, 32 * 2);

        this.physics.add.existing(block, false);

        block.body.setVelocity(130, 180);
        block.body.setBounce(1, 1);
        block.body.setCollideWorldBounds(true);

        //  Our static shader that will just receive collide events
        const staticBlock = this.add.shader({
            name: 'Plasma',
            fragmentKey: 'Plasma',
            initialUniforms: {
                resolution: [ 32 * 3, 32 * 8 ]
            },
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            }
        }, 400, 300, 32 * 3, 32 * 8);

        this.physics.add.existing(staticBlock, true);

        this.physics.add.collider(block, staticBlock);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: true,
            gravity: { y: 200 }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка и настройка: готовим шейдеры

Перед использованием шейдеров их необходимо загрузить в память. В методе preload() мы используем метод this.load.glsl(). В отличие от загрузки изображений, этот метод загружает файлы шейдеров с расширением .frag (фрагментные шейдеры). Мы загружаем два разных шейдера под ключами 'Marble' и 'Plasma'. Эти ключи будут использоваться позже для их создания.

this.load.glsl('Marble', 'assets/shaders/marble.frag');
this.load.glsl('Plasma', 'assets/shaders/plasma2.frag');

Создание динамического шейдера с физикой

Первый шейдер будет динамическим объектом. Мы создаём его с помощью this.add.shader(). Конфигурационный объект позволяет задать имя (name), ключ загруженного фрагментного шейдера (fragmentKey) и функцию setupUniforms для обновления uniform-переменных (например, времени time) каждый кадр. Важно указать координаты (x, y) и размеры (width, height) объекта.

const block = this.add.shader({
    name: 'Marble',
    fragmentKey: 'Marble',
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 0, 0, 32 * 4, 32 * 2);

Чтобы шейдер мог двигаться и сталкиваться, мы добавляем к нему физическое тело с помощью this.physics.add.existing(). Второй аргумент false указывает, что объект не является статическим. Затем мы задаём ему начальную скорость, упругость и включаем столкновение с границами мира.

this.physics.add.existing(block, false);
block.body.setVelocity(130, 180);
block.body.setBounce(1, 1);
block.body.setCollideWorldBounds(true);

Создание статичного шейдера-препятствия

Второй шейдер будет статичным препятствием. Процесс создания похож, но есть ключевые отличия. Во-первых, мы используем другой шейдер ('Plasma'). Во-вторых, мы задаём начальные uniform-переменные (например, resolution) через объект initialUniforms. Это полезно для параметров, которые не меняются каждый кадр.

const staticBlock = this.add.shader({
    name: 'Plasma',
    fragmentKey: 'Plasma',
    initialUniforms: {
        resolution: [ 32 * 3, 32 * 8 ]
    },
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 400, 300, 32 * 3, 32 * 8);

При добавлении физики вторым аргументом передаём true, чтобы сделать тело статичным. Такое тело не двигается под действием сил, но может участвовать в столкновениях.

this.physics.add.existing(staticBlock, true);

Оркестровка столкновений

Теперь нужно заставить объекты взаимодействовать. Для этого используется коллайдер. Метод this.physics.add.collider() регистрирует столкновение между двумя физическими телами. При столкновении динамического шейдера block со статическим staticBlock Arcade Physics автоматически рассчитает отскок, исходя из свойств тел (в данном случае, упругости, заданной ранее).

this.physics.add.collider(block, staticBlock);

Коллайдер — это основная сущность для управления взаимодействиями в физическом мире Phaser.

Настройка физического мира

Вся магия физики активируется настройками в конфигурации игры. В объекте physics мы указываем, что используем систему 'arcade'. В её настройках можно включить отладку (debug: true), чтобы видеть контуры тел, и задать глобальную гравитацию.

physics: {
    default: 'arcade',
    arcade: {
        debug: true,
        gravity: { y: 200 }
    }
}

Гравитация в этом примере действует на все динамические тела, но её эффект перекрывается упругими отскоками от границ и статичного объекта, создавая хаотичное движение.

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

Вы только что создали сцену, где сложные визуальные эффекты подчиняются законам физики. Это открывает двери для множества экспериментов: попробуйте изменить uniform-переменные шейдера в зависимости от скорости столкновения, создайте поле из множества статичных шейдеров-платформ или замените отскок на уничтожение объекта при коллизии. Комбинируя мощь GLSL-шейдеров и простоту Arcade Physics, можно создавать поистине уникальные игровые механики.