ネットワークとLLAPIを理解する

滑らかなマルチプレイヤー体験のためにARDKのP2PネットワークLLAPIを用いる。

概要

特に、仮想コンテンツと、プレイヤーがカメラフィードを通して見ることのできる現実世界のオブジェクトの同期を行う場合は、共有AR体験に迫力を加える高速なネットワークが必要です。たとえば、エアホッケーのARゲームで、メッセージに0.5秒のタイムラグがあるとします。プレイヤーは相手の動きを実際に見ていながら、パックを「見逃す」ことがありますが、ゲームではヒットと判定される場合があります。

流動的なマルチプレイヤー体験を実現するために、ARDKにはLLAPIピアツーピアのネットワークスタック(このページで説明)と ゲーム抽象化 API のHLAPIレイヤーが付属しています。

クライアント - サーバー間のアーキテクチャ

ARDKは、セッション管理、ピアディスカバリー、メッセージルーティング、セッションベースのデータストアを提供するサーバー(ARBE: ARバックエンド)で管理されています。サーバーはほとんどステートレスであるため、体験はセッション内のデバイスによって実現できます。体験では、単一の「権限者」が計算を行い、他のピアに状態メッセージを送信するか、分散ロジックシステムを使用します。

セッション

セッションとは、ARDKの基本的なネットワーク概念です。クライアントが指定されたセッションIDを使用して参加リクエストを送信すると、サーバーは次のいずれかを行います。

  • クライアントをセッションに追加します(セッションが存在する場合)。

または

  • セッションを作成し、クライアントを「ホスト」として指定します。

その後、同一セッションに参加するクライアントには、ホストであるクライアントの情報が通知されます。クライアントがセッションへの接続に成功し、メッセージの送受信を開始できるようになると、IMultipeerNetworking.Connected(ConnectedArgs args)イベントが発生します。

セッションを作成し、参加するには、次のように行います。

using Niantic.ARDK.AR.Networking;
using Niantic.ARDK.Networking.MultipeerNetworkingEventArgs;
using Niantic.ARDK.Networking;
using System.Text;

...

void CreateAndJoinNetworking()
{
  // Create a networking session with default ARBE server endpoint
  var networking = MultipeerNetworkingFactory.Create();

  // Subscribe to the connected event to be notified upon connection success
  networking.Connected += OnNetworkingConnected;

  // Get a session ID from a string. In a real application, all clients that join
  //   the same session will need to agree upon a session ID.
  string sampleSessionString = "sample_id";
  var sessionIdFromString = Encoding.UTF8.GetBytes(sampleSessionString);

  // Send a Join message to ARBE with the session ID
  networking.Join(sessionIdFromString);
  return;
}

void OnNetworkingConnected(ConnectedArgs args)
{
  // Log some information from the connected message
  Debug.LogFormat
  (
    "Connected to session with client ID: {0}, and host ID: {1}. Am I the host? {2}",
    args.Self.Identifier,
    args.Host.Identifier,
    args.IsHost
  );

  // Do something now that the session is connected
}

セッションIDの衝突

セッションは、最後のクライアントのセッションから切断されてから30秒後にタイムアウトします。このタイムアウトの間に新しいクライアントが誤ってセッションに参加すると、進行中または完了したゲームステートに入り、意図しない動作が生じる可能性があります。

そのため、セッションごとに一意のセッションIDを使用することをお勧めします。さらに、デフォルトですべてのセッションIDにプレフィックスまたはサフィックス(アプリケーション名など)を付加することも、衝突を防止する上で役に立ちます。

NetworkingManagerの使用

ネットワークセッションの作成と参加のプロセスを簡略化するために、Manager をシーンに追加します。NetworkSessionManagerのAPIリファレンスとコード内のコメントやツールのヒントでは、その使用方法について説明しています。

ピア

ピアは、ネットワーキングセッションにおけるクライアントをコード内で表現したものです。接続後数秒以内に、IMultipeerNetworking.PeerAdded(PeerAddedArgs args)イベントを通じて、セッションに参加済みのピアの存在がクライアントに通知されます。このイベントは、ローカルクライアントの後に参加したピアにも発生します。セッションに参加中のピア(ローカルクライアントを除く)の詳細なリストにアクセスするには、IMultipeerNetworking.OtherPeers.Peersイベントを使用します。

同様に、ピアがセッションから離脱すると、IMultipeerNetworking.PeerRemoved(PeerRemovedArgs args)イベントが発生します。ローカルクライアントが参加する前にセッションに参加して離脱したピアは表示されません。

ピアを追加または削除するには、次のように行います。

void SubscribeToPeerAddedRemoved(IMultipeerNetworking networking)
{
  networking.PeerAdded += OnPeerAdded;
  networking.PeerRemoved += OnPeerRemoved;
}

void OnPeerAdded(PeerAddedArgs args)
{
  Debug.LogFormat("Peer joined: {0}", args.Peer.Identifier);
}

void OnPeerRemoved(PeerRemovedArgs args)
{
  Debug.LogFormat("Peer left: {0}", args.Peer.Identifier);
}

メッセージ

ネットワークのロジックの大部分は、メッセージの送受信が中心で、アクションやゲームステートの変更をピアに通知したり、アニメーションの同期を行ったりします。低レベルのAPI(このページで説明)では、ネットワークメッセージは uint タグと byte[] メッセージで構成されています。

タグは、メッセージの内容に関するルールの作成に役立ちます。たとえば、Pongゲームでは、 1 タグの付いたメッセージにはローカルプレイヤーの位置を表すシリアル化された Vector3 が含まれるのに対し、 2 タグの付いたメッセージには現在のスコアを表す ints のペアが含まれます。

