ランダム

シード指定の決定論的な乱数。名前付きストリームとスナップショット/リストアで、セーブ整合な実行を支える。

再現可能なランダム性:同じシードなら同じ出目 — どのプラットフォームでも、セーブとロードをまたいでも。

インターフェースIRandomService
オフスイッチNullRandomService
アセンブリCommonGameSystem.Core
起動起動時に自動登録 — セットアップ不要

概要

ランダムサービスは、Unity の単一グローバルな Random 状態を独立した再現可能な乱数ソースに置き換えます。決定論的とはこういう意味です:固定シードを与えれば、ルートテーブル、敵とのエンカウント、プロシージャル生成がリプレイのたびに正確に再現され、しかもそのシーケンスはすべてのプラットフォームで同一です。バグ報告(「シード 12345、3 番目の部屋」)に理想的で、セーブ/ロードの正しさには不可欠です。

各ゲームシステムは自分専用の名前付きストリーム"loot""vfx" のような文字列で識別される独立した乱数シーケンス — から値を引けます。ストリームは互いに分離されているため、あるシステムに乱数呼び出しを 1 つ追加しても、別のシステムのシーケンスがずれることはありません。演出用のスクリーンシェイクのロールが、ドロップするルートを変えることは決してありません。

全状態はスナップショットにキャプチャして後から復元できます:ラン途中のセーブをリロードしても、次のロールは中断した正確な位置から続きます — リロードでより良いドロップを狙うセーブスカムはできません。

クイックスタート

フレームワークの起動後に実行されるよう、[DefaultExecutionOrder(100)] を付けた MonoBehaviour でサービスを解決してキャッシュします:

var rng = ServiceLocator.Resolve<IRandomService>();
// or the short alias: var rng = SL.Resolve<IRandomService>();

完全に決定論的なランにするには、明示的なシードから構築したサービスに置き換えます:

ServiceLocator.Replace<IRandomService>(new RandomService(new RandomOptions(seed: 12345)));

サービス自身がデフォルト(マスター)ストリームそのものです — すべての抽選メソッドは即座に呼び出せます。

API リファレンス

コア抽選メソッド

各呼び出しはストリームの状態を進めます。

  • ulong NextULong() — 生の 64 ビット値。
  • uint NextUInt() — 生の 32 ビット値。
  • int NextInt(int maxExclusive)[0, maxExclusive) の偏りのない整数。
  • int NextInt(int minInclusive, int maxExclusive) — 指定範囲の偏りのない整数。
  • float NextFloat()[0, 1) の一様値。正確に 1.0 を返すことはありません。
  • float NextFloat(float min, float max) — 指定範囲の一様値。
  • double NextDouble()[0, 1) の一様値。
  • bool NextBool() — 公平なコイントス。
  • bool Chance(float probability) — 指定確率で true。確率がちょうど 0 または 1 の場合は抽選せずに即座に返ります。

便利メソッド

  • int NextIndex(int count)[0, count) の一様なインデックス。count が 0 以下のときは -1 を返します。
  • int NextWeightedIndex(IReadOnlyList<float> weights) — 重みに比例してインデックスを選びます。退化した入力(空のリスト、すべて 0 の重み)には -1 を返します。
  • T Pick<T>(IReadOnlyList<T> items) — リストから一様に選ばれた要素。
  • void Shuffle<T>(IList<T> items) — リストをその場でシャッフルします。すべての並び順が等確率です。

名前付きストリーム(分離)

  • IRandomStream GetStream(string name) — 独立したストリームを取得または作成します。同じ名前はセッションを通じて同じストリームインスタンスを返します。IRandomStream は上記と同じ抽選メソッドを提供します。
  • ulong Seed { get; } — 現在のマスターシード。
  • void Reseed(ulong seed) — 新しいシードにリセットします。これによりすべての名前付きストリームは破棄されます。

セーブとリストア

  • RandomSnapshot Capture() — 全状態をシリアライズします:マスターシード、デフォルトストリーム、すべての名前付きストリーム。
  • void Restore(RandomSnapshot snapshot) — スナップショットから再開します。たとえばラン途中のセーブをロードするときに使います。

RandomSnapshot はプレーンなシリアライズ可能構造体なので、そのままセーブデータクラスに入れられます(下のセーブ統合の例を参照)。その IsValid プロパティは、ロードしたスナップショットが使用可能なペイロードを持っているかを教えてくれます。

使用例

日常的な抽選

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class LootDropper : MonoBehaviour
{
    private IRandomService _rng;

    void Awake()
    {
        _rng = ServiceLocator.Resolve<IRandomService>();
    }

    public void RollDrop()
    {
        // Deterministic damage roll: the same seed gives the same sequence on all platforms.
        int dmg = _rng.NextInt(8, 13);
        Debug.Log($"Damage: {dmg}");

        // A named stream keeps cosmetic effects isolated from gameplay rolls.
        var vfx = _rng.GetStream("vfx");
        float shake = vfx.NextFloat(0f, 0.5f);
        Debug.Log($"Screen shake: {shake}");

        // Weighted loot table.
        float[] weights = { 30f, 50f, 20f }; // common, rare, epic
        int rarity = _rng.NextWeightedIndex(weights);
        Debug.Log($"Rarity: {rarity}");
    }
}

セーブサービスと組み合わせたスナップショット/リストア

サービス自身はスナップショットをどこにも書き込みません — セーブ/ロードサービスを通じて、残りのセーブデータと一緒にルーティングします。RandomSnapshot をセーブクラスのフィールドとして埋め込みます:

using CommonGameSystem.Core;
using UnityEngine;

[System.Serializable]
public class RunSaveData
{
    public int stage;
    public RandomSnapshot rng; // the full random state rides inside your save data
}

[DefaultExecutionOrder(100)]
public class RunSaveManager : MonoBehaviour
{
    private IRandomService _rng;
    private ISaveService _save;

    void Awake()
    {
        _rng = ServiceLocator.Resolve<IRandomService>();
        _save = ServiceLocator.Resolve<ISaveService>();
    }

    public void SaveRun(int currentStage)
    {
        var data = new RunSaveData
        {
            stage = currentStage,
            rng = _rng.Capture() // freeze the dice exactly where they are
        };
        _save.Save("run-autosave", data);
    }

    public void LoadRun()
    {
        var result = _save.Load<RunSaveData>("run-autosave");
        if (result.Status != SaveStatus.Ok)
            return; // no save yet, or the file is unreadable — start fresh

        if (result.Value.rng.IsValid)
            _rng.Restore(result.Value.rng); // the next roll continues the saved sequence

        // ... restore the rest of your run state from result.Value ...
    }
}

Restore の後、(マスターストリームでも任意の名前付きストリームでも)次の NextInt は、ゲームを終了しなかった場合に返したであろう値を正確に返します。セーブをリロードしても結果は振り直せません。

無効化する

ServiceLocator.Replace<IRandomService>(new NullRandomService());

これでランダムサービスがミュートされます。すべての抽選は乱数の代わりに固定の安全なデフォルト値(0、0f、false、-1、または default(T))を返します。テストや決定論的なサンドボックスモードでランダム性を抑止するのに便利です。プログラミングエラーは引き続き例外を投げます:null のストリーム名や Dispose 後の呼び出しは、本物のサービスと同様に例外になります。

よくある落とし穴

  • メインスレッド専用です。 すべての抽選とストリームアクセスはメインスレッドで行わなければなりません。ワーカースレッドが乱数を必要とする場合は、先にメインスレッドで値を引いて(またはスナップショットをキャプチャして)から、結果をワーカーに渡してください。
  • Reseed() をまたいで名前付きストリームを保持しないでください。 Reseed() はサービスからすべての名前付きストリームを破棄します。リシード前にキャッシュしたストリームは単体では動き続けますが、サービスはもう追跡しておらず、スナップショットにも現れません。リシード後は GetStream() を呼び直して、新しいシードから導出されたストリームを取得してください。
  • Chance(0f)Chance(1f) は抽選をスキップします。 この 2 つのケースは即座に返り、ストリーム状態を進めません。決定論のために正確な抽選回数に依存するコードでは、この点を考慮してください。
  • スナップショットの保存はあなたの仕事です。 Capture() はプレーンな値を返すだけで、サービスはどこにも書き込みません。上で示したように、残りのセーブデータと一緒にセーブ/ロードサービスへルーティングし、ロード時に Restore を呼んでください。
  • 不正な入力は例外ではなく安全な値を返します。 空のリスト、幅 0 の範囲、すべて 0 の重み、負のカウントは 0、-1、または default(T) を返し、場合によって警告をログします(RandomOptions.WarnOnDegenerateInput で制御)。退化したデータでサービスがクラッシュすることはありません。

関連ページ