9.2. PiaClone 基本機能

基本的な使い方

PiaClone の初期化と終了処理

Pia の他のプロトコルと同様、CloneProtocol のインスタンスを作成して初期化する必要があります。

nn::pia::clone::Initialize();
nn::pia::clone::BeginSetup();
uint32_t handle = nn::pia::transport::Transport::GetInstance()->CreateProtocol<nn::pia::clone::CloneProtocol>();
nn::pia::clone::CloneProtocol* pCloneProtocol = nn::pia::transport::Transport::GetInstance()->GetProtocol<nn::pia::clone::CloneProtocol>(handle);
nn::pia::clone::CloneProtocol::Setting setting;
// setting に各種設定をする(詳細はリファレンスを参照してください)
pCloneProtocol->Initialize(setting);
nn::pia::clone::EndSetup(); 

CloneProtocol の終了処理も他のプロトコルと同様です。

pCloneProtocol->Finalize();
nn::pia::transport::Transport::GetInstance()->DestroyProtocol(handle);
nn::pia::clone::Finalize(); 

 

クローン、クローンエレメントのインスタンス作成

クローン、クローンエレメントのインスタンスは、(通信開始前でも開始後でも)任意のタイミングで作成、破棄できます。ただし、CloneProtocol に登録したままの状態でインスタンスを破棄してはいけません。不正なメモリアクセスが発生する可能性があります。

クローンエレメントのインスタンスの登録/登録解除

クローンエレメントはクローンに登録して使用します。クローンエレメントをクローンに登録/登録解除する場合は、クローンが CloneProtocol に登録されていない状態で行う必要があります。

nn::pia::clone::SendClone sendClone;
nn::pia::clone::ReceiveClone receiveClone;
 
nn::pia::clone::UnreliableCloneElement<uint32_t> elementForSend;
nn::pia::clone::UnreliableCloneElement<uint32_t> elementForReceive;
 
sendClone.RegisterElement(&elementForSend, 0x0001);
receiveClone.RegisterElement(&elementForReceive, 0x0001);

 

クローンのインスタンスの登録/登録解除

クローンは CloneProtocol に登録して使用します。クローンの登録/登録解除は、CloneProtocol::Initialize() から Finalize() の間の任意のタイミングで行う事ができます。

CloneProtocol による通信中に登録解除を行った場合、CloneProtocol 内で整合性を維持するための処理が行われるため、すぐに完了しない場合があります。登録解除の完了は CloneBase::IsRegisteredWithProtocol() が false になったことをチェックすることで確認することができます。登録解除の完了を待ってから再登録やインスタンスの破棄を行う必要があります。

pCloneProtocol->RegisterSendClone(&sendClone, 0x00000001);
pCloneProtocol->RegisterReceiveClone(&receiveClone, nn::pia::StationIndex_1, 0x00000001);
 
pCloneProtocol->UnregisterSendClone(&sendClone);
pCloneProtocol->UnregisterReceiveClone(&receiveClone);

 

CloneProtocol の通信開始と通信終了

PiaSession によるセッション参加後 CloneProtocol による通信を始めるには CloneProtocol::Start() を呼びます。Start() するとスタート状態(State_Start)に遷移します。既に CloneProtocol による通信を行っているステーションと時刻の同期が行われ、それが完了すると CloneProtocol::GetClock() で時刻が取得できるようになります。その後、他のステーションに CloneProtocol による通信が始まったことを通知して、それが完了するとアクティブ状態(State_Active)に遷移します。既にクローンが登録されていた場合は、そこからクローン間の接続処理が始まります。

CloneProtocol による通信を終了する場合は、CloneProtocol::Stop() を呼びます。Stop() するとストップ状態(State_Stop)に遷移します。クローンエレメントが送り残したデータを送りきり、クローン間の切断処理を行った後、 CloneProtocol による通信が終了したことを通知して非アクティブ状態(State_Inactive)に遷移します。

セッションからの離脱は、可能な限り非アクティブ状態で行う必要があります。非アクティブ状態でない時にセッションから離脱しても、残ったステーション間で可能な限り整合性を取るように動作しますが、そのための余分な通信も発生します。(詳細は 9.3. PiaClone 詳細仕様の「PiaClone におけるデータの到達保証」を参照してください。)そのため、正常シーケンスでセッションから離脱する場合は、先に CloneProtocol の通信を終了させ、非アクティブ状態になったのを確認した後にセッションから離脱することを推奨します。

