Relay と UTP の使用

Relay SDK は、ネットワークを抽象化するために Unity ゲームエンジン用に構築された最新のネットワークライブラリ、Unity Transport Package (UTP) と良く連携して機能します。UTP を使用することで、開発者は低レベルのプロトコルやネットワークフレームワークよりもゲームに集中できます。UTP はネットコードに依存しないため、さまざまな高レベルのネットワークコード抽象化に対応でき、Unity のすべてのネットコードソリューションをサポートし、他のネットコードライブラリと連携します。

ノート: 推奨されるベストプラクティスは、Relay と NGO を使用することです。NGO 以外のもの (カスタムネットコードなど) を使用してゲームオブジェクトを同期したい場合は、Relay と UTP をトランスポートと Relay プロキシの用途で使用することができます。

Relay SDK を設定する

Relay を使用する前に、Unity 用に Unity プロジェクトを設定する必要があります。そのためには、Relay の使用を開始する を参照してください。Relay サービスを有効にした後で、Unity エディターで Unity プロジェクト ID をリンクします。

Relay と UTP の使用の基本については、Simple Relay Sample (using UTP) (シンプルな Relay サンプル (UTP を使用)) を参照してください。

Simple Relay Sample (using UTP)

このサンプルは、Simple Relay Sample (シンプルな Relay サンプル) を拡張したものです。これは、Relay と UTP を使用したマルチプレイヤーゲームの構築の基本を示しています。Package Manager (パッケージマネージャー) で Relay パッケージからこのサンプルをインポートし、含まれているスクリプトに従ってください。

このサンプルは、2 つ以上の別々のクライアントで動作します。1 人のホストプレイヤーと、1 人以上の参加プレイヤーです。

サンプルプロジェクトのコードをテストするときに、同じマシン上で複数のクライアントを実行したい場合があるかもしれません。これを行うには、プロジェクトを実行可能なバイナリにビルドするか、同じプロジェクトの複数の Unity エディターインスタンスを実行できる ParrelSyncMultiplayer Play Mode などのソリューションを使用します。

プロジェクトをバイナリにビルドする場合は、開いている Unity エディターまたはバイナリのクローンと共にバイナリを使用できます。

シーンを再生したら、クライアントをホストプレイヤー (ゲームセッションを開始するプレイヤー) または参加プレイヤーとして起動できます。

少なくとも 1 人のホストプレイヤーが必要であり、ホストプレイヤーは参加プレイヤーのためにゲームを実行する必要があります。

ホストプレイヤーのフローは以下のようになります。

  • サインインする

  • 地域を取得する (任意)

  • 地域を選択する (任意)

  • ゲームセッションを割り当てる

  • 選択された Relay サーバーにバインドする

  • 参加コードを取得する

ホストプレイヤーは、参加コードを参加プレイヤーと共有します。参加プレイヤーが参加してホストプレイヤーに接続したら、ホストプレイヤーは以下のことができます。

  • すべての接続プレイヤーにメッセージを送信する

  • すべての接続プレイヤーを切断する

プレイヤーのフローは以下のようになります。

  • サインインする

  • ホストプレイヤーの参加コードを使用して、ゲームセッションに参加する

  • 選択された Relay サーバーにバインドする

  • ホストプレイヤーへの接続をリクエストする

ホストプレイヤーに接続したら、プレイヤーは以下のことができます。

  • ホストプレイヤーにメッセージを送信する

  • ホストプレイヤーから切断する

Simple Relay Sample (using UTP) プロジェクトをインポートする

  1. Unity エディター (バージョン 2020.3) で Relay プロジェクトを開きます。Relay プロジェクトがまだない場合は、Relay プロジェクトを設定する を参照してください。

  2. Package Manager (パッケージマネージャー) を開き、Relay パッケージに移動します。

  3. Samples (サンプル) セクションを展開します。

  4. Import (インポート) を選択して、Simple Relay Sample (using UTP) プロジェクトをインポートします。

  5. Simple Relay Sample プロジェクトをインポートしたら、シーンとして開くことができます。これは Assets/Samples/Relay/1.0.4/Simple Relay Sample (using UTP) の下の、現在のプロジェクト内にあります。

  6. File (ファイル) > Open Scene (シーンを開く) を選択します。

  7. Simple Relay Sample (using UTP) シーンに移動します。

