10.3. ReckoningStrategy のカスタマイズ

PiaReckoning の推測航法アルゴリズム(ReckoningStrategy)は、アプリケーションでカスタマイズすることが可能です。

ライブラリとして用意されている Simple3dReckoningStrategy は基本的な推測アルゴリズムとなっています。実際のゲームプログラムでは Simple3dReckoningStrategy による推測値をさらにゲームのステージの移動制限などに合わせて補正する必要があります。

Simple3dReckoningStrategy のソースコードは、sources\reckoning\reckoning_Simple3dReckoningStrategy.cpp に公開されています。ゲーム仕様に合わせてカスタマイズして使用してください。

参考:

Simple3dReckoningStrategy では 1 次関数による補間が用いられています。2 次関数を使用することもできますが 2 次関数の場合、急激な(加速度の高い)移動時に誤差が大きくなりスムーズに補間できなくなることがあります。一般的なアクションゲームでは 1 次関数を用いることをおすすめします。

 

推測アルゴリズムの定義

 ReckoningCloneElement のテンプレート引数 ReckoningStrategy に指定するクラスには、以下の定義がされている必要があります。

その型名でアクセスできるものであれば、ReckoningStrategy の内部クラスでも、他の型を typedef したものでも構いません。

 

Value

SetValue()、GetValue() でアクセスする値の型です。UnreliableCloneElement、ReliableCloneElement のテンプレート引数である Value の指定に相当します。

 

Sample

標本値の型です。 SetValue()、GetValue() する値をそのまま標本値として保持しておく場合は、Value と同じ型を指定する必要があります。この型が実際に送信されるデータの型になります。SerializePolicy でシリアライズされるデータも Value 型ではなく Sample 型なので、SerializePolicy 指定時には注意してください。

 

SetValueArg

SetValue() 呼び出し時に、ReckoningStrategy::CheckSample()、ReckoningStrategy::MakeSample() に渡すデータの型です。標本値として追加するかを判断するため、または標本値を実際に作成するために Value 以外に必要な情報がある場合に使用します。必要が無い場合は、void を指定します。

Work

推測の計算の効率化のために事前計算しておく用途など、ReckoningCloneElement のインスタンスごとにデータを保持しておくことができ、その型をここで指定します。必要のない場合は void を指定します。その場合、余分なメモリ消費は発生しません。詳しくは、下記の「ReckoningStrategy のインスタンスと Work の使い方」を参照してください。

 

定数

定数として扱えれば、enum ハックとして定義されていても構いません。

 

static const uint32_t BufferSize

保持しておく標本値の個数を指定します。 1 以上 LONG_MAX 以下の値を指定する必要があります。

 

関数 

 

bool Estimate (
    Value* pValue,
    const nn::pia::clone::ReckoningCloneElementBase::SampleAccessor<Sample>& accessor,
    nn::pia::reckoning::ClockValue clock,
    Work* pWork);

標本値から時刻 clock 時点の値を予測します。予測値を pValue に書き込んで true を返す必要があります。もし予測が不可能な場合は、false を返す必要があります。この関数は、ReckoningCloneElement::GetValue() で値を取得する際に呼び出されます。計算結果はキャッシュされますので、同じ時刻にもう一度 ReckoningCloneElement::GetValue() された際は、呼び出されません。 

  • [out] pValue : 予測した値を書き込む先へのポインタです。
  • [in] accessor : 標本値バッファのアクセサです。
  • [in] clock : 予測する時刻(= 現在の時刻)です。
  • [inout] pWork : ワークメモリへのポインタです。Work が void の時は NULL が渡されます。

 

bool CheckSample(
    bool* pIsReliable,
    const Value& value,
    const SetValueArg* cpSetValueArg,
    const nn::pia::reckoning::ReckoningCloneElementBase::SampleAccessor<Sample>& accessor,
    ClockValue clock,
    Work* pWork);

ReckoningCloneElement::SetValue() された値 value を標本値として採用するかどうかを判定します。標本値として採用する場合は true を返す必要があります。また、その標本値を送信する際に到達保証が必要であれば、 *pIsReliable に true を設定する必要があります。この関数は、ReckoningCloneElement::SetValue() で値を設定する際に呼び出されます。

  • [out] pIsReliable : 標本値として採用する場合、その標本値を送信する際に到達を保証する必要があるかを指定するポインタです。明示的に指定されない場合は、到達保証されません。
  • [in] value : ReckoningCloneElement::SetValue() された値です。
  • [in] cpSetValueArg : ReckoningCloneElement::SetValue() 時に指定された追加情報です。
  • [in] accessor : 標本値バッファのアクセサです。
  • [in] clock : 現在の時刻です。
  • [inout] pWork : ワークメモリへのポインタです。 Work が void の時は NULL が渡されます。

 