CloneProtocol の状態

CloneProtocol の状態遷移を図 9-2. CloneProtocol の状態に示します。

図 9-2. CloneProtocol の状態

 

State_Start、State_Active、State_Stop の間は定期的に(1ゲームフレームに1回) CloneProtocol::UpdateClock() を呼ぶ必要があります。CloneProtocol::UpdateClock() と common::Scheduler::Dispatch() が定期的に呼ばれない場合、他のステーションがパケットの到着を待ち続ける状態になってしまう可能性がありますので注意してください。

クローンの状態

各クローンは、 CloneProtocol に登録されている間は CloneBase::IsRegisteredWithProtocol() が true を返します。また、登録されている CloneProtocol が通信中の間は CloneBase::IsActive() が true を返します。

更に、データを共有する相手のクローンと接続されているかを判定する場合はクローンの種類ごとに IsConnected() などの関数が利用できます。(詳細は各リファレンスを参照してください。) IsConnected() により接続されいると判定される相手に対しては、実際に値を送れる、または、受け取れることが保証されます。逆に、IsConnected() により接続されていないと判定される相手に対しても、タイミングによってデータが届く場合があります。 

クローンエレメントの状態

クローンエレメントがクローンに登録されている間は CloneElementBase::IsRegisteredWithCloneBase() が true を返します。また、そのクローンが CloneProtocol に登録されている間は CloneElementBase::IsRegisteredWithProtocol() が true を返します。

登録されているクローンが値を設定できる状態であれば、CloneElementBase::IsReadyToSetValue() が true を返します。この状態であれば SetValue() を呼んで値を設定できます。ただし、バッファがいっぱいなどの理由で失敗する場合もありますので注意してください。

値を受信して有効な値を取得できる状態であれば、IsValidValue() が true を返します。この状態であれば、GetValue() で有効な値を取得できます。EventCloneElement の場合は、新たなイベントが届いているときは HandleNext() が有効なアドレスを返します。

ReliableCloneElement または ReliableLargeCloneElement の場合、後から参加したステーションでも最新の値を取得できます。

送信間隔

送信間隔は CloneProtocol::SetSendPeriod() で設定することができます。この関数で設定した回数 CloneProtocol::UpdateClock() が呼び出されるたびに、common::Scheduler::Dispatch() 呼び出し時に送信処理が行われます。これにより、EventCloneElement で発行した数フレーム分のイベントを 1 つのパケットにまとめて送信することができる等、送信パケット数を抑えることができます。UnreliableCloneElement、ReliableCloneElement、ReliableLargeCloneElement については、最新の値のみ送信されます。

シリアライズについて

クローンエレメントに設定した値をパケットに入れて送信するときのシリアライズ方法を指定する必要があります。

uint8_t、int8_t、uint16_t、int16_t、uint32_t、int32_t、uint64_t、int64_t、float、double については予め SerializePolicy が特殊化されているため、明示的にシリアライズ方法を定義することなくクローンエレメントを作成することができます。

シリアライズ方法を定義する場合は、SerializePolicy<Type> の Serialize / Deserialize / GetSerializedSize 関数の特殊化をアプリケーションで実装する必要があります。

異種プラットフォーム間マッチメイクによる異なるプラットフォーム間での通信を行う際には、パディングによる構造体のサイズ変動やエンディアンの不一致等が起こる可能性があります。その場合に、Serialize / Deserialize / GetSerializedSize 関数で構造体のメモリコピーや sizeof() によるデータサイズ取得を行うように実装すると、正常に通信を行うことができません。将来にわたって異種プラットフォーム間マッチメイクを行わない場合は、構造体のメモリコピーや sizeof() によるデータサイズ取得を行うように実装することも可能です。

異種プラットフォーム間マッチメイクを想定した実装例を以下に示します。

例えば、

struct StructValue
{
    uint32_t v1;
    float v2;
};

という型をクローンエレメントに設定したい場合は、

template<>
void nn::pia::clone::SerializePolicy<StructValue>::Serialize(void* pBuffer, const Type& value)
{
    uint8_t* offset = reinterpret_cast<uint8_t*>(pBuffer);
    clone::SerializePolicy<uint32_t>::Serialize(offset, value.v1);
    offset += 4;
    clone::SerializePolicy<float>::Serialize(offset, value.v2);
}

