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

При работе с визуальными эффектами в Phaser 3 разработчики часто сталкиваются с необходимостью привязывать фильтры к динамичным объектам. Ручное управление областью применения фильтра может быть утомительным. В этой статье мы разберем встроенный механизм Phaser — автоматический фокус фильтров на трансформируемых объектах. Вы узнаете, как легко добавлять эффекты, которые "следуют" за спрайтами при их движении, вращении и изменении масштаба, без лишнего кода для обновления координат фильтра.

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

Живой запуск

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

Исходный код


// This example demonstrates that transformed objects are automatically focused on by filters.

class Example extends Phaser.Scene
{
    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('thrust_ship', 'assets/sprites/thrust_ship.png');
        this.load.image('sky', 'assets/skies/pixelsky.png');
    }

    create ()
    {
        // Add a sky background.
        this.add.image(640, 360, 'sky');

        // Create a series of spaceships.
        this.ships = [];
        for (let i = 0; i < 8; i++)
        {
            const ship = this.add.image(Phaser.Math.Between(0, 1280), Phaser.Math.Between(0, 720), 'thrust_ship')
            .setScale(4)
            .setRotation(Phaser.Math.FloatBetween(0, Math.PI * 2));
            this.ships.push(ship);

            ship.enableFilters();

            const colorMatrix = ship.filters.internal.addColorMatrix();
            colorMatrix.colorMatrix.hue(Phaser.Math.Between(0, 360));

            const blur = ship.filters.internal.addBlur(1, 1, 0);
            blur.setPaddingOverride(-8, 0, 8, 0);

            // Cache properties on the ship.
            ship.blur = blur;
            ship.thrust = 0;
            ship.steering = Phaser.Math.FloatBetween(0.01, 0.02);
            ship.power = Phaser.Math.FloatBetween(0.005, 0.02);
            ship.friction = Phaser.Math.FloatBetween(0.985, 0.995);
        }

        // Add keyboard controls.
        this.keys = this.input.keyboard.createCursorKeys();
        this.wrapRect = new Phaser.Geom.Rectangle(0 - 128, 0 - 128, 1280 + 128, 720 + 128);
    }

    update (time, delta)
    {
        const d = delta / 16.666;

        // Get controls.
        const up = this.keys.up.isDown;
        const down = this.keys.down.isDown;
        const left = this.keys.left.isDown;
        const right = this.keys.right.isDown;

        // Update the spaceships.
        for (const ship of this.ships)
        {
            // Get properties.
            const { blur, steering, power, friction } = ship;
            let { thrust } = ship;

            // Update the spaceship.
            if (up)
            {
                thrust += power * d;
            }
            else
            {
                thrust *= friction / Math.max(d, 1);
            }
            if (down)
            {
                thrust -= power * d;
            }
            if (left)
            {
                ship.rotation -= steering * d * (thrust + 1);
            }
            if (right)
            {
                ship.rotation += steering * d * (thrust + 1);
            }

            thrust = Phaser.Math.Clamp(thrust, 0, 1);

            ship.x += Math.cos(ship.rotation) * thrust * d * 15;
            ship.y += Math.sin(ship.rotation) * thrust * d * 15;

            blur.strength = thrust;
            ship.thrust = thrust;
        }
        
        Phaser.Actions.WrapInRectangle(this.ships, this.wrapRect);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 1280,
    height: 720,
    parent: 'phaser-example',
    scene: Example,
    smoothPixelArt: true
};

let game = new Phaser.Game(config);

Суть автоматического фокуса

В Phaser 3, когда фильтр применяется к игровому объекту (например, через enableFilters()), система автоматически настраивает область (viewport) этого фильтра так, чтобы она охватывала текущие границы объекта с учетом всех его трансформаций: позиции (`x,y), вращения (rotation) и масштаба (scale`). Это и есть "автофокус".

Фильтр не просто накладывается на статичную текстуру спрайта. Он работает в собственном координатном пространстве, которое динамически пересчитывается каждый кадр, следуя за объектом. Это избавляет разработчика от необходимости вручную синхронизировать позицию фильтра с позицией спрайта, что критично для движущихся объектов.

Подготовка сцены и создание объектов

В примере создается группа из 8 космических кораблей. Каждому кораблю присваиваются случайные начальные позиция и вращение. Важный шаг — активация системы фильтров для объекта.

ship.enableFilters();

Этот вызов создает для объекта ship контейнер filters.internal, в который можно добавлять различные эффекты. Без этого вызова фильтры применять нельзя.

Далее, для каждого корабля создаются два фильтра: цветовой матрицы (для изменения оттенка) и размытия. Обратите внимание, как настраивается отступ (padding) у фильтра размытия. Это нужно, чтобы эффект размытия не обрезался по границам текстуры спрайта.

const colorMatrix = ship.filters.internal.addColorMatrix();
colorMatrix.colorMatrix.hue(Phaser.Math.Between(0, 360));

const blur = ship.filters.internal.addBlur(1, 1, 0);
blur.setPaddingOverride(-8, 0, 8, 0);

Фильтр размытия сохраняется прямо на объекте корабля (ship.blur = blur), чтобы позднее менять его силу в зависимости от "тяги".

Динамическое обновление и связь с фильтром

Логика управления в update демонстрирует, как состояние игрового объекта влияет на параметры фильтра. "Тяга" корабля (thrust) рассчитывается на основе ввода с клавиатуры и физики (ускорение, трение).

Ключевой момент — сила размытия (blur.strength) напрямую привязывается к значению тяги.

blur.strength = thrust;
ship.thrust = thrust;

Поскольку фильтр "сфокусирован" на корабле (благодаря enableFilters()), изменение его параметра strength немедленно визуализируется в правильном месте на экране, куда бы корабль ни переместился. Система автофокуса гарантирует, что область применения фильтра (blur) всегда соответствует текущему положению и вращению спрайта ship.

Метод Phaser.Actions.WrapInRectangle обеспечивает телепортацию кораблей с одной стороны экрана на другую, и фильтры продолжают корректно работать после телепортации.

Практические аспекты и производительность

Автофокус — мощная, но требующая ресурсов функция. Каждый кадр для каждого объекта с фильтрами Phaser пересчитывает его границы и обновляет viewport фильтра.

* **Когда использовать:** Для динамических объектов, которые двигаются, вращаются или меняют масштаб (игроки, враги, снаряды). * **Когда избегать:** Для статичных элементов интерфейса или фоновых декораций. Для них лучше использовать фильтры на уровне камеры или сцены, где область применения фиксирована.

Важно помнить про setPaddingOverride. Фильтры вроде размытия или свечения работают с областью больше исходного спрайта. Если не задать отступы, эффект будет обрезан. В примере отступы заданы вручную, но можно использовать blur.setAutoPadding() для автоматической настройки на основе силы эффекта.

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

Автоматический фокус фильтров в Phaser 3 — это элегантное решение для привязки визуальных эффектов к движущимся объектам. Оно минимизирует boilerplate-код и позволяет сосредоточиться на творческой части — настройке поведения и визуала. **Идеи для экспериментов:** 1. Добавьте фильтр свечения (Glow), интенсивность которого зависит от здоровья корабля. 2. Свяжите параметр hue в цветовой матрице со скоростью вращения корабля. 3. Примените автофокус не к Image, а к Container с группой спрайтов и посмотрите, как фильтр будет следовать за границами всего контейнера. 4. Поэкспериментируйте с отключением автофокуса через filter.setAutoFocus(false) и попробуйте управлять положением фильтра вручную, чтобы ощутить разницу.