17. 複製オブジェクトの効率的な使用方法

17.1. 複製オブジェクト更新の負荷低減

DuplicatedObject::Update() は、コールするたびに1メッセージが生成されます。

複数のデータセットを同時に更新する場合に DuplicatedObject::Update(DataSet &refDataset) をそれぞれ呼び出すのではなく、 複製オブジェクトをまるごと更新する DuplicatedObject::Update() をコールしたほうが、処理のオーバーヘッドが減って効率的です。

注意

複製オブジェクト全体を更新する DuplicatedObject::Update() をコールしたとき、 DDLでunreliableを指定しているデータセットしか更新していなくても、DDLでreliableが指定されたデータセットも更新対象となります。 reliable指定されたデータセットは通常毎フレーム変更されるものではないので、 このような場合には後述の upon_request_filter をデータセットのDDLに付け、 特定のデータセットを送信対象から外すように実装してください。

17.2. データセットの効率化

DuplicatedObject::Update() のたびには変更されないパラメータがある場合は、 それらを別のデータセットに移動し、DDLに upon_request_filter を付けることを推奨します。 upon_request_filter をデータセットに定義すると、DataSet::RequestUpdate() を実行した時にだけ該当のデータセットがメッセージに含まれるように設定できます。 よって、該当のデータセット更新が必要ないときにはパケットサイズを小さくすることができます。

例えば、試合前にだけ変更し、試合中は変わらない(またはほとんど更新されない)パラメータが挙げられます。 ※キャラクターの名前や最大ヒットポイントなどがこれにあたります。

補足

パケットサイズが小さくなると、メッセージのシリアライズ・デシリアライズのコストが下がってCPU負荷が軽くなり、 限られたネットワーク帯域を有効利用できるというメリットもあります。

また、更新頻度が大きく異なるものを同一のデータセットに定義している場合も、改善の余地があります。

Code 17.1 効率の悪いデータセットの例
// 更新頻度が大きく異るものが同じデータセットに定義され、サイズも大きいデータセット
dataset Parameters {
    uint32 avatarState; // 更新頻度高。
    qbuffer binaryData; // 更新頻度低。 これがある程度大きなデータサイズであると仮定。
} upon_request_filter;

Code 17.1 において avatarState のみが更新された場合、 binaryData も同じデータセットであるため、同時に更新することになり、 binaryData のサイズが大きな場合には無駄にネットワーク帯域を食いつぶし、CPUが高負荷になる原因になります。