Unity Services を初期化する

Relay などの Unity サービスを使用する前には、UnityServices シングルトンを初期化する必要があります。Services Core API を参照してください。

await UnityServices.InitializeAsync();

プレイヤーの認証を行う

ホストプレイヤーと接続プレイヤーの両方の認証を行う必要があります。プレイヤーの認証を行う最も簡単な方法は、Authentication サービスの SignInAnonymouslyAsync() メソッドを使用する方法です。匿名ログインの使用方法ソーシャルサインインの使用方法 を参照してください。

詳細については、Unity Authentication の概要Unity Authentication のユースケース を参照してください。

public async void OnSignIn()
{
    	await  AuthenticationService.Instance.SignInAnonymouslyAsync();
    	playerId = AuthenticationService.Instance.PlayerId;

    	Debug.Log($"Signed in. Player ID: {playerId}");
}

ホストプレイヤー

ゲームクライアントがホストプレイヤーとして機能する場合、そのゲームクライアントは、割り当ての作成、参加コードのリクエスト、接続タイプの設定、Relay サーバーをバインドして参加プレイヤーからのリクエストをリッスンするための NetworkDriver のインスタンスの作成を実行できる必要があります。

ホストプレイヤーの更新ループ

割り当てフローの最初のステップを開始する前に、着信した接続とメッセージを処理する、ホストプレイヤーの更新ループを設定する必要があります。

ノート: わかりやすくするために、サンプルコードではこのループを Update() フレームごとに呼び出しています。実際には、別のコルーチンでホストプレイヤーの更新ループを処理する方が理にかなっています。

void UpdateHost()
{
    	// Skip update logic if the Host isn't yet bound.
    	if (!hostDriver.IsCreated || !hostDriver.Bound)
    	{
        	return;
    	}

    	// This keeps the binding to the Relay server alive,
    	// preventing it from timing out due to inactivity.
    	hostDriver.ScheduleUpdate().Complete();

    	// Clean up stale connections.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	if (!serverConnections[i].IsCreated)
        	{
            	Debug.Log("Stale connection removed");
            	serverConnections.RemoveAt(i);
            	--i;
        	}
    	}

    	// Accept incoming client connections.
    	NetworkConnection incomingConnection;
    	while ((incomingConnection = hostDriver.Accept()) != default(NetworkConnection))
    	{
        	// Adds the requesting Player to the serverConnections list.
        	// This also sends a Connect event back the requesting Player,
        	// as a means of acknowledging acceptance.
        	Debug.Log("Accepted an incoming connection.");
        	serverConnections.Add(incomingConnection);
    	}

    	// Process events from all connections.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	Assert.IsTrue(serverConnections[i].IsCreated);

        	// Resolve event queue.
        	NetworkEvent.Type eventType;
        	while ((eventType = hostDriver.PopEventForConnection(serverConnections[i], out var stream)) != NetworkEvent.Type.Empty)
        	{
            	switch (eventType)
            	{
                	// Handle Relay events.
                	case NetworkEvent.Type.Data:
                    	FixedString32Bytes msg = stream.ReadFixedString32();
                    	Debug.Log($"Server received msg: {msg}");
                    	hostLatestMessageReceived = msg.ToString();
                    	break;

                	// Handle Disconnect events.
                	case NetworkEvent.Type.Disconnect:
                    	Debug.Log("Server received disconnect from client");
                    	serverConnections[i] = default(NetworkConnection);
                    	break;
            	}
        	}
    	}
}

割り当てを作成する

以下のコードスニペットには、OnAllocate という関数が含まれています。これは、Relay SDK を使用して割り当てを作成する方法を示しています。