void MakeSample(
    Sample* pSample,
    const Value& value,
    const SetValueArg* cpSetValueArg,
    const nn::pia::reckoning::ReckoningCloneElementBase::SampleAccessor<Sample>& accessor,
    ClockValue clock,
    Work* pWork);

ReckoningCloneElement::SetValue() された値から標本値を作成します。この関数は、ReckoningCloneElement::SetValue() 内で CheckSample() で true と判定された場合に呼び出されます。

  • [out] pSample : 作成する標本値を書き込むポインタです。
  • [in] value : ReckoningCloneElement::SetValue() された値です。
  • [in] cpSetValueArg : ReckoningCloneElement::SetValue() 時に指定された追加情報です。
  • [in] accessor : 標本値バッファのアクセサです。この関数が呼び出されるタイミングでは、 index 0 にここで書き込むバッファが確保された状態なので、pSample に書き込むまでは accessor.GetSample(0) の値は不定です。
  • [in] clock : 現在の時刻です。
  • [inout] pWork : ワークメモリへのポインタです。Work が void の時は NULL が渡されます。

 

void OnUpdateSample(
    const nn::pia::reckoning::ReckoningCloneElementBase::SampleAccessor<Sample>& accessor,
    int index,
    Work* pWork);

標本値が追加された際に呼び出され、必要に応じてワークメモリの値を更新するなどの処理を行います。この関数は、ReckoningCloneElement::SetValue() 内で MakeSample() を行った後、もしくは、common::Scheduler::Dispatch() 内で新しい標本値を受信してバッファに追加された際に呼び出されます。

  • [in] accessor : 標本値バッファのアクセサです。
  • [in] index : 新たに追加された標本値のインデックスです。受信するタイミングなどによって、必ずしも古い順番に追加されるわけではないことに注意してください。
  • [inout] pWork : ワークメモリへのポインタです。 Work が void の時は NULL が渡されます。

 

ReckoningCloneElement におけるパケットロス

MakeSample() で作られた標本値は送信され、受信側の標本値バッファに追加されます。通信が順調であれば送信側と受信側の標本値バッファの状態は(タイミングのずれはあっても)いずれ同じになります。しかし、実際はパケットロスにより標本値が届かなかったり、届く順番が入れ替わったりといったことが起こって、受信側の標本値バッファの状態も送信側と異なる状態になる場合があります。Estimate() での推測や、CheckSample() での判定は、ある程度パケットロスが起きても推測値に大きく影響が出ないようになっていることが望ましいと言えます。

CheckSample() で *pIsReliable に true を指定した場合、そこで追加した標本値は到達保証されます。ただし、到達が確認できる前に新たな標本値によって送信側のバッファからあふれてしまった場合にはそれ以上送信されませんので、全ての標本値が到達保証されるようにしたとしても標本値バッファの状態が必ずしも同じ状態になるわけではないことに注意してください。到達保証に関しての詳細は9.3. PiaClone 詳細仕様の「PiaClone におけるデータの到達保証」を参照してください。

 

ReckoningStrategy のインスタンスと Work の使い方

ReckoningStrategy の各関数で計算を行う際、引数で与えられる値からその都度計算をするのであれば、一つの ReckoningStrategy インスタンスを複数の ReckoningCloneElement に登録して使用することができます。

一方、一般に新しい標本値が追加される回数と比べて、ReckoningCloneElement::SetValue() のたびに呼ばれる CheckSample() や、 ReckoningCloneElement::GetValue() のたびに呼ばれる Estimate() 関数の方が呼ばれる回数が多いので、計算量削減のために、標本値のみから計算できる部分は標本値が更新された時(OnUpdateSample())に事前計算して保存しておくことで、CheckSample() や Estimate() での計算を減らすことができます。しかし、この事前計算したものを ReckoningStrategy インスタンスの中に持っておくとすると、ReckoningCloneElement のインスタンス毎に ReckoningStrategy のインスタンスを作る必要があります。

そこで、この事前計算したデータを保存する場所を ReckoningStrategy::Work 型のバッファとして ReckoningCloneElement に持たせることにより、 事前計算を行いつつ ReckoningStrategy のインスタンスは共有して使うという事が可能になります。

もちろん、ReckoningStrategy のインスタンスを個別に用意することに問題があるわけではありませんが、実際のゲーム開発では、例えばどの程度の頻度で標本値に加えるかを調整するパラメータを ReckoningStrategy に持たせておいて、実際にその値を変えながら予測の精度と通信量のバランスの良いところを調整していくことになると思われます。その際、ReckoningStrategy のインスタンスが共有されていればそのパラメータ自体も共有されるので、調整もやりやすいであろうという事を想定しています。