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

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

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

Живой запуск

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

Исходный код


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

        this.rope;
        this.count = 0;
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('patchouli', 'assets/rope/patchouli.png');
        this.load.image('wwy', 'assets/rope/weathering-with-you.jpg');
    }

    create ()
    {
        const rope = this.add.rope(400, 300, 'wwy', null, 16).setScale(0.7);

        const mask = this.make.image({ x: 200, y: 300, key: 'patchouli' }, false);

        rope.enableFilters().filters.external.addMask(mask);

        this.rope = rope;

        this.tweens.add({
            targets: mask,
            x: 600,
            ease: 'sine.inOut',
            yoyo: true,
            duration: 2000,
            repeat: -1
        });

        this.add.text(10, 10, 'A Rope with a mask', { font: '16px Courier', fill: '#ffffff' }).setShadow(1, 1);
    }

    update ()
    {
        this.count += 0.1;

        let points = this.rope.points;

        for (let i = 0; i < points.length; i++)
        {
            if (this.rope.horizontal)
            {
                points[i].y = Math.sin(i * 0.5 + this.count) * 10;
            }
            else
            {
                points[i].x = Math.sin(i * 0.5 + this.count) * 20;
            }
        }

        this.rope.setDirty();
    }
}

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

let game = new Phaser.Game(config);

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

В методе preload загружаются два изображения: одно для текстуры веревки ('wwy'), другое — для маски ('patchouli'). Базовый URL задается через this.load.setBaseURL, что удобно для примеров с общими ресурсами.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('patchouli', 'assets/rope/patchouli.png');
    this.load.image('wwy', 'assets/rope/weathering-with-you.jpg');
}

Создание Rope и наложение маски

В create создается основной объект — this.add.rope. Его параметры: позиция (400, 300), ключ текстуры, массив точек (здесь null, что означает равномерное распределение) и количество сегментов (16). Масштаб уменьшен до 0.7.

Маска создается через this.make.image. Это объект Image, который не добавляется автоматически на дисплей. Ключевой момент: метод rope.enableFilters() активирует систему фильтров для этого Rope. Затем к внешним фильтрам (filters.external) добавляется маска с помощью .addMask(mask).

const rope = this.add.rope(400, 300, 'wwy', null, 16).setScale(0.7);
const mask = this.make.image({ x: 200, y: 300, key: 'patchouli' }, false);
rope.enableFilters().filters.external.addMask(mask);
this.rope = rope;

Анимация маски и текстовой подсказки

Чтобы маска двигалась, на нее добавляется твин. Он перемещает маску по оси X от 200 до 600 пикселей с плавным sine.inOut движением, повторяясь бесконечно (repeat: -1) и возвращаясь назад (yoyo: true). Также на сцену выводится поясняющий текст.

this.tweens.add({
    targets: mask,
    x: 600,
    ease: 'sine.inOut',
    yoyo: true,
    duration: 2000,
    repeat: -1
});

this.add.text(10, 10, 'A Rope with a mask', { font: '16px Courier', fill: '#ffffff' }).setShadow(1, 1);

Анимация вершин Rope (волновой эффект)

В методе update происходит главная магия. Счетчик this.count увеличивается каждому кадру, создавая основу для анимации. Мы получаем массив точек Rope через this.rope.points. Каждая точка — объект с координатами `xиy`.

Цикл проходит по всем точкам. В зависимости от флага this.rope.horizontal (который в данном примере не меняется и, вероятно, равен false по умолчанию, что означает вертикальную веревку), анимируется либо координата `x, либоy. Используется синусоидальная функцияMath.sin`, которая зависит от индекса точки и общего счетчика. Это создает бегущую волну.

После изменения координат необходимо вызвать this.rope.setDirty(), чтобы Phaser знал, что геометрия объекта изменилась и ее нужно перерисовать.

update ()
{
    this.count += 0.1;
    let points = this.rope.points;
    for (let i = 0; i < points.length; i++)
    {
        if (this.rope.horizontal)
        {
            points[i].y = Math.sin(i * 0.5 + this.count) * 10;
        }
        else
        {
            points[i].x = Math.sin(i * 0.5 + this.count) * 20;
        }
    }
    this.rope.setDirty();
}

Конфигурация игры

Стандартная конфигурация Phaser Game. Важно, что сцена (scene) указывается как класс Example. Фон задан темно-серым цветом для контраста.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};
let game = new Phaser.Game(config);

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

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