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

Случайность — ключевой элемент игрового процесса, но её сложно отлаживать и тестировать. Встроенный генератор псевдослучайных чисел (RNG) Phaser решает эту проблему, позволяя задавать начальное значение (seed). Это гарантирует, что последовательность «случайных» событий будет одинаковой при каждом запуске игры, что критически важно для воспроизведения багов, создания процедурного контента и справедливых реплеев в мультиплеере.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    seed = 1419463258969;

    create ()
    {
        console.log(`RNG: ${Phaser.Math.RND.state()}`);

        const text = this.add.text(80, 80, '', { font: '16px Courier', fill: '#ffffff' });

        //  Every time you reload the page you should get the same values back again
        //  because the RNG was seeded in the Game Config with the same seed value.
        //  Adjust the seed for different results. Remove it for random results.

        const shuffleTest = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G' ];

        this.input.on('pointerdown', () => {

            text.setText([
                'RNG',
                '---',
                `Seed: ${this.seed}`,
                '',
                `Between 0 and 50: ${Phaser.Math.RND.between(0, 50)}`,
                `Between 0 and 1: ${Phaser.Math.RND.realInRange(0, 1)}`,
                `Normal: ${Phaser.Math.RND.normal()}`,
                `UUID: ${Phaser.Math.RND.uuid()}`,
                `Angle: ${Phaser.Math.RND.angle()}`,
                `Shuffle: ${Phaser.Math.RND.shuffle(shuffleTest).join(' ')}`
            ]);

            console.log(`RNG: ${Phaser.Math.RND.state()}`);

        });

        text.setText([
            'RNG',
            '---',
            `Seed: ${this.seed}`,
            '',
            `Between 0 and 50: ${Phaser.Math.RND.between(0, 50)}`,
            `Between 0 and 1: ${Phaser.Math.RND.realInRange(0, 1)}`,
            `Normal: ${Phaser.Math.RND.normal()}`,
            `UUID: ${Phaser.Math.RND.uuid()}`,
            `Angle: ${Phaser.Math.RND.angle()}`,
            `Shuffle: ${Phaser.Math.RND.shuffle(shuffleTest).join(' ')}`
        ]);

        console.log(`RNG: ${Phaser.Math.RND.state()}`);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    seed: '!rnd,1,0.2889960983302444,0.9171234022360295,0.7489350214600563',
    scene: Example
};

const game = new Phaser.Game(config);

Зачем нужен seed?

По умолчанию генератор случайных чисел в Phaser (и в JavaScript) использует системное время для инициализации. Это даёт разные результаты при каждом запуске. Однако для отладки, тестирования и создания детерминированных игровых миров (например, в roguelike) нужна воспроизводимая последовательность.

Задав seed в конфигурации игры, вы «замораживаете» генератор. Все вызовы Phaser.Math.RND будут возвращать одни и те же значения, пока вы не измените seed или не перезапустите его вручную. В примере seed задан явно, поэтому при каждом обновлении страницы вы увидите идентичные числа.

Настройка генератора в конфиге

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

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    seed: '!rnd,1,0.288996...,0.748935...',
    scene: Example
};

Строка, начинающаяся с !rnd, — это сериализованное внутреннее состояние генератора. Его можно получить через Phaser.Math.RND.state() и сохранить. Если передать просто число (например, seed: 1419463258969), Phaser инициализирует генератор с этим значением, и последовательность будет другой, но тоже фиксированной.

Основные методы генератора Phaser.Math.RND

Класс Phaser.Math.RND предоставляет набор полезных методов для работы со случайностью.

// Целое число в диапазоне [0, 50]
Phaser.Math.RND.between(0, 50);

// Дробное число в диапазоне [0, 1)
Phaser.Math.RND.realInRange(0, 1);

// Случайный угол в радианах от -PI до PI
Phaser.Math.RND.angle();

// Значение из нормального распределения (среднее 0, отклонение 1)
Phaser.Math.RND.normal();

// Перемешивание массива (алгоритм Фишера-Йетса)
Phaser.Math.RND.shuffle(shuffleTest);

// Генерация UUID версии 4
Phaser.Math.RND.uuid();

Все эти методы зависят от заданного seed. При каждом клике (pointerdown) в примере вызывается новый набор методов, но так как seed зафиксирован, последовательность значений будет предопределена с самого начала игры.

Практическое применение: отладка и процедурная генерация

1. **Воспроизведение багов:** Если игрок сообщил о странном поведении, попросите его seed из консоли (Phaser.Math.RND.state()). Установив этот seed в свою сборку, вы воспроизведёте точную последовательность случайных событий, приведшую к багу.

console.log(`RNG: ${Phaser.Math.RND.state()}`);

2. **Генерация мира:** Для создания процедурных карт (пещер, лесов, уровней) используйте детерминированный алгоритм на основе seed. Это позволит генерировать огромный, но всегда одинаковый мир по одному ключевому числу, которое можно сохранить как «код уровня».

3. **Сетевые игры:** В competitive-играх синхронизация случайных событий (криты, дроп) между клиентами через общий seed избавляет от необходимости передавать каждое событие по сети.

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

Использование seed в Phaser превращает хаос случайности в управляемый инструмент. Это фундамент для отладки, тестирования и создания сложных процедурных систем. Для экспериментов попробуйте: изменить seed в конфиге на простое число; убрать параметр seed совсем, чтобы получить истинную случайность; сохранить состояние генератора (state()) в момент создания игрового предмета и восстановить его позже для «клонирования» случайных свойств.