О чем этот пример
Процедурный шум — мощный инструмент для создания органичной, «живой» анимации в играх. Вместо жёстких математических формул он позволяет генерировать плавные, непредсказуемые и натуральные движения. В 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) для более сложного поведения.
