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

Процедурный шум — мощный инструмент для создания органичной, «живой» анимации в играх. Вместо жёстких математических формул он позволяет генерировать плавные, непредсказуемые и натуральные движения. В Phaser для этого есть встроенный класс `Phaser.Math.HashSimplex`. Эта статья покажет, как с его помощью заставить листок реалистично парить на ветру, используя шум для расчёта скорости и поворота. Вы научитесь применять этот метод для анимации частиц, врагов, камеры или элементов окружения.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    config = {
        noiseCells: [ 16 / 2, 9 / 2 ],
        noiseIterations: 3,
        noiseWarpAmount: 0.5,
        noiseColorStart: 0x88aaff,
        noiseColorEnd: 0x4466aa,
    };
    leaf;
    noise;

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('leaf', 'assets/particles/leaf2.png');
    }

    create()
    {
        const { width, height } = this.scale;

        this.noise = this.add.noisesimplex2d(this.config, width / 2, height / 2, width, height);

        this.leaf = this.add.image(640, 200, 'leaf');

        this.leaf.enableFilters().filters.internal.addGlow(0x88aaff);
    }

    update (time, delta)
    {
        const { width, height } = this.scale;

        this.noise.noiseFlow = time / 500;
        this.config.noiseFlow = this.noise.noiseFlow;

        const contourX = Phaser.Math.HashSimplex([this.leaf.x / width, this.leaf.y / height], config);
        const contourY = Phaser.Math.HashSimplex([this.leaf.x / width, this.leaf.y / height], { ...config, noiseSeed: [ 3, 4 ] }) + 1;

        this.leaf.x += contourX * delta / 64;
        this.leaf.y += contourY * delta / 64;

        const angle = Phaser.Math.Angle.GetShortestDistance(this.leaf.rotation, Math.atan2(contourY, contourX));

        this.leaf.rotation += angle * 0.03;

        if (this.leaf.x < -100 || this.leaf.x > 1500) {
            this.leaf.x = 640;
            this.leaf.y = -100;
        }

        if (this.leaf.y > 900) { this.leaf.y = -100; }
        if (this.leaf.y < -300) { this.leaf.y = -100; }
    }
}

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

const game = new Phaser.Game(config);

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

В методе preload() загружается изображение листка, которое будет нашим анимируемым объектом. Важно использовать this.load.setBaseURL() для указания базового пути к ресурсам.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('leaf', 'assets/particles/leaf2.png');
}

В create() происходит основная настройка. Во-первых, создаётся фон на основе шума (Simplex2D Noise) с помощью this.add.noisesimplex2d(). Это визуализация того самого векторного поля, по которому будет двигаться листок. Во-вторых, создаётся сам спрайт листка и на него добавляется фильтр свечения для красоты.

create()
{
    const { width, height } = this.scale;
    this.noise = this.add.noisesimplex2d(this.config, width / 2, height / 2, width, height);
    this.leaf = this.add.image(640, 200, 'leaf');
    this.leaf.enableFilters().filters.internal.addGlow(0x88aaff);
}

Сердце анимации: обновление в update()

Вся магия происходит в методе update(time, delta), который вызывается каждый кадр. Ключевая идея: мы используем текущие координаты листка как входные данные для функции шума Phaser.Math.HashSimplex(). Эта функция возвращает значение шума (обычно от -1 до 1) в заданной точке.

Сначала мы обновляем свойство noiseFlow у визуализации фона и конфига, чтобы шум плавно «перетекал» со временем.

this.noise.noiseFlow = time / 500;
this.config.noiseFlow = this.noise.noiseFlow;

Затем вычисляем две компоненты вектора движения (contourX и contourY). Для этого дважды вызываем HashSimplex с разными параметрами. Второй вызов использует другой noiseSeed, чтобы значения по X и Y были независимыми, создавая движение по круговым траекториям, а не по прямой.

const contourX = Phaser.Math.HashSimplex([this.leaf.x / width, this.leaf.y / height], config);
const contourY = Phaser.Math.HashSimplex([this.leaf.x / width, this.leaf.y / height], { ...config, noiseSeed: [ 3, 4 ] }) + 1;

Полученные значения contourX и contourY — это и есть вектор направления. Мы умножаем их на delta / 64, чтобы движение было плавным и независимым от частоты кадров.

this.leaf.x += contourX * delta / 64;
this.leaf.y += contourY * delta / 64;

Поворот объекта по направлению движения

Чтобы листок не просто перемещался, а разворачивался по ходу движения (как на ветру), нужно вычислить угол между его текущим поворотом и вектором скорости.

Сначала вычисляем целевой угол вектора движения с помощью Math.atan2(contourY, contourX).

Затем находим кратчайший угол между текущим поворотом листка (this.leaf.rotation) и этим целевым углом. Для этого используется удобная функция Phaser Phaser.Math.Angle.GetShortestDistance(). Она возвращает разницу в радианах, учитывая закольцованность углов (например, разница между 359° и 1° будет 2°, а не 358°).

const angle = Phaser.Math.Angle.GetShortestDistance(this.leaf.rotation, Math.atan2(contourY, contourX));

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

this.leaf.rotation += angle * 0.03;

Базовая обработка границ экрана

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

if (this.leaf.x < -100 || this.leaf.x > 1500) {
    this.leaf.x = 640;
    this.leaf.y = -100;
}
if (this.leaf.y > 900) { this.leaf.y = -100; }
if (this.leaf.y < -300) { this.leaf.y = -100; }

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

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

Phaser.Math.HashSimplex открывает простой путь к созданию сложной, органичной анимации. Векторное поле, построенное на шуме, управляет движением объектов, делая его естественным и неповторяющимся. **Идеи для экспериментов:** 1. Примените этот метод к группе частиц (this.add.particles) для создания роя светлячков или пепла. 2. Используйте шум не для позиции, а для других свойств: масштаба, прозрачности (alpha) или цвета спрайта. 3. Создайте «течение» для камеры, чтобы она плавно дрейфовала за игроком в пошаговой стратегии. 4. Комбинируйте несколько слоёв шума с разными параметрами (noiseCells, noiseSeed) для более сложного поведения.