このような場合には avatarState と binaryData を別のデータセットに分割することで、 無駄なデータ送信を最小限に抑えることができ、CPU負荷低減、ネットワーク帯域の有効利用が可能です。( Code 17.2

Code 17.2 効率の良いデータセットの例
dataset AvatarParameter {
    uint32 avatarState; // 更新頻度高。
} upon_request_filter;

dataset BinaryParameter {
    qbuffer binaryData; // 更新頻度低。 これがある程度大きなデータサイズであると仮定。
} upon_request_filter;

補足

avatarState と binaryData が同じタイミングで変更される場合には、同じデータセットに定義しておくのが一番効率が良いです。 データセット内に同じタイミングで変更されるものが多いほど効率が良いということになります。

17.3. Reliable通信の頻度についての注意書き

NEXは標準ではReliable通信のストリームを1つしか保持しておらず、 高頻度でReliable通信を行うと、通信環境が悪い場合に遅延が徐々に大きくなる傾向にあります。 8.3.4. を参考に複数本のReliable通信を行うように初期化し、 データセットの場合は DataSet::SetSubStreamID や、 RMCの場合は DOCallContext::SetSubStreamID でSubStreamIDを指定することにより、 独立した再送制御による信頼性のあるデータ通信を行うようにできます。

複数本のReliable通信を利用したとしても、再送によりデータ量が増えて、通信環境が悪化により遅延が増大すること 考えられるので、Reliable通信によるデータセット、RMCの使用は必要最低限に抑えることをお勧めします。

  • Unreliable通信に適しているパラメータ
    • 更新頻度が高く、少しくらいロスしてもゲームとして問題がないもの
    • 毎フレーム更新される可能性のあるキャラクターのポジションなど
  • Reliable通信に適しているパラメータ
    • 更新頻度が低く、到達しないとゲームとして成り立たないもの
    • 試合開始・終了フラグや、試合結果など

データセットをUnreliable通信で送信するかReliable通信で送信するかは、アプリケーションのソースではなく、 DDLの定義によって指定します。詳細については 15. を参照してください。

注意

毎フレームのような高頻度でreliable指定されたデータセットを常に更新するようなことがないように注意してください。

また、DuplicatedObject::Update() でreliable指定されたデータセットが更新される場合と、 Reliable通信を行う複製オブジェクトのRMC送信を行う場合は、 Reliable通信バッファが足りない時に、 それぞれ QERROR(Transport, ReliableSendBufferFull) と DOCallContext::ErrorReliableSendBufferFull が発生します。 以下の関数で、複製オブジェクトの最大Reliableバッファ数を設定できます。 詳細は API リファレンスを参照してください。

P2P 通信の一部を除くパケットのバッファは、パケットバッファマネージャーによって固定長のメモリプールで管理されます。 パケットバッファの空きが十分にある間は、リライアブル通信バッファの上限に達するまでエラーとなりません。 パケットバッファの空きが不足した場合、DuplicatedObject::Update() と RMC送信 において、 それぞれ QERROR(Transport, PacketBufferFull) と DOCallContext::ErrorPacketBufferFull が発生します。 PacketBufferFullが発生した場合は、ディスパッチを行い時間をおいてから必要に応じて再度送信を行ってください。 PacketBufferFullが高頻度で発生する場合はパケットバッファを増やす対策も行ってください。 パケットバッファについては、 パケットバッファのメモリ管理 を参照してください)

17.4. SessionClock::GetTime() のキャッシュ

以下の関数をセッションクロックの指定なしで使うと、関数内で SessionClock::GetTime() をコールします。 CTRでは SessionClock::GetTime() にそれなりのCPUコストがかかります。 何度も使用する場合はキャッシュした時間値を使用すると良いでしょう。

注意

同じセッションクロックを同一フレーム外で使いまわすと、データセットの誤差の原因になります。 キャッシュするスコープは同一フレーム内とすることを推奨します。

Code 17.3 は、 DuplicatedObject::Refresh() と DuplicatedObject::Update() の両方にキャッシュしたセッションクロックを使いまわす例です。 同一フレーム内では、このようにセッションクロックを共有しても問題ありません。

Code 17.3 セッションクロックをキャッシュして使いまわす例
// RefreshもUpdateもこのセッションクロックを使いまわす
Time time = SessionClock::GetTime();

// すべてのデュプリカをリフレッシュ
Avatar::SelectionIterator itAvatarDuplica(nn::nex::DUPLICA);
while (!itAvatarDuplica.EndReached()) {
    // extrapolation_filter、buffered を使用している場合は、Refreshで最新データが取得できるようになる
    itAvatarDuplica->Refresh(time);
    ++itAvatarDuplica;
}

//
// 物理演算などを行い、自分がデュプリカマスターであるアバターのポジションを変更する
//

// デュプリカマスターのパラメータ変更をデュプリカに伝える
Avatar::SelectionIterator itAvatarMaster(nn::nex::DUPLICATION_MASTER);
while (!itAvatarMaster.EndReached()) {
    itAvatarMaster->Update(time);
    ++itAvatarMaster;
}

17.5. 推測航法の使用例

13. 推測航法機能 で紹介している推測航法機能は、 ネットワークレイテンシを隠蔽し、データセットをなめらかに変化させるとともに、パケット数を削減することにも効果的です。

ここではある程度的を絞って、以下のデータセットについての推測航法の設定例を示します。 推測航法のより詳細な説明については 13. 推測航法機能 を参照してください。

Code 17.4 推測航法の設定例
dataset Position {
    float x;
    float y;
    float z;
} extrapolation_filter, unreliable;

17.5.1. 最小更新遅延の設定例