public async void OnAllocate()
{
    	Debug.Log("Host - Creating an allocation. Upon success, I have 10 seconds to BIND to the Relay server that I've allocated.");

    	// Determine region to use (user-selected or auto-select/QoS)
    	string region = GetRegionOrQosDefault();
    	Debug.Log($"The chosen region is: {region ?? autoSelectRegionName}");

    	// Set max connections. Can be up to 150, but note the more players connected, the higher the bandwidth/latency impact.
    	int maxConnections = 4;

    	// Important: After the allocation is created, you have ten seconds to BIND, else the allocation times out.
    	hostAllocation = await RelayService.Instance.CreateAllocationAsync(maxConnections, region);
    	Debug.Log($"Host Allocation ID: {hostAllocation.AllocationId}, region: {hostAllocation.Region}");

    	// Initialize NetworkConnection list for the server (Host).
    	// This list object manages the NetworkConnections which represent connected players.
    	serverConnections = new NativeList<NetworkConnection>(maxConnections, Allocator.Persistent);
}

警告: SetRelayServerData または SetRelayClientData を通じて提供された情報と、割り当てから取得した情報が一致しない場合は、Relay サーバーに接続できません。例えば、isSecure パラメーターが一致しない場合は、"Failed to connect to server (サーバーに接続できません)" というエラーメッセージが表示されます。

このエラーの発生を回避する最も簡単な方法の 1 つは、RelayServerData を割り当てから直接構築する方法です。ただし、Netcode for GameObjects (NGO) バージョン 1.1.0 以降を使用する必要があります。

Relay サーバーにバインドして接続をリッスンする

以下のコードスニペットには、OnBindHost という関数が含まれています。これは、Relay SDK を使用してホストプレイヤーを Relay サーバーに接続し、参加プレイヤーからの接続リクエストをリッスンする方法を示しています。

public void OnBindHost()
{
    	Debug.Log("Host - Binding to the Relay server using UTP.");

    	// Extract the Relay server data from the Allocation response.
    	var relayServerData = new RelayServerData(hostAllocation, "udp");

    	// Create NetworkSettings using the Relay server data.
    	var settings = new NetworkSettings();
    	settings.WithRelayParameters(ref relayServerData);

    	// Create the Host's NetworkDriver from the NetworkSettings.
    	hostDriver = NetworkDriver.Create(settings);

    	// Bind to the Relay server.
    	if (hostDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
    	{
        	Debug.LogError("Host client failed to bind");
    	}
    	else
    	{
        	if (hostDriver.Listen() != 0)
        	{
            	Debug.LogError("Host client failed to listen");
        	}
        	else
        	{
            	Debug.Log("Host client bound to Relay server");
        	}
    	}
}

参加コードをリクエストする

以下のコードスニペットには、OnJoinCode という関数が含まれています。これは、割り当てのリクエストが成功した後で参加コードをリクエストする方法を示しています。ホストプレイヤーはこの参加コードを参加プレイヤーと共有して、参加プレイヤーがホストプレイヤーのゲームセッションに参加できるようにします。

public async void OnJoinCode()
{
    	Debug.Log("Host - Getting a join code for my allocation. I would share that join code with the other players so they can join my session.");

    	try
    	{
        	joinCode = await RelayService.Instance.GetJoinCodeAsync(hostAllocation.AllocationId);
        	Debug.Log("Host - Got join code: " + joinCode);
    	}
    	catch (RelayServiceException ex)
    	{
        	Debug.LogError(ex.Message + "\n" + ex.StackTrace);
    	}
}

参加プレイヤー

ゲームクライアントが参加プレイヤーとして機能する場合、そのゲームクライアントは、割り当てへの参加、接続タイプの設定、ホストプレイヤーの Relay サーバーにバインドしてホストプレイヤーに接続リクエストを送信するための NetworkDriver のインスタンスの作成を実行できる必要があります。

ノート: DefaultDriverBuilder.RegisterClientDriver メソッドが NetworkDriver を作成するときのデフォルトの動作は、プレイヤーが Relay サーバーに参加する際に、可能であればソケット接続を使用しないようにすることです。ただし以下のシナリオでは、クライアントはサーバーに接続するためにソケットを使用します。

  • NetworkSimulator が有効になっている。
  • ClientServerBootstrap.RequestPlayTypeClient に設定されている。
  • ServerWorld が (何らかの理由で) 存在しない。

プレイヤーの更新ループ

ホストプレイヤーの場合と同様に、ホストプレイヤーのゲームセッションに参加する前には、着信したメッセージを処理する、参加プレイヤーの更新ループを設定する必要があります。

ノート: わかりやすくするために、サンプルコードではこのループを Update() フレームごとに呼び出しています。ただし、実際には、プレイヤーの更新ループを別のコルーチンで処理する方が理にかなっています。

void UpdatePlayer()
{
    	// Skip update logic if the Player isn't yet bound.
    	if (!playerDriver.IsCreated || !playerDriver.Bound)
    	{
        	return;
    	}

    	// This keeps the binding to the Relay server alive,
    	// preventing it from timing out due to inactivity.
    	playerDriver.ScheduleUpdate().Complete();

    	// Resolve event queue.
    	NetworkEvent.Type eventType;
    	while ((eventType = clientConnection.PopEvent(playerDriver, out var stream)) != NetworkEvent.Type.Empty)
    	{
        	switch (eventType)
        	{
            	// Handle Relay events.
            	case NetworkEvent.Type.Data:
                	FixedString32Bytes msg = stream.ReadFixedString32();
                	Debug.Log($"Player received msg: {msg}");
                	playerLatestMessageReceived = msg.ToString();
                	break;

            	// Handle Connect events.
            	case NetworkEvent.Type.Connect:
                	Debug.Log("Player connected to the Host");
                	break;

            	// Handle Disconnect events.
            	case NetworkEvent.Type.Disconnect:
                	Debug.Log("Player got disconnected from the Host");
                	clientConnection = default(NetworkConnection);
                	break;
        	}
    	}
}

割り当てに参加する

以下のコードスニペットには、OnJoin という関数が含まれています。これは、Relay SDK を使用して、参加コードを使用して割り当てに参加し、接続タイプを UDP に設定する方法を示しています。

public async void OnJoin()
{
    	// Input join code in the respective input field first.
    	if (String.IsNullOrEmpty(JoinCodeInput.text))
    	{
        	Debug.LogError("Please input a join code.");
        	return;
    	}

    	Debug.Log("Player - Joining host allocation using join code. Upon success, I have 10 seconds to BIND to the Relay server that I've allocated.");

    	try
    	{
        	playerAllocation = await RelayService.Instance.JoinAllocationAsync(JoinCodeInput.text);
        	Debug.Log("Player Allocation ID: " + playerAllocation.AllocationId);
    	}
    	catch (RelayServiceException ex)
    	{
        	Debug.LogError(ex.Message + "\n" + ex.StackTrace);
    	}
}

警告: SetRelayServerData または SetRelayClientData を通じて提供された情報と、割り当てから取得した情報が一致しない場合は、Relay サーバーに接続できません。例えば、isSecure パラメーターが一致しない場合は、"Failed to connect to server (サーバーに接続できません)" というエラーメッセージが表示されます。

このエラーの発生を回避する最も簡単な方法の 1 つは、RelayServerData を割り当てから直接構築する方法です。ただし、Netcode for GameObjects (NGO) バージョン 1.1.0 以降を使用する必要があります。

Relay サーバーにバインドしてホストプレイヤーに接続する

以下のコードスニペットには、OnBindPlayer という関数が含まれています。これは、Relay サーバーにバインドする方法を示しています。

ノート: これにより、プレイヤーが Relay サーバーにバインドされます。次のステップでは、プレイヤーをホストプレイヤーに接続して、メッセージの交換を開始できるようにします。

public void OnBindPlayer()
{
    	Debug.Log("Player - Binding to the Relay server using UTP.");

    	// Extract the Relay server data from the Join Allocation response.
    	var relayServerData = new RelayServerData(playerAllocation, "udp");
    	// Create NetworkSettings using the Relay server data.
    	var settings = new NetworkSettings();
    	settings.WithRelayParameters(ref relayServerData);

    	// Create the Player's NetworkDriver from the NetworkSettings object.
    	playerDriver = NetworkDriver.Create(settings);

    	// Bind to the Relay server.
    	if (playerDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
    	{
        	Debug.LogError("Player client failed to bind");
    	}
    	else
    	{
        	Debug.Log("Player client bound to Relay server");
    	}
}

Relay サーバーにバインドしてホストプレイヤーに接続する

以下のコードスニペットには、OnConnectPlayer という関数が含まれています。これは、バインドしてホストプレイヤーに接続リクエストを送信する方法を示しています。これは hostDriver.Accept() を呼び出すため、ホストプレイヤーの更新ループに着信接続として表示されます。

public void OnConnectPlayer()
{
    	Debug.Log("Player - Connecting to Host's client.");
    	// Sends a connection request to the Host Player.
    	clientConnection = playerDriver.Connect();
}

メッセージを送信する

これでホストプレイヤーとプレイヤーが接続されたので、プレイヤー同士がメッセージを送信して通信することができます。

ホストとしてメッセージを送信する

以下のコードスニペットには、OnHostSendMessage という関数が含まれています。これは、ホストプレイヤーが接続プレイヤーにメッセージを送信する方法を示しています。このサンプルでは、ホストはすべての接続プレイヤーにメッセージをブロードキャストします。

public void OnHostSendMessage()
{
    	if (serverConnections.Length == 0)
    	{
        	Debug.LogError("No players connected to send messages to.");
        	return;
    	}

    	// Get message from the input field, or default to the placeholder text.
    	var msg = !String.IsNullOrEmpty(HostMessageInput.text) ? HostMessageInput.text : HostMessageInput.placeholder.GetComponent<Text>().text;

    	// In this sample, we will simply broadcast a message to all connected clients.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	if (hostDriver.BeginSend(serverConnections[i], out var writer) == 0)
        	{
            	// Send the message. Aside from FixedString32, many different types can be used.
            	writer.WriteFixedString32(msg);
            	hostDriver.EndSend(writer);
        	}
    	}
}

プレイヤーとしてメッセージを送信する

以下のコードスニペットには、OnPlayerSendMessage という関数が含まれています。これは、プレイヤーがホストプレイヤーにメッセージを送信する方法を示しています。

public void OnPlayerSendMessage()
{
    	if (!clientConnection.IsCreated)
    	{
        	Debug.LogError("Player isn't connected. No Host client to send message to.");
        	return;
    	}

    	// Get message from the input field, or default to the placeholder text.
    	var msg = !String.IsNullOrEmpty(PlayerMessageInput.text) ? PlayerMessageInput.text : PlayerMessageInput.placeholder.GetComponent<Text>().text;
    	if (playerDriver.BeginSend(clientConnection, out var writer) == 0)
    	{
        	// Send the message. Aside from FixedString32, many different types can be used.
        	writer.WriteFixedString32(msg);
        	playerDriver.EndSend(writer);
    	}
}

切断する

プレイヤーは終了後に、ホストから切断できます。ホストプレイヤーが接続プレイヤーを強制的に切断することもできます。

ホストとしてプレイヤーを切断する

以下のコードスニペットには、OnDisconnectPlayers という関数が含まれています。これは、ホストプレイヤーが接続プレイヤーを強制的に切断する方法を示しています。このサンプルでは、ホストプレイヤーはすべての接続プレイヤーを切断します。

public void OnDisconnectPlayers()
{
    	if (serverConnections.Length == 0)
    	{
        	Debug.LogError("No players connected to disconnect.");
        	return;
    	}

    	// In this sample, we will simply disconnect all connected clients.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	// This sends a disconnect event to the destination client,
        	// letting them know they're disconnected from the Host.
        	hostDriver.Disconnect(serverConnections[i]);

        	// Here, we set the destination client's NetworkConnection to the default value.
        	// It will be recognized in the Host's Update loop as a stale connection, and be removed.
        	serverConnections[i] = default(NetworkConnection);
    	}
}

プレイヤーとして切断する

以下のコードスニペットには、OnDisconnect という関数が含まれています。これは、プレイヤーがホストプレイヤーから切断する方法を示しています。

public void OnDisconnect()
{
    	// This sends a disconnect event to the Host client,
    	// letting them know they're disconnecting.
    	playerDriver.Disconnect(clientConnection);

    	// We remove the reference to the current connection by overriding it.
    	clientConnection = default(NetworkConnection);
}