О чем этот пример
Визуальные эффекты, созданные с помощью процедурной генерации, могут оживить любую игру, добавив уникальности и динамики. В этой статье мы разберем, как использовать метод `Phaser.Math.HashSimplex` для создания плавно изменяющегося шума Перлина, который управляет движением и цветом сотен объектов на экране в реальном времени. Этот подход идеально подходит для создания эффектов волн, облаков, магических полей или живых фонов без использования тяжелых текстурных атласов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
color = new Phaser.Display.Color();
noiseConfigX = {
noiseCells: [ 16, 9, 4 ],
noiseSeed: [ 2, 3, 4 ]
};
noiseConfigY = { ...this.noiseConfigX, noiseSeed: [ 5, 6, 7 ] };
rectangles = [];
create()
{
const { width, height } = this.scale;
// Increase step to 16 or 32 to decrease count if performance is unacceptable.
const step = 8;
for (let y = 0; y <= height; y += step)
{
for (let x = 0; x <= width; x += step)
{
const vector = [ x / width, y / height ];
const rectangle = this.add.rectangle(x, y, 4, 4, 0xff0000);
rectangle.baseVector = [ x, y ];
rectangle.noiseVector = vector;
this.rectangles.push(rectangle);
}
}
}
update(time)
{
// Update pre-existing configs,
// because creating many objects is expensive.
this.noiseConfigX.noiseFlow = time / 1000;
this.noiseConfigY.noiseFlow = time / 1000;
const noiseConfigX = this.noiseConfigX;
const noiseConfigY = this.noiseConfigY;
const color = this.color;
const { width, height } = this.scale;
for (const rectangle of this.rectangles)
{
const vector = rectangle.noiseVector;
const hashX = Phaser.Math.HashSimplex(vector, noiseConfigX);
const hashY = Phaser.Math.HashSimplex(vector, noiseConfigY);
rectangle.x = rectangle.baseVector[0] + hashX * 8;
rectangle.y = rectangle.baseVector[1] + hashY * 8;
color.setGLTo(
hashX / 2 + (rectangle.baseVector[0] / width) - (rectangle.baseVector[1] / height / 2),
(rectangle.baseVector[1] / height) - (rectangle.baseVector[0] / width / 2),
0.5 + hashY / 2
);
rectangle.setFillStyle(color.color);
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1280,
height: 720,
scene: Example
};
const game = new Phaser.Game(config);
Создание сетки из прямоугольников
В методе create() мы создаем основу для нашего визуального эффекта — плотную сетку из маленьких прямоугольников, покрывающую весь экран. Размер шага (step) определяет плотность сетки: чем меньше шаг, тем больше объектов и выше детализация, но и выше нагрузка на производительность.
Каждому созданному прямоугольнику мы присваиваем три ключевых свойства:
1. baseVector: исходные координаты [x, y] на экране.
2. noiseVector: нормализованные координаты (от 0 до 1), которые будут использоваться как входные данные для функции шума.
Все созданные объекты сохраняются в массив this.rectangles для последующего обновления.
create()
{
const { width, height } = this.scale;
const step = 8;
for (let y = 0; y <= height; y += step)
{
for (let x = 0; x <= width; x += step)
{
const vector = [ x / width, y / height ];
const rectangle = this.add.rectangle(x, y, 4, 4, 0xff0000);
rectangle.baseVector = [ x, y ];
rectangle.noiseVector = vector;
this.rectangles.push(rectangle);
}
}
}
Анимация: Шум как источник движения
В методе update() происходит магия. Вместо создания новых конфигурационных объектов каждый кадр (что ресурсоемко), мы модифицируем существующие. Параметр noiseFlow устанавливается на основе времени, что заставляет шум "течь" и плавно меняться.
Затем для каждого прямоугольника в массиве:
1. Из его noiseVector и конфигов вычисляются два значения шума с помощью Phaser.Math.HashSimplex — для смещения по X и Y.
2. Эти значения (в диапазоне примерно от -1 до 1) умножаются на коэффициент (здесь 8) и добавляются к исходным координатам, создавая волнообразное движение.
update(time)
{
this.noiseConfigX.noiseFlow = time / 1000;
this.noiseConfigY.noiseFlow = time / 1000;
const noiseConfigX = this.noiseConfigX;
const noiseConfigY = this.noiseConfigY;
for (const rectangle of this.rectangles)
{
const vector = rectangle.noiseVector;
const hashX = Phaser.Math.HashSimplex(vector, noiseConfigX);
const hashY = Phaser.Math.HashSimplex(vector, noiseConfigY);
rectangle.x = rectangle.baseVector[0] + hashX * 8;
rectangle.y = rectangle.baseVector[1] + hashY * 8;
Динамическая окраска на основе шума
Движение — это только половина эффекта. Цвет каждого прямоугольника также вычисляется динамически на основе его позиции и значений шума. Используется метод color.setGLTo(), который задает цвет в формате WebGL (значения red, green, blue от 0 до 1).
Формула для каждого канала комбинирует:
- Нормализованные координаты прямоугольника (baseVector[0] / width), создающие градиент.
- Значения шума (hashX, hashY), добавляющие вариативность и связь с движением.
Полученный цвет применяется к прямоугольнику через rectangle.setFillStyle(). Это создает сложную, живую цветовую палитру, которая эволюционирует вместе с движением.
color.setGLTo(
hashX / 2 + (rectangle.baseVector[0] / width) - (rectangle.baseVector[1] / height / 2),
(rectangle.baseVector[1] / height) - (rectangle.baseVector[0] / width / 2),
0.5 + hashY / 2
);
rectangle.setFillStyle(color.color);
}
}
Инициализация игры и конфигурация
Финальный шаг — создание экземпляра игры Phaser с необходимой конфигурацией. Ключевой момент — использование type: Phaser.WEBGL. WebGL-рендерер критически важен для производительности при отрисовке тысяч динамически окрашиваемых объектов в реальном времени. Canvas-рендерер может не справиться с такой нагрузкой.
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1280,
height: 720,
scene: Example
};
const game = new Phaser.Game(config);
Что попробовать дальше
Метод Phaser.Math.HashSimplex открывает мощный инструментарий для процедурной генерации контента прямо во время выполнения игры. Вы можете экспериментировать: измените step для контроля производительности, поиграйте с множителями движения (hashX * 8) для изменения амплитуды "волн", модифицируйте формулу цвета для создания разных настроений (огненные, ледяные, ядовитые эффекты) или привяжите шум не к прямоугольникам, а к спрайтам противников для создания "роящегося" поведения. Попробуйте использовать одно значение шума для управления масштабом или вращением объектов.
