8.2. PiaSync 基本機能

PiaSync の基本的な機能を利用する場合の、手順や注意事項を以下に説明します。

PiaSync の初期化

SyncProtocol の初期化時に渡す SyncProtocol::Setting 構造体に各設定を行います。

警告:

SyncProtocol::Setting 構造体は、同期通信するステーション間で同じ値を指定する必要があります。異なる値を指定した場合の動作は保証されていません。

コード 8-1. PiaSync の初期化
nn::pia::Result result;
 
// PiaSync を初期化します。
result = nn::pia::sync::Initialize();
PIA_ASSERT(result.IsSuccess());
 
result = nn::pia::sync::BeginSetup();
PIA_ASSERT(result.IsSuccess());
 
// Transport シングルトン取得。
nn::pia::transport::Transport* pTransport = nn::pia::transport::Transport::GetInstance();
PIA_ASSERT(PIA_IS_VALID_POINTER(pTransport));
 
// SyncProtocol の作成
uint32_t m_hSyncProtocol = pTransport->CreateProtocol<nn::pia::sync::SyncProtocol>();
// CreateProtocol() は、既に同じプロトコル ID が存在していた場合は 0 を返すので、そうではないことを確認
PIA_ASSERT(m_hSyncProtocol != 0);
 
// SyncProtocol::Initialize に渡す設定を用意します
nn::pia::sync::SyncProtocol::Setting setting;
// maxDelay に入力遅延の最大値を設定します。
// 同期通信中に実際に設定される入力遅延の値は、同期通信開始時に設定します。
setting.maxDelay = 10;
// timeoutFrame にタイムアウトまでのフレーム数を設定します。
// パケットを受信できない状態がこのフレーム数(SyncProtocol::Step() の呼び出し回数)連続したら、タイムアウトとして同期通信を終了します。
// 0 を指定すると、タイムアウトは無効になります。
setting.timeoutFrame = 240;
// dataUnitSize は SyncProtocol::DataIdNum 個まで設定可能です。
// 各データ ID ごとに、同期データのサイズを設定する必要があります。ここでは データ ID の 0 番、1 番、2 番について設定しています。
// 設定できる最大値は、SyncProtocol::GetDataUnitSizeMax() で取得できます。
// これらのデータ ID の内、どの同期データを送信対象とするかは同期通信開始時に設定します。
setting.dataUnitSize[0] = 16;
setting.dataUnitSize[1] = 32;
setting.dataUnitSize[2] = 32;
// dataCompressionLevel に同期データの圧縮レベルを設定します。
// CompressionLevelNone、CompressionLevelLow、CompressionLevelMiddle、CompressionLevelHigh のいずれかを設定します。
// CompressionLevelNone を指定すると圧縮を行いません。CompressionLevelLow が速度重視、CompressionLevelHigh が圧縮率重視となります。
// CompressionLevelMiddle はその中間です。
// プラットフォームの CPU 性能によって処理時間が大きく変動します。
setting.dataCompressionLevel = nn::pia::sync::SyncProtocol::CompressionLevelMiddle;
 
// SyncProtocol の初期化
result = pTransport->GetProtocol<nn::pia::sync::SyncProtocol>(m_hSyncProtocol)->Initialize(setting);
PIA_ASSERT(result.IsSuccess());
 
result = nn::pia::sync::EndSetup();
PIA_ASSERT(result.IsSuccess());

 

同期通信開始

同期通信の開始時に、自分が送信対象とする同期データや自分が希望する入力遅延、自分の送信間隔を設定できます。同期通信を開始後、セッションに参加中のすべてのステーションの同期通信の準備完了が確認されて、全てのステーションの 0 フレーム用のデータを取得できるようになると、同期通信中の状態になります。

参考:

自分の送信対象のデータ ID は同期通信に参加しているすべてのステーションに共有されるため、各ステーションが送信するデータの ID が異なっても構いません。

コード 8-2. 同期通信開始
// SyncProtocol::Initialize() に渡した Setting 構造体で正の値のサイズを設定した同期データの内、どの同期データを自分の送信対象にするか設定します。
// ここでは、初期化時に SyncProtocol::Setting::dataUnitSize を設定して有効にしたデータ ID の 0 番、1 番、2 番の内、
// データ ID の 0 番と 2 番を送信するよう、ビットを立てて指定しています。
int32_t usingDataIdBitmap = 0x5;
 
// 入力遅延を指定します。この値は、初期化時に設定した SyncProtocol::Setting::maxDelay 以下である必要があります。
// フレームドロップが必要な状況が頻繁に起こる場合は、この値を大きくすることで改善することがあります。
// この値がそのまま入力遅延として設定されるわけではなく、各ステーションで指定された値の最大値が入力遅延として設定される点に注意してください。
uint32_t delay = 4;
 