最小更新遅延・最大更新遅延は、最後のデータ送信時間からの経過時間を比較して、DOマスターがデュプリカに値を送信すべきかどうか判断する仕組みです。

データセットのデフォルト最小更新遅延は 66msec となっています。 リアルタイム性の高いアプリケーションではもっと小さくするなど適切な値への変更を推奨します。

Code 17.4 のDDLにおいて、 最小更新遅延を設定するには、 Code 17.5 のようにしてください。

Code 17.5 Positionの最小更新遅延を変更するサンプルコード
// 例として、約2フレームにあたる 33msec での最小更新遅延を設定しています。
// これにより、DOマスターのPosition更新がデュプリカに送信される回数は2フレームに1回が上限となります。
Position::SetMinimumUpdateDelay(33);

注意

実際には2フレームは約 33.34msec なので、最小更新遅延に 33msec を設定すると約 0.34msec の誤差があり、 タイミングによっては2フレームに2回、更新がデュプリカに送信されることがあります。

17.5.2. エラー許容の設定例

エラー許容はDOマスターとデュプリカのデータセット値の差を比較して、DOマスターがデュプリカに値を送信すべきか判断する仕組みです。

デフォルトのエラー許容は0のため、そのままでは少しデータセットが変化しただけで、 必ず最小更新遅延の間隔でDOマスターからデュプリカにパケットが送信されます。 適切な値を設定することを推奨します。

Code 17.4 のDDLにおいて、 一定エラー許容を設定するには、 Code 17.6 のようにしてください。

Code 17.6 Positionのエラー許容を変更するサンプルコード
// 例として、0.5fの一定エラー許容を設定しています。
// 0.5fよりエラーが大きい場合、DOマスターのPosition更新がデュプリカに送信されるようになります。
ErrorToleranceFunction * pErrorToleranceFunction = Position::GetErrorToleranceFunction();
pErrorToleranceFunction->SetConstantError(0.5f);

注意

パケットの搬出タイミングは、エラー許容よりも最小更新遅延のほうが優先されます。 どんなに小さなエラー許容を設定しても、最小更新遅延より短い間隔でパケットは送信されません。

17.5.3. 連続性の遮断の利用

推測航法は座標予測をしているため、キャラクターが着地したときや壁にぶつかったときなどに、 複製オブジェクトのデュプリカがめり込むような見え方をします。 ネットワークレイテンシが大きい場合には、よりめり込み動作が目立ちます。

そのような場合には、DataSet::IndicateContinuityBreak() の使って連続性の遮断を行うことを推奨します。 連続性の遮断を行うことで、めり込みが緩和されます。(必ずなくなるというわけではありません)

DataSet::IndicateContinuityBreak() の第2引数には連続性の遮断を通達するプロトコルとして、 Reliable通信を行うのか、Unreliable通信を行うのかを選択できます。 Reliable通信はパケットがロストした場合に再送が行われますが、 連続性の遮断が遅れてデュプリカに届いても意味が無いような状況ではUnreliable通信の使用を推奨します。

Code 17.7 連続性の遮断を使用する推測航法の設定例
doclass Avatar {
    Position m_pos;
};
Code 17.8 DataSet::IndicateContinuityBreak() の使用例
Time time = SessionClock::GetTime();
Avatar::SelectionIterator itAvatarMaster(nn::nex::DUPLICATION_MASTER);
while (!itAvatarMaster.EndReached()) {
    //
    // 物理演算などを行い、自分がデュプリカマスターであるアバターのポジションを変更する
    //

    if () // アバターが壁にぶつかった
    {
        itAvatarMaster->m_pos.IndicateContinuityBreak(CONTINUITY_BREAK_STOP, false);
    }
    else if () // アバターが急に進行方向を変えた
    {
        itAvatarMaster->m_pos.IndicateContinuityBreak(CONTINUITY_BREAK_SUDDEN_CHANGE, false);
    }
    else if () // アバターがワープして瞬間移動した
    {
        itAvatarMaster->m_pos.IndicateContinuityBreak(CONTINUITY_BREAK_TELEPORT, false);
    }

    itAvatarMaster->Update(time);
    ++itAvatarMaster;
}