![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
2023 年の Unity ネットワーキングの状況に関する私の記事シリーズは続きます。今日の投稿では、リアルタイム マルチプレイヤー ゲームで利用されるデータ送信プロトコルについて説明します。
みなさんこんにちは!私は MY.GAMES のリード ソフトウェア エンジニア、Dmitrii Ivashchenko です。まず、ネットワーク相互作用のさまざまなレベルにどのようなプロトコルが存在するのかについて簡単に概要を説明します。
OSI (Open Systems Interconnection) モデルは、抽象化レベルの観点から通信システムの機能を特徴付け、標準化する概念モデルです。このモデルは、1978 年に国際標準化機構 (ISO) によって開発されました。
これは 7 つの層で構成されています。
物理層: この層は、チャネル上の生ビットの送受信を処理します。この層のプロトコルは、ビット表現、伝送速度、物理ケーブル、カード、コネクタ設計などの物理インターフェイスと媒体の特性を記述します。
データリンク層: この層は、物理媒体全体でのデータ転送を提供し、物理レベルで発生する可能性のあるエラーを処理します。
ネットワーク層: この層は、ネットワーク間のデータ送信のパス (ルーティング) を決定します。
トランスポート層: この層はポイント間のメッセージ配信を管理し、必要に応じて信頼性を提供します。
セッション層: この層は、ユーザーとアプリケーション間のセッションの確立、維持、終了を管理します。
プレゼンテーション層: この層は、送信側と受信側のデータ表現 (エンコーディング) の違いからのデータの独立性を保証します。
アプリケーション層: この層には、ユーザーが操作するアプリケーションに直接関係するプロトコルが含まれます。
各レイヤーは前のレイヤーの上に構築されることに注意してください。トランスポート層より上の層 (セッション、プレゼンテーション、およびアプリケーション) は高度に特殊化されており、リアルタイム マルチプレイヤーを組織するのには役立ちません。したがって、プレーヤー間のデータ交換を最適化するには、トランスポート層で停止し、その TCP および UDP プロトコルを使用する必要があります。
TCPは接続指向のプロトコルであり、データを交換するための接続を確立する 2 つのデバイス間で通信が行われます。このプロトコルは、送信されたすべてのデータが正しい順序で宛先に到達することを保証するため、信頼性を保証します。送信中にデータが失われた場合、TCP はすべてのデータが正常に送信されるまでリクエストを自動的に再試行します。
接続を確立するには、次の手順を実行します。
以下の例は、TCP クライアントの基本的な実装を示しており、特定のデータやゲーム ロジックを処理するように拡張できます。
このコードは、指定された IP アドレスとポートを使用してサーバーに接続し、その接続を通じてデータを送受信します。ネットワーク ストリームは、サーバーからの非同期データ受信に使用されます。
using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class TCPClient : MonoBehaviour { private TcpClient tcpClient; private NetworkStream networkStream; private byte[] receiveBuffer; private void Start() { // Example: Connect to server with IP address 127.0.0.1 (localhost) and port 5555 ConnectToServer("127.0.0.1", 5555); } private void ConnectToServer(string ipAddress, int port) { tcpClient = new TcpClient(); tcpClient.Connect(IPAddress.Parse(ipAddress), port); networkStream = tcpClient.GetStream(); // Start asynchronous operation to receive data from the server receiveBuffer = new byte[tcpClient.ReceiveBufferSize]; networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void ReceiveData(IAsyncResult result) { int bytesRead = networkStream.EndRead(result); byte[] receivedBytes = new byte[bytesRead]; Array.Copy(receiveBuffer, receivedBytes, bytesRead); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from server: " + receivedMessage); // Continue the asynchronous operation to receive data networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); networkStream.Write(sendBytes, 0, sendBytes.Length); networkStream.Flush(); } }
ConnectToServer(string ipAddress, int port)
メソッドは、指定された IP アドレスとポートでサーバーへの接続を確立します。サーバーからのデータ受信はメソッドReceiveData(IAsyncResult result)
で実行され、データ送信はメソッドSendData(string message)
で実行されます。受信したデータはDebug.Log
を使用してコンソールに出力されます。
以下のコード例は、Unity の単純な TCP サーバーを表しています。このコードはサーバーを初期化し、指定されたポートをリッスンして、クライアントからの受信接続を受け入れます。クライアントに接続した後、サーバーはネットワーク ストリームを通じてデータを送受信します。
StartServer(int port)
メソッドは、指定されたポートでサーバーを初期化し、受信接続の待機を開始します。クライアント接続が確立されると、 HandleIncomingConnection(IAsyncResult result)
メソッドが実行され、クライアントからデータを受信し、データを受信するための非同期操作が開始されます。
受信したデータはReceiveData(IAsyncResult result)
メソッドで処理されます。クライアントからデータを受信した後、サーバーは必要な処理を実行するか、データをクライアントに送り返すことができます。
SendData(string message)
メソッドは、ネットワーク ストリームを通じてクライアントにデータを送信します。データはバイト配列に変換され、クライアントに送信されます。
using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class TCPServer : MonoBehaviour { private TcpListener tcpListener; private TcpClient connectedClient; private NetworkStream networkStream; private byte[] receiveBuffer; private void Start() { // Example: Start the server on port 5555 StartServer(5555); } private void StartServer(int port) { tcpListener = new TcpListener(IPAddress.Any, port); tcpListener.Start(); Debug.Log("Server started. Waiting for connections..."); // Start accepting client connections asynchronously tcpListener.BeginAcceptTcpClient(HandleIncomingConnection, null); } }
HandleIncomingConnection
メソッドは、受信クライアント接続を処理するために使用されます。接続を受け入れた後、クライアントとのデータ交換用のストリームを取得し、クライアントからデータを受信するための非同期操作を開始します。
次に、接続されたクライアントから受信したバッファ サイズに基づいて、データを受信するためのバッファを作成します。非同期操作が起動され、作成されたバッファを使用してストリームからデータが読み取られます。読み取り操作が完了すると、データはさらなる処理のためにReceiveData
メソッドに渡されます。
また、このメソッドは別の非同期操作を開始して、次のクライアントを受け入れる可能性があるために着信接続を受け入れます。
クライアントがサーバーに接続すると、このメソッドが呼び出されて接続が処理され、サーバーは次のクライアントを非同期で受け入れる準備が整います。
private void HandleIncomingConnection(IAsyncResult result) { connectedClient = tcpListener.EndAcceptTcpClient(result); networkStream = connectedClient.GetStream(); Debug.Log("Client connected: " + connectedClient.Client.RemoteEndPoint); // Start asynchronous operation to receive data from the client receiveBuffer = new byte[connectedClient.ReceiveBufferSize]; networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); // Accept next client connection asynchronously tcpListener.BeginAcceptTcpClient(HandleIncomingConnection, null); }
ReceiveData
メソッドは、クライアントから受信したデータを処理するために使用されます。データ読み取り操作が完了した後、メソッドは読み取られたバイト数をチェックします。バイト数が 0 以下の場合、クライアントが切断されたことを意味します。この場合、メソッドはクライアントとの接続を閉じ、実行を終了します。
読み取られたバイト数が 0 より大きい場合、メソッドは受信データのバイト配列を作成し、読み取ったデータをこの配列にコピーします。次に、メソッドは受信したバイトを UTF-8 エンコードを使用して文字列に変換し、受信したメッセージをコンソールに出力します。
private void ReceiveData(IAsyncResult result) { int bytesRead = networkStream.EndRead(result); if (bytesRead <= 0) { Debug.Log("Client disconnected: " + connectedClient.Client.RemoteEndPoint); connectedClient.Close(); return; } byte[] receivedBytes = new byte[bytesRead]; Array.Copy(receiveBuffer, receivedBytes, bytesRead); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from client: " + receivedMessage); // Process received data // Continue the asynchronous operation to receive data networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); networkStream.Write(sendBytes, 0, sendBytes.Length); networkStream.Flush(); Debug.Log("Sent to client: " + message); }
SendData
メソッドは、クライアントにデータを送信するために使用されます。 UTF-8 エンコーディングを使用してメッセージ文字列をバイト配列に変換し、この配列をネットワーク ストリームに書き込みます。データを送信した後、メソッドはストリームをクリアし、送信されたメッセージをコンソールに出力します。
信頼性は大きなプラスのように聞こえるかもしれませんが、この TCP 機能はリアルタイム マルチプレイヤー ゲームで問題を引き起こす可能性があります。 TCP でのデータ送信は、再送信とフロー制御のメカニズムによって遅くなり、遅延または「ラグ」が発生する可能性があります。
UDP は、配信やパケットの順序を保証しない単純なプロトコルです。これにより、接続の確立や失われたパケットの再送信に時間がかからないため、TCP よりもはるかに高速になります。 UDP は、その速度とシンプルさにより、リアルタイム データ送信を必要とするネットワーク ゲームやその他のアプリケーションでよく使用されます。
ただし、UDP を使用するには、開発者はデータ送信をより慎重に管理する必要があります。 UDP は配信を保証しないため、失われたパケットや順序が狂って到着したパケットを処理する独自のメカニズムを実装する必要がある場合があります。
このコードは、Unity での UDP クライアントの基本的な実装を示しています。 StartUDPClient
メソッドは、UDP クライアントを初期化し、IP アドレスとポートで指定されたリモート サーバーに接続します。クライアントはBeginReceive
メソッドを使用して非同期でデータの受信を開始し、 SendData
メソッドを使用してサーバーにメッセージを送信します。
using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class UDPExample : MonoBehaviour { private UdpClient udpClient; private IPEndPoint remoteEndPoint; private void Start() { // Example: Start the UDP client and connect to the remote server StartUDPClient("127.0.0.1", 5555); } private void StartUDPClient(string ipAddress, int port) { udpClient = new UdpClient(); remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); // Start receiving data asynchronously udpClient.BeginReceive(ReceiveData, null); // Send a message to the server SendData("Hello, server!"); } private void ReceiveData(IAsyncResult result) { byte[] receivedBytes = udpClient.EndReceive(result, ref remoteEndPoint); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from server: " + receivedMessage); // Continue receiving data asynchronously udpClient.BeginReceive(ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); // Send the message to the server udpClient.Send(sendBytes, sendBytes.Length, remoteEndPoint); Debug.Log("Sent to server: " + message); } }
サーバーからデータを受信すると、 ReceiveData
メソッドが呼び出され、受信したバイトが処理されて文字列に変換されます。受信したメッセージはコンソールに記録されます。クライアントは、 BeginReceive
再度呼び出すことで、データの非同期受信を続けます。
SendData
メソッドはメッセージをバイトに変換し、UDP クライアントのSend
メソッドを使用してサーバーに送信します。
このコードは、Unity での UDP サーバーの基本的な実装を示しています。 StartUDPServer
メソッドは、指定されたポートで UDP サーバーを初期化し、 BeginReceive
メソッドを使用してデータの非同期受信を開始します。
クライアントからデータを受信すると、 ReceiveData
メソッドが呼び出され、受信したバイトが処理されて文字列に変換されます。受信したメッセージはコンソールに記録されます。サーバーはBeginReceive
再度呼び出すことで、データの非同期受信を続けます。
using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class UDPServer : MonoBehaviour { private UdpClient udpServer; private IPEndPoint remoteEndPoint; private void Start() { // Example: Start the UDP server on port 5555 StartUDPServer(5555); } private void StartUDPServer(int port) { udpServer = new UdpClient(port); remoteEndPoint = new IPEndPoint(IPAddress.Any, port); Debug.Log("Server started. Waiting for messages..."); // Start receiving data asynchronously udpServer.BeginReceive(ReceiveData, null); } private void ReceiveData(IAsyncResult result) { byte[] receivedBytes = udpServer.EndReceive(result, ref remoteEndPoint); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from client: " + receivedMessage); // Process the received data // Continue receiving data asynchronously udpServer.BeginReceive(ReceiveData, null); } private void SendData(string message, IPEndPoint endPoint) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); // Send the message to the client udpServer.Send(sendBytes, sendBytes.Length, endPoint); Debug.Log("Sent to client: " + message); } }
SendData
メソッドは、メッセージと、クライアントのアドレスとポートを表すIPEndPoint
を受け取ります。メッセージをバイトに変換し、UDP サーバーのSend
メソッドを使用してクライアントに送信します。
ゲーム開発のコンテキストでは、TCP と UDP のどちらを選択するかは、ゲームのタイプに大きく依存します。ゲームで信頼性の高いデータ配信が必要であり、応答時間が重要な要素ではない場合 (リアルタイム ストラテジーやターンベース ゲームなど)、TCP が適切な選択肢となる可能性があります。一方、ゲームで高速データ転送が必要で、ある程度のデータ損失に対処できる場合 (たとえば、一人称シューティング ゲームやレーシング ゲームなど)、UDP が最適な選択となる可能性があります。
WebSocket は、ブラウザとサーバー間の永続的な接続の確立を可能にする通信プロトコルです。通常の HTTP との主な違いは、双方向通信が可能であることです。サーバーはクライアントのリクエストに応答するだけでなく、クライアントにメッセージを送信することもできます。
WebSocket は、TCP に基づくアプリケーション レベルのプロトコルです。ストリームではなくメッセージをサポートするため、通常の TCP とは異なります。 WebSocket には、パフォーマンスのオーバーヘッドを追加する可能性のある追加機能が含まれています。
仕組みは次のとおりです。
クライアントは、「アップグレード リクエスト」と呼ばれる特別な HTTP リクエストを送信します。このリクエストは、クライアントが WebSocket に切り替えたいことをサーバーに通知します。
サーバーが WebSocket をサポートし、切り替えに同意した場合、WebSocket へのアップグレードを確認する特別な HTTP 応答で応答します。
これらのメッセージを交換した後、クライアントとサーバーの間に永続的な双方向接続が確立されます。双方は、相手側からのリクエストに応答するだけでなく、いつでもメッセージを送信できます。
これで、クライアントとサーバーはいつでもメッセージを送受信できるようになります。各 WebSocket メッセージは、メッセージの開始と終了を示す「フレーム」でラップされます。これにより、メッセージが混合順序で到着したり、ネットワークの問題により部分に分割されたりした場合でも、ブラウザーとサーバーはメッセージを正しく解釈できます。
どちらの側でも、特別な「接続を閉じる」フレームを送信することで、いつでも接続を閉じることができます。相手側はクロージャの確認で応答することができ、その後、双方は他のデータの送信を直ちに停止する必要があります。ステータス コードは、閉鎖の理由を示すために使用することもできます。
以下のコードは、C# 言語とWebSocketSharpライブラリを使用して Unity に WebSocket クライアントを実装する例を示しています。
オブジェクトの初期化時に呼び出されるStart()
メソッドでは、 WebSocket
の新しいインスタンスが作成され、WebSocket サーバーのアドレスで初期化されます。
その後、イベント ハンドラーOnOpen
およびOnMessage
が設定されます。
using UnityEngine; using WebSocketSharp; public class WebSocketClient : MonoBehaviour { private WebSocket ws; void Start() { ws = new WebSocket("ws://your-websocket-server-url/Auth"); ws.OnOpen += OnOpenHandler; ws.OnMessage += OnMessageHandler; ws.ConnectAsync(); } private void OnOpenHandler(object sender, System.EventArgs e) { var data = "Player1"; ws.Send(data) } private void OnMessageHandler(object sender, MessageEventArgs e) { Debug.Log("WebSocket server said: " + e.Data); } }
OnOpen
は、サーバーとの接続が確立されるとトリガーされます。この例では、接続が確立されると、テキスト「Player1」を含むメッセージがサーバーに送信されます。
OnMessage
は、サーバーからメッセージを受信したときにトリガーされます。ここでは、メッセージを受信すると、その内容がコンソールに表示されます。
次に、 ConnectAsync()
メソッドが呼び出され、サーバーに非同期的に接続されます。
以下のコードは、WebSocket サーバーを作成する例です。
オブジェクトの初期化時に呼び出されるStart()
メソッドでは、 WebSocketServer
の新しいインスタンスが作成され、WebSocket サーバーのアドレスで初期化されます。次に、 AuthBehaviour
WebSocket サービスがサーバーに追加され、 /Auth
パスで利用可能になります。その後、 Start()
メソッドを使用してサーバーが起動されます。
using UnityEngine; using WebSocketSharp; using WebSocketSharp.Server; public class WebSocketServer : MonoBehaviour { void Start() { var socket = new WebSocketServer("ws://your-websocket-server-url"); socket.AddWebSocketService<AuthBehaviour>("/Auth"); socket.Start(); } } public class AuthBehaviour : WebSocketBehavior { protected override void OnMessage (MessageEventArgs e) { var playerName = e.Data; Debug.Log("WebSocket client connected: " + playerName); Send("Auth Complete: " + playerName); } }
AuthBehaviour
、クライアントからメッセージを受信するときのサーバーの動作を定義するWebSocketBehavior
から派生したクラスです。ここでは、サーバーがクライアントからメッセージを受信したときに呼び出されるOnMessage()
メソッドがオーバーライドされます。
このメソッドでは、最初にメッセージ テキストが抽出され、次に、メッセージで渡された名前を使用して、どのクライアントが接続したかを示すメッセージがコンソールに出力されます。次に、サーバーは認証の完了に関する情報を含むメッセージをクライアントに送り返します。
TCP、UDP、WebSocket を使用して Unity ゲームの接続を作成する方法と、それぞれの長所と短所について説明しました。 UDP の軽量な性質と TCP の信頼性にもかかわらず、どちらもゲームでリアルタイム マルチプレイヤーを組織するのには良い選択肢ではありません。 UDP では一部のデータが失われ、TCP では必要な伝送速度が提供されません。次の記事では、UDP を使用して信頼性の高いデータ送信を構成する方法について説明します。