// sendPeriod は、送信間隔(SyncProtocol::Step() を何回呼ぶごとに送信処理を行うか)を指定するパラメータです。
// 小さい値の方が頻繁に送信処理が行われますが、その分、単位時間当たりの送信パケット数が多くなります。
// 通信を安定させたいときは送信間隔を大きくしてください。
uint32_t sendPeriod = 1;
 
// 同期通信を開始します
// 参加中のすべてのステーションが同期通信を開始すると同期が開始されます
result = nn::pia::transport::Transport::GetInstance()->GetProtocol<nn::pia::sync::SyncProtocol>(m_hSyncProtocol)->Start(usingDataIdBitmap, delay, sendPeriod);
PIA_ASSERT(result.IsSuccess());

 

同期通信中の処理

同期通信中は、同期データの送受信を行いながら、同期通信のフレームを進めていきます。

通信状況が悪化し、次のフレーム用の同期データが全てのステーションから受信できていない場合は、SyncProtocol::Step() が ResultDataIsNotArrivedYet を返します。この Result も含め、Result が失敗を返したフレームではゲームの進行を止める必要があります。

警告:

ここでユーザー入力が無かったと判定して処理を進めると、ステーション間のゲーム状態が一致しなくなり同期ずれが起こります。

参考:

PiaSync では、SyncProtocol::Step() 成功時に、フレームが 1 進みます。現在のフレームを n、設定した入力遅延を m フレームとすると、そのフレームでは (n + m) フレーム用のデータを設定することになります。その後、m フレーム進む(= SyncProtocol::Step() に m 回成功する)と、SyncProtocol::GetData() により、(n + m) フレーム用のデータを取得できるようになります。

 

コード 8-3. 同期通信中の処理
nn::pia::sync::SyncProtocol* pSyncProtocol = nn::pia::transport::Transport::GetInstance()->GetProtocol<nn::pia::sync::SyncProtocol>(m_hSyncProtocol);

// インターネット通信の場合、サーバーとのキープアライブ通信などのために NEX の Scheduler::Dispatch を呼ぶ必要があります
if (nn::nex::Scheduler::GetInstance() != NULL)
{
    nn::nex::Scheduler::GetInstance()->DispatchAll();
}

// nn::pia::common::Scheduler::Dispatch() を定期的に呼び出す必要があります。
nn::pia::common::Scheduler::GetInstance()->Dispatch();

if (nn::pia::session::Session::GetInstance()->CheckConnectionError().IsFailure())
{
    // 通信が切断したので終了します。
    // エラーハンドリング
    return;
}

// 1 フレーム進めます
result = pSyncProtocol->Step();
if (result.IsFailure())
{
    if (result == nn::pia::ResultDataIsNotArrivedYet())
    {
        // まだ同期データが届いていないのでゲームフレームをドロップさせます
        // データを設定してからフレームが進んでいないので、ここで設定する必要はありません。
        PIA_ASSERT(pSyncProtocol->NeedsSetData() == false);
        return;
    }
    else if (r == nn::pia::ResultDataIsNotSet())
    {
        // 既に同期データを設定してあるので、この Result にはなりません。
        PIA_ASSERT(false);
    }
    else if (r == nn::pia::ResultInvalidState())
    {
        // 通信中なので、この Result にはなりません。
        PIA_ASSERT(false);
    }
    else if (r == nn::pia::ResultTemporaryUnavailable())
    {
        // 単独同期終了処理中には、この Result が返りますが、ハンドリングの必要はありません。
    }
    else
    {
        // その他の Result が返ることはありません。
        PIA_ASSERT(false);
    }
}

// 受信した同期データを取得します
for (uint32_t i = 0; i < MaxStations; ++i)
{
    nn::pia::StationId stationId = stationIdList[i];
    // 自分の状態と他のステーションの状態を確認し、同期データを取得可能であれば取得します。
    if (pSyncProtocol->CanGetData() && pSyncProtocol->CheckEntry(stationId))
    {
        uint32_t usingDataIdBitmap = pSyncProtocol->GetUsingDataIdBitmap(stationId);
        // 初期化時に SyncProtocol::Setting::dataUnitSize を設定して有効にしたデータ ID の 0 番、1 番、2 番 について確認します
        for (uint32_t j = 0; j < 3; ++j)
        {
            // 相手のステーションが送信対象としている同期データか確認し、対象であれば同期データを取得します
            // データ ID に応じて、適切な大きさの dataBuffer を用意する必要があります
            if ((0x1 << j) & usingDataIdBitmap)
            {
                result = pSyncProtocol->GetData(stationId, j, &dataBuffer);
                PIA_ASSERT(result.IsSuccess());
            }
        }
    }
}