メッセージの送信

メッセージを送信するためのAPIと、メッセージの送信に使用するトランスポートプロトコルのオプションが用意されています。

プロトコルの異なるさまざまなピアにメッセージを送信する3つの例をご紹介します。

// A message sent to a single peer in the session
void SendToASinglePeer(IMultipeerNetworking networking, IPeer peer, byte[] data)
{
  networking.SendDataToPeer(tag: 0, data: data, peer: peer, transportType: TransportType.UnreliableUnordered);
}

// Send data all peers in the session except self and host
void SendToAllPeersButHost(IMultipeerNetworking networking, byte[] data)
{
  // Generate a list of all other peers in the session
  var peerListCopy = networking.OtherPeers.ToList();

  // Remove the host from the list
  peerListCopy.Remove(networking.Host);

  networking.SendDataToPeers(tag: 1, data: data, peers: peerListCopy, transportType: TransportType.ReliableOrdered);
}

// Send data to everyone in the session, including the local peer
void BroadCastToSession(IMultipeerNetworking networking, byte[] data)
{
  // SendToSelf: true indicates that this message will be sent to the local peer as well
  networking.BroadcastData(tag: 2, data: data, transportType: TransportType.UnreliableOrdered, sendToSelf: true);
}

メッセージの受信

パイプの反対側では、メッセージを送信するメソッドに関係なく、受信したすべてのピアメッセージによって、イベント IMultipeerNetworking.PeerDataReceived(PeerDataReceivedArgs args) が発生します。

なお、空のメッセージ(datanull 、または空の配列)が送信された場合は、空のメッセージ(args.DataLength == 0)が届きます。このメッセージは、タグと併用すると、デバッグする際に役立ちます。

using System.IO;

void SubscribeToPeerDataReceived(IMultipeerNetworking networking)
{
  networking.PeerDataReceived += OnPeerDataReceived;
}

// Every time a message is received, this will be called
void OnPeerDataReceived(PeerDataReceivedArgs args)
{
  // Log the details of the peer message
  Debug.LogFormat
  (
    "Received a message from {0}, with tag {1} and length {2}",
    args.Peer,
    args.Tag,
    args.DataLength
  );

  MemoryStream data = args.CreateDataReader();

  // Properly handle the message depending on the tag and contents
}

トランスポートタイプ

ARDKでは、 UnreliableUnorderedUnreliableOrderedReliableUnorderedReliableOrdered の4種類のトランスポートタイプを使用してメッセージを送信することができます。

注釈

ARDK 2.2時点では、 UnreliableOrderedReliableOrdered はサポートされていますが、廃止予定であり、将来のリリースで削除される可能性があります。代わりに UnreliableUnorderedReliableUnordered タイプを使用してください。

送信者によって選択されたトランスポートタイプによって、受信者が受け取る(または受け取らない)メッセージの動作が異なる場合があります。

信頼性が低いメッセージはドロップされるか、または受信されないことがあります。このタイプは、アバターの現在位置やビデオの1フレームなど、ドロップが受け入れられる一時データなどに適切です。クライアントが20通のメッセージのうち1通を受信できなかった場合は、多少もたつきはあるかもしれませんが、そのデータを再送または復元しようとするとオーバーヘッドがかかるだけでなく、「完璧」を求めてメッセージをロールバックまたは待機すると、ラグが生じます。一方、信頼性が高いメッセージは受信することが「保証」されていますが、かかるオーバーヘッドは大きくなり、一般的に遅くなる可能性があります。

順序なしのメッセージは順序を無視して到達するのに対し、順序あり のメッセージは到達時の順序が維持されます。ただし、 ReliableOrdered のメッセージの連鎖は、まだ到達していない単一のメッセージでブロックされる可能性があるため、デフォルトで ReliableOrdered を使用する場合には注意が必要です。

現在 UnreliableUnordered はサーバー中継方式のUDPとして実装されています。メッセージは順序どおりに受信されない場合があります(先着順)。信頼性の低いメッセージは、信頼性が高いメッセージよりも帯域幅のオーバーヘッドが小さく、レイテンシーが発生しにくいのが特徴です。一般的に、信頼性が低いメッセージは、アニメーションの再生や、最新の位置情報データ(次の位置情報更新で上書きされる)、またはオーバーヘッドが小さいメッセージの送信など、時間による影響を受けやすい「破棄可能な」アクションに適しています。

現在 ReliableUnordered は、サーバー中継方式のTCPとして実装されています。メッセージは順序どおりに受信されますが、TCPメッセージには追加のオーバーヘッドがあり、複数回連続してメッセージが破棄されると、その後のメッセージの処理にかかるレイテンシーが大きくなります。一般的に、信頼性の高いメッセージは、スコアの記録やアセットの生成などのゲームステート情報や、すべてのデバイス間で同期されないと共有体験が低下するような内容に適しています。

ユースケースに応じて、順序どおりに送信されないメッセージを破棄する「信頼性は低いが順序どおりに送信する」動作が必要になる場合があります。たとえば、送信側がメッセージ {1, 2, 3, 4} を送信し、受信側が {1, 4, 2, 3} を順序どおりに受信した場合は、メッセージ1と4のみを受け取ります(2と3は破棄されます)。そのためには、 UnreliableUnordered タイプを使用して、送信側が新しいメッセージごとにインクリメントするシーケンス番号をペイロードデータに埋め込んでおくことをお勧めします。受信側 OnPeerDataReceived() で最新のシーケンス番号を記録し、古いシーケンス番号の受信メッセージは削除します。

トランスポートタイプに関するその他のベストプラクティスについては、 ネットワークの制限とベストプラクティス を参照してください。

こちらもご覧ください