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

Создание процедурного контента — ключевой навык в разработке игр. Часто нужно, чтобы сгенерированные элементы (цвета, текстуры, ландшафт) были разными, но при этом воспроизводимыми при каждом запуске игры. Обычный `Math.random()` даёт случайность, но не даёт стабильности. В этой статье мы разберём, как с помощью `Phaser.Math.Hash` создавать стабильные, детерминированные значения, например, для генерации цветовой палитры, которая будет одинаковой каждый раз.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    rects = [];

    create()
    {
        this.add.text(640, 64, 'Click to rebuild colors').setOrigin(0.5);

        this.populate();

        this.input.on('pointerdown', () => {
            for (const rect of this.rects)
            {
                rect.destroy();
            }
            this.populate();
        });
    }

    populate()
    {
        const color = new Phaser.Display.Color();

        for (let y = 128; y < 640; y += 32)
        {
            // Add random colors. They're always different.
            color.setGLTo(Math.random(), Math.random(), Math.random());
            const r1 = this.add.rectangle(320, y, 384, 32, color.color);
            this.rects.push(r1);

            // Add noise-derived colors. They're always the same.
            color.setGLTo(Phaser.Math.Hash(y), Phaser.Math.Hash(y + 1), Phaser.Math.Hash(y + 2));
            const r2 = this.add.rectangle(960, y, 384, 32, color.color);
            this.rects.push(r2);
        }
    }
}

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

const game = new Phaser.Game(config);

Проблема обычного рандома

Метод Math.random() отлично подходит для создания уникальных, непредсказуемых значений. Однако у него есть существенный недостаток для процедурной генерации: его результат нельзя воспроизвести. Каждый запуск сцены будет генерировать совершенно новую последовательность чисел.

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

color.setGLTo(Math.random(), Math.random(), Math.random());

Решение: Детерминированный хэш Phaser.Math.Hash

Phaser предоставляет простую, но мощную функцию Phaser.Math.Hash(). Она принимает любое числовое значение (seed) и возвращает псевдослучайное число в диапазоне от 0 до 1. Ключевое свойство: для одного и того же входного значения (seed) функция всегда вернёт один и тот же результат.

Это делает её идеальным инструментом для детерминированной, но визуально "случайной" генерации. В примере seed-ами являются координата `y` и её смещения.

color.setGLTo(Phaser.Math.Hash(y), Phaser.Math.Hash(y + 1), Phaser.Math.Hash(y + 2));

Как работает пример: Анатомия сцены

Сцена создаёт два столбца прямоугольников. Левый (x=320) использует Math.random(), правый (x=960) — Phaser.Math.Hash. По клику все прямоугольники уничтожаются и создаются заново, что позволяет увидеть разницу в поведении.

1. **Инициализация:** В create() вызывается метод populate() для первичного заполнения и устанавливается обработчик клика. 2. **Очистка:** При клике все объекты в массиве this.rects уничтожаются через rect.destroy(). 3. **Генерация:** Метод populate() проходит по вертикали с шагом в 32 пикселя. * Для левого столбца цвет задаётся тремя независимыми вызовами Math.random(). * Для правого столбца в качестве компонентов RGB используются результаты хэширования `y,y+1иy+2. Это гарантирует, что для строки с координатойy=160` цвет будет всегда одним и тем же.

this.input.on('pointerdown', () => {
    for (const rect of this.rects) {
        rect.destroy();
    }
    this.populate();
});

Практическое применение в играх

Стабильная генерация на основе хэша открывает множество возможностей:

* **Создание уникальных, но воспроизводимых миров:** Игровое семя (seed) может хэшироваться для генерации ландшафта, расстановки врагов или предметов. Все игроки, введя один seed, увидят одинаковый мир. * **Генерация цветовых палитр для фракций или биомов:** Цвета дома игрока, команды или типа местности могут быть вычислены на основе их ID. * "Случайный" выбор, который нужно запомнить:** Например, порядок карт в колоде для режима реплея.

// Цвет фракции на основе её ID
const factionColor = Phaser.Display.Color.HSLToColor(
    Phaser.Math.Hash(factionId), // Стабильный Hue
    0.8, // Насыщенность
    0.6  // Светлота
);

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

Phaser.Math.Hash — это простой и эффективный способ добавить детерминированную процедурность в вашу игру. Он обеспечивает баланс между визуальным разнообразием и необходимой стабильностью. Для экспериментов попробуйте использовать хэш от комбинации координат (x * 1000 + y) для генерации тайлов карты или создать плавный цветовой градиент, интерполируя между результатами хэша для соседних значений.