// 送信する同期データを設定します
if (pSyncProtocol->NeedsSetData())
{
    // 送信する同期データを用意して SyncProtocol に設定します。
    // 初期化時に SyncProtocol::Setting::dataUnitSize を設定して有効にしたデータ ID の 0 番、1 番、2 番 について確認します
    for (uint32_t i = 0; i < 3; ++i)
    {
        // 自分が送信対象としている同期データか確認し、対象であれば同期データを設定します
        // データ ID に応じて、適切な大きさの dataBuffer を用意する必要があります
        if (pSyncProtocol->NeedsSetData(i))
        {
            result = pSyncProtocol->SetData(i, &dataBuffer);
            PIA_ASSERT(result.IsSuccess());
        }
    }
}

// 同期データの送信処理をすぐに行うために、もう一度 nn::pia::common::Scheduler::Dispatch() を呼びます。
nn::pia::common::Scheduler::GetInstance()->Dispatch();

 

同期通信の終了

SyncProtorol::End() を呼ぶと State_Ending に遷移し、同期通信を終了することを他のステーションに伝えます。その後、同期通信が終了し State_NotSynchronized に遷移します。

警告:

自分が SyncProtorol::End() を呼ばなくても、 State_Ending に遷移し、同期通信を終了することがある点に注意してください(下記の同期通信の進行状態遷移図を参照)。また、このとき終了するタイミング(フレーム番号)は、ステーション間で一致しないので、注意してください。

SyncProtocol::End() を呼んだ直後の段階では State_Ending のため、再び SyncProtocol::Start() を呼んで同期通信を開始するには、数フレームの間 SyncProtocol::Step()  を呼ぶことにより、 State_NotSynchronized になるまで同期通信の進行状態を進めておく必要があります。

 

PiaSync の終了処理

PiaSync の利用を終える場合には、終了処理を行う必要があります。初期化処理時とは逆順に各モジュールの終了処理を行う必要があります。

コード 8-4. PiaSync の終了処理
// SyncProtocol の終了処理
nn::pia::transport::Transport::GetInstance()->GetProtocol<nn::pia::sync::SyncProtocol>(m_hSyncProtocol)->Finalize();

// SyncProtocol の破棄
nn::pia::transport::Transport::GetInstance()->DestroyProtocol(m_hSyncProtocol);

// sync の終了処理
nn::pia::sync::Finalize();

 

同期通信の進行状態

SyncProtocol::GetState() により、自分の同期通信の進行状態を取得することができます。状態は、以下の表の 6 種類あります。

表 8-1. 同期通信の進行状態
SyncProtocol::State 説明
State_NotSynchronized 同期通信をしていない状態です。
State_Waiting

同期通信準備中の状態です。State_NotSynchronized のときに SyncProtocol::Start() を呼ぶと、この状態に遷移します。

State_Starting 同期通信開始処理中の状態です。セッションに参加中のすべてのステーションが State_Waiting の状態になると、この状態に遷移します。SyncProtocol::SetData() を呼び出して、同期データを設定する必要があります。
State_Synchronized 同期通信中の状態です。セッションに参加中の全てのステーションが State_Starting の状態になり、全てのステーションの 0 フレーム用の同期データを取得できるようになると、この状態に遷移します。この状態になった最初のフレームでは SyncProtocol::GetFrameNo() が 0 を返し、SyncProtocol::GetData() により 0 フレーム用の同期データを取得できます。その後、この状態で SyncProtocol::Step() が成功するたびに、フレームが 1 進み、そのフレーム用の同期データを取得できるようになります。
State_Ending 同期通信終了処理中の状態です。自分がSyncProtocol::End() を呼ぶ、または、他のステーションから終了を通知される等すると、この状態に遷移します。SyncProtocol::GetLastEndReason() で終了の理由を取得できます。
State_EndedAlone 自分だけ単独で同期通信を終了した状態です。SyncProtocol::EndAlone() による同期通信の単独終了が完了すると、この状態に遷移します。

State_NotSynchronized 以外の状態のときは、毎フレーム SyncProtocol::Step() を呼ぶ必要があります。

状態遷移をまとめると、以下のようになります。

図 8-3. 同期通信の進行状態遷移図



ステーションの離脱

ホスト、クライアント問わず、同期通信中にいずれかのステーションが離脱した場合は、同期通信が終了します。ただし、単独終了機能を使用することで、離脱するステーション以外の同期通信を維持させることも可能です。詳細は、8.3. PiaSync 応用機能 の「同期通信の単独終了」の項を参照してください。(異常切断による離脱時には同期通信の終了を回避できません)