template<>
void nn::pia::clone::SerializePolicy<StructValue>::Deserialize(Type* pValue, const void* cpData)
{
    const uint8_t* offset = reinterpret_cast<const uint8_t*>(cpData);
    clone::SerializePolicy<uint32_t>::Deserialize(&pValue->v1, offset);
    offset += 4;
    clone::SerializePolicy<float>::Deserialize(&pValue->v2, offset);
}

template<>
uint32_t nn::pia::clone::SerializePolicy<StructValue>::GetSerializedSize()
{
    return 4 + 4;
}

という実装をする必要があります。各引数のアライメントは不正な場合がある点に注意してください。

詳しくは、9.3. PiaClone 詳細仕様の「アプリケーションで SerializePolicy を定義する場合」を参照してください。

時刻の管理

CloneProtocol は、ステーション間で同期された時刻を管理しています。この時刻に基づいて、クローンエレメントの値の設定された順番や、 AtomicSharingClone のロックを取得しようとした順番などが判定されます。

アプリケーションは、CloneProtocol を使った通信を行っている間は定期的に(1 ゲームフレームに 1 回) CloneProtocol::UpdateClock() を呼び出す必要があります。UpdateClock() を呼ぶと現在時刻が 1 ゲームフレーム分更新されます。その他のタイミングで時刻が更新されることはありません。

時刻の管理は、アプリケーションの仕様によって 2 種類のクロックタイプから選ぶことができ、CloneProtocol::Initialize() で指定することができます。1 回の UpdateClock() 呼び出しで進む時間に違いがあり、それぞれ次の特徴があります。

  • RTC クロック

同期を開始した時刻からの RTC の経過時間の分、現在の時刻が進みます。1 秒あたりに進める時刻の値は、CloneProtocol::Setting::clockPerSec で指定します。

CloneProtocol での通信が開始されるときに時刻が同期され、それ以降、各ステーションの RTC に基づいて時刻が進んでいきます。そのため、RTC の誤差を無視すれば、ステーション間での時刻の同期も維持できていると考えることができ、時刻の再同期を行う必要がありません。ただし、処理落ちで UpdateClock() の呼び出しが一度飛ばされたり、微妙にタイミングがずれたりといった要因で、1回の UpdateClock() 呼び出しで進む時間は一定ではなくなります。特に、時刻の精度を平均的な UpdateClock() 呼び出し間隔程度に設定していた場合、 UpdateClock() を呼び出しても時刻の値は変わらないということがありえますので注意してください。

進んだ時刻に応じてゲーム内の1フレームあたりの更新量を可変にできる場合や、ステーション間でのフレームの対応関係が多少ずれても問題のないアプリケーションでは、こちらを選ぶことを推奨します。デフォルト値としては RTC クロックが使用されます。

  • フレームクロック

1 回の UpdateClock() 呼び出しにつき一定の時間だけ時刻が進みます。1 フレームあたりに進める時刻の値は、CloneProtocol::Setting::clockPerFrame で指定します。

単位時間当たりの UpdateClock() の呼び出し回数が全ステーションで確実に同じであることが保証できる場合は問題ありませんが、処理落ちなどにより UpdateClock() の呼び出し回数がずれてくると、それだけ時刻がずれてしまいます。

1 フレームに一定量ずつ処理が進むことが重要で、ステーション間でのフレームの対応関係を厳密に維持したいアプリケーションでは、こちらを選ぶことを推奨します。

時刻がずれた場合は、CloneProtocol::RegulateClock() を呼ぶことで再調整を行う事ができます。時刻の再調整時は、他のステーションの時刻との ずれを計測した後、通常より高速/低速に時刻を進めることにより、徐々にずれを補正していきます。

時刻は、セッション内で最初に CloneProtocol::Start() を行ったタイミングを基準に 0 から始まり、後から Start() したステーションは先に Start() したステーションに時刻を合わせていきます。

全員が CloneProtocol::Stop() で終了すると、次に Start() する際はまた 0 から時刻が始まりますが、CloneProtocol を動作させ続けている状態が長期間続くようなアプリケーションでは、いずれ時刻の値がオーバーフローしてしまう可能性があります。時刻がオーバーフローし てしまった場合は、CloneProtocol::GetError() が ErrorType_ClockOverflow を返します。この状態では値の保証ができませんので、一旦 CloneProtocol::Stop() で終了させる必要があります。時刻の進む量に極端に大きい値を設定した場合や、数か月のオーダーで CloneProtocol の通信が続いた場合以外ではオーバーフローすることはありません。

詳しい設定方法については、CloneProtocol::Setting のリファレンスを参照してください。