ランダム
シード指定の決定論的な乱数。名前付きストリームとスナップショット/リストアで、セーブ整合な実行を支える。
再現可能なランダム性:同じシードなら同じ出目 — どのプラットフォームでも、セーブとロードをまたいでも。
| インターフェース | 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で制御)。退化したデータでサービスがクラッシュすることはありません。