入力遅延の設定

同期通信の開始時に、自分が希望する入力遅延を設定できます。フレームドロップを起こさないために必要な入力遅延の最小値は、次のようにして見積もることができます。

入力遅延を d フレームとし、片道通信遅延を c フレームとします。ある時刻におけるゲームフレームはステーションによって異なりますが、その差は縮まる傾向があります。そのため、この差は 0 と仮定します。また、毎ゲームフレーム、「同期データの受信」、「フレームの進行」、「同期データの送信」を行うこととし、「同期データの受信」から「同期データの送信」までの間隔を p フレームとします。「同期データの受信」や「同期データの送信」は、ディスパッチ処理中に行われます。ステーション 1 は n フレーム目の「同期データの送信」時に、(n + d) フレーム用のデータを送信します。ステーション 2 は、それを (n + d) フレーム目の「同期データの受信」時までに受信する必要があります。そのため、c <= (d - p) を満たす必要があるため、d の最小値は (c + p) となります。ただし、実際の通信時には、c が一定ではなく、パケットロスも発生するため、フレームドロップが全く発生しないわけではありません。

送信間隔の設定

同期通信の開始時に、送信間隔を設定できます。フレームドロップを起こさないために必要な送信間隔の最大値は次のようにして見積もることができます。

上述の「入力遅延の設定」では、毎ゲームフレーム送信を行う、つまり送信間隔が 1 としていました。送信間隔を q とすると、毎ゲームフレーム送信を行う場合と比較して、「同期データの送信」が最大で (q - 1) フレーム遅れることになります。そのため、c <= (d - p - (q - 1)) を満たす必要があるため、q の最大値は (d - c - p + 1) となります。

パケット補間機能

PiaSync では、最新のフレームだけではなく、まだ全てのステーションが受信できていない過去のフレームの同期データも送信しています。そのため、パケットロスが発生しても、次に受信するパケットから必要な同期データを取得することができます。これにより、通信帯域の増加を最小限に抑えながらパケットロス耐性を向上させています。

同期データのデータサイズ

同期データのデータサイズが大きいと、送信する同期データが 1 パケットに収まらない場合があります。その場合は複数のパケットに分割されて送信されます。パケット数を減らしたい場合は、同期データのサイズを小さくする必要があります。

同期データを 1 パケットに収めるための目安を表に示します(下記)。上記パケット補間機能により、送信する同期データは通信遅延(RTT)が大きいほど大きくなりますが最大まで大きくなった場合にも、各データ ID のデータサイズの合計が下記の表の値以下であれば 1 パケットに収められます。各データ ID のデータサイズは 4 の倍数になるように切り上げられるため、複数のデータ ID を使用する場合は、各データ ID のデータサイズが 4 の倍数になるようにすると、サイズの無駄なく送信を行えます。

この表の値はインターネット通信とローカル通信、MTU、署名の有無などによって異なります。同期データサイズ等を検討する際の目安としてください。

(インターネット通信 / MTU=1240 / 署名有り / 圧縮無効 / 送信間隔 1)

    データ ID 数
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

 

 

 

 

 

入力遅延

1 568 564 560 556 552 548 544 540 536 532 528 524 520 516 512 508
2 376 372 368 364 360 356 352 348 344 340 336 332 328 324 320 316
3 224 220 216 212 208 204 200 196 192 188 184 180 176 172 168 164
4 184 180 176 172 168 164 160 156 152 148 144 140 136 132 128 124
5 156 152 148 144 140 136 132 128 124 120 116 112 108 104 100 96
6 156 152 148 144 140 136 132 128 124 120 116 112 108 104 100 96
7 136 132 128 124 120 116 112 108 104 100 96 92 88 84 80 76
8 136 132 128 124 120 116 112 108 104 100 96 92 88 84 80 76
9 120 116 112 108 104 100 96 92 88 84 80 76 72 68 64 60
10 120 116 112 108 104 100 96 92 88 84 80 76 72 68 64 60

同期データの圧縮

同期データは圧縮することができます。圧縮の無効化や有効時の圧縮レベルは、SyncProtocol::Setting::dataCompressionLevel で設定することができます。

圧縮を有効化することでより多くの同期データを 1 つのパケットに収めることができ、パケット自体のサイズも小さくできる可能性があります。一方で、圧縮を有効化すると、圧縮/解凍の処理を行うために計算量は増加します。圧縮/解凍の処理は common::Scheduler::Dispatch() の中で行われます。

データの圧縮は zlib を用いて行われます。冗長性の乏しいデータであったり、ごく小さなサイズのデータを圧縮しようとした場合は、圧縮処理後のサイズがかえって増加してしまう可能性があります。