RNGセッション
はじめに
決定論的な設定では、同じ入力に対して常に同じ出力が得られます。しかし、予測不能なイベントの影響をシミュレートしたり、システムに一定のばらつきを与えるために、ランダム性を導入したい状況があります。乱数生成器、通称RNGは、Quantum内でこれを実現するための重要なコンポーネントです。
RNG Session
Quantumが提供しているRNGSessionを使用すると、シミュレーション内で疑似乱数を決定論的に生成できます。フレームのglobals内には、グローバルセッションがプリロードされていて、frame.RNGから直接アクセス可能です。セッションは、異なるシードが与えられない限り、「常に」同じ数列を生成します。
使用方法:
C#
// Quantumシミュレーション内
public override void OnInit(Frame frame)
{
int randomNumber = frame.RNG->Next(0, 100);
}
フレーム内のセッションはRuntimeConfig.Seedフィールドからシードが与えられるため、必要に応じて特定のシード値を設定することが可能です。デフォルトのシード値は0で、同じ値が使用されている間、生成される乱数列は常に同じになります。ここからは、ゲーム開始前/実行中にシード値を変更する方法について詳しく説明します。
ゲーム開始前のシード変更
すべてのQuantumゲームは、特定の引数セット(シードを含むRuntimeConfigなど)を指定して開始されます。
そのため、ゲームの初期シードを変更するには、Quantumゲーム開始を行っているスクリプトを見つけて、シードを変更してください。
例えばQuantum Start UIを使用している場合なら、それはQuantumStartUIConnect.ConnectAsync()メソッドになります。RuntimeConfig.Seed = UnityEngine.Random.Range(int.MinValue, int.MaxValue);のようにして、ランダムシードの引数を定義できます。これによって、ゲームが実行されるたびにランダムなシードが設定されるため、乱数列が異なることも保証されます。
**追記:**上記の方法では、Quantum SDKに付属するクラスを変更することになります。よりベストなアプローチは、接続スクリプトを独自実装して、そちらを変更することです。SDKファイルを直接変更した場合は、Quantum SDKアップグレード時に変更内容が上書きされてしまうことに注意してください。
実行時のシード変更
シミュレーション実行中に、必要に応じてセッションを上書きしてシードを直接変更することができます。これは以下のような非常に単純なロジックで実現可能です:
C#
// シミュレーション内
public void ResetSeed(Frame frame)
{
int newSeed = 100;
frame.Global->RngSession = new Photon.Deterministic.RNGSession(newSeed);
}
コンポーネント内のRNG Session
共有のグローバルセッション(frame.RNGなど)は、予測カリングと組み合わせた場合に問題が発生する場合があります。予測中、カリング半径外のエンティティは、RNGセッションから乱数を取得して使用することはありません。しかし、その後の確定フレームでは、すべてのエンティティがセッションを使用することになるため、セッションの進行が異なってしまいます。つまり、カリング処理そのものが、乱数列が異なる原因となります。
これによって、エンティティの挙動が一時的に「乱れる」可能性があります。例えば、あるエンティティがframe.RNGを使用して攻撃対象をランダムに選択するとして、予測カリング時と確定フレーム時の乱数列が異なるため、エンティティは結果的に別の対象を攻撃することになります。
これを回避するには、グローバルセッションを共有するかわりに、コンポーネント内でエンティティ自身のRNGセッションを割り当ててください。各エンティティは、他のエンティティの結果に影響を与えることなく、各自でセッションを進行できます。コンポーネント内のRNG Sessionは次のように定義できます:
C#
// DSLコンポーネント
component MyComponent
{
RNGSession Session;
}
そして、フレームのセッションと同じようにシードを定義できます:
C#
public void InitComponentWithSeed(MyComponent* component)
{
int newSeed = 100;
component->Session = new RNGSession(newSeed);
}
詳細はRNG問題の回避をご覧ください。
チート
決定論性の性質上、ランダム性の予測は非常に容易です。例えば、ハッカーがローカルシミュレーションを読み取り、シミュレーション外でRNGSessionを複製することで、事前に乱数列を知ることができます。これに対抗する一般的な方法は、シードを頻繁にリセットして、数値を簡単に予測できないようにすることです。例えば、プレイヤーの入力をハッシュ化して、その結果をシードとして使用します。これによって、シードを操作したり予測したりすることが非常に困難になります。
これは、RNGセッションの予測可能性の低減に役立ちます。特定のシードが与えられればセッションは常に同じ挙動になることから、悪意のあるプレイヤーが今後の展開を予測する手段になりうるためです。
RNGセッションを頻繁に変更する方法の一例として、プレイヤーキャラクター全員の位置の合計値を毎秒設定する方法があります。悪意のあるプレイヤーはこの変数を完全に制御することはできないため、シードを特定して乱数列を事前に予測することが困難になります。実装例としては、各プレイヤーが操作するエンティティの位置X + Y + Zを合計した最終値をシードに設定した新しいRNGセッションを毎秒作成します。
決定論性についての備考
重要: RNGSessionは、Quantumシミュレーションコード内でのみ使用し、Unity/ビューコードでは絶対に使用しないでください。RNGSessionは乱数列を決定するステート値を内部的に持っているため、これは非常に重要です。
つまり、frame.RNG->Next()(またはコンポーネントのセッション)や同様のAPIを使用すると、内部ステートが進行するため、ビューコードで誤って使用するとゲームの同期ズレ(チェックサムエラー)を引き起こします。