Closed6

Unity Transport を試す

Kenta WatashimaKenta Watashima

https://docs-multiplayer.unity3d.com/transport/current/about/index.html

The Unity Transport package (com.unity.transport) is a low-level networking library for multiplayer game development.

It’s the underlying protocol of both Netcode for GameObjects and Netcode for Entities, but you can also use it with a custom solution.

Unity Transport seamlessly supports all platforms the Unity Engine supports thanks to a connection-based abstraction layer (built-in network driver) provided over UDP and WebSockets.

You can set up both UDP and WebSockets with or without encryption. The following illustration shows encrypted connections with a closed padlock and unencrypted connections with an open padlock.

ブラウザベースの WebSocket は TCP だが、こっちは UDP らしい。ゲーム用途で UDP を採用したいケースは多そうなので、まあわかる。

Kenta WatashimaKenta Watashima

パッケージのインストール

  1. Unity Editor から、Window > Package Manager を選択
  2. Add (+) > Add package by nameを選択
  3. Name フィールドに com.unity.transport を入力
Kenta WatashimaKenta Watashima

サーバの実装

以下、ほぼ公式ドキュメントのコピペ。

NetworkDriver のセットアップ

public class ServerBehaiviour : MonoBehaviour
{
  private NetworkDriver _driver;
  private NativeList<NetworkConnection> _connections;

  void Start()
  {
    _driver = NetworkDriver.Create();
    var endpoint = NetworkEndPoint.AnyIpv4;
    endpoint.Port = 9000;
    if (_driver.Bind(endpoint) != 0)
      Debug.Log("Failed to bind to port 9000");
    else
      _driver.Listen();

    _connections = new NativeList<NetworkConnection>(16, Allocator.Persistent);
  }

ポート 9000 で listen を開始。NetworkEndPoint.AnyIpv4 はつまり 0.0.0.0 のことで、任意のアドレスを指す。
接続の状態はリストに保持しておく。

更新処理

  void Update()
  {
    _driver.ScheduleUpdate().Complete();

NetworkDriver の更新。

    for (int i = 0; i < _connections.Length; i++)
    {
      if (!_connections[i].IsCreated)
      {
        _connections.RemoveAtSwapBack(i);
        --i;
      }
    }

新規の接続を処理する前に、接続リストの中から、古くなったものを一掃しておく。

    NetworkConnection c;
    while ((c = _driver.Accept()) != default(NetworkConnection))
    {
      _connections.Add(c);
      Debug.Log("Accepted a connection");
    }

新規の接続があるかどうかをチェック。

    DataStreamReader stream;
    for (int i = 0; i < _connections.Length; i++)
    {
      NetworkEvent.Type cmd;
      while ((cmd = _driver.PopEventForConnection(_connections[i], out stream)) != NetworkEvent.Type.Empty)
      {

各接続に対して PopEventForConnection をコールして新規のイベントがあるかどうかを問い合わせる。

        if (cmd == NetworkEvent.Type.Data)
        {
          uint number = stream.ReadUInt();

          Debug.Log("Got " + number + " from the Client adding + 2 to it.");
          number += 2;

          _driver.BeginSend(NetworkPipeline.Null, _connections[i], out var writer);
          writer.WriteUInt(number);
          _driver.EndSend(writer);
        }

Data イベントのとき、uint として読み取った値に処理を加えてクライアントに送り返す。

        else if (cmd == NetworkEvent.Type.Disconnect)
        {
          Debug.Log("Client disconnected from server");
          _connections[i] = default(NetworkConnection);
        }
      }
    }
  }

Disconnect イベントのとき、その接続が次の Update のタイミングで破棄されるよう、初期化する。

後処理

  void OnDestroy()
  {
    if (_driver.IsCreated)
    {
      _driver.Dispose();
      _connections.Dispose();
    }
  }
}
Kenta WatashimaKenta Watashima

クライアントの実装

サーバへの接続

public class ClientBehaviour : MonoBehaviour
{
  private NetworkDriver _driver;
  private NetworkConnection _connection;
  private bool _isDone;

  void Start()
  {
    _driver = NetworkDriver.Create();
    _connection = default(NetworkConnection);

    var endpoint = NetworkEndPoint.LoopbackIpv4;
    endpoint.Port = 9000;
    _connection = _driver.Connect(endpoint);
  }

NetworkEndPoint.LoopbackIpv4 はつまり 127.0.0.1 のこと。

更新処理

  void Update()
  {
    _driver.ScheduleUpdate().Complete();

    if (!_connection.IsCreated)
    {
      if (!_isDone)
        Debug.Log("Something went wrong during connect");
      return;
    }

接続に成功したかどうかを確認。

    DataStreamReader stream;
    NetworkEvent.Type cmd;

    while ((cmd = _connection.PopEvent(_driver, out stream)) != NetworkEvent.Type.Empty)
    {
      if (cmd == NetworkEvent.Type.Connect)
      {
        Debug.Log("We are now connected to the server");

        uint value = 1;
        _driver.BeginSend(_connection, out var writer);
        writer.WriteUInt(value);
        _driver.EndSend(writer);
      }

接続が確立したタイミングで、サーバに値を送信。

      else if (cmd == NetworkEvent.Type.Data)
      {
        uint value = stream.ReadUInt();
        Debug.Log("Got the value = " + value + " back from the server");
        _isDone = true;
        _connection.Disconnect(_driver);
        _connection = default(NetworkConnection);
      }

サーバから値を受け取ったら切断する。

      else if (cmd == NetworkEvent.Type.Disconnect)
      {
        Debug.Log("Client got disconnected from server");
        _connection = default(NetworkConnection);
      }
    }
  }

後処理

  void OnDestroy()
  {
    _driver.Dispose();
  }
}
Kenta WatashimaKenta Watashima

パイプライン

https://docs-multiplayer.unity3d.com/transport/current/pipelines/

Pipelines are a core functionality of Unity Transport that allows selectively adding layers of functionality on top of the standard unreliable datagrams that UTP provides by default.

You can use pipelines to add sequencing, reliability, and fragmentation features.

Pipelines are a sequence of one or more stages. When you send a message through a pipeline, Unity Transport runs through the stages in order, piping the output of the first stage into the second stage. As a result, if the first stage adds a header to the packet, the second stage processes the entire packet, including the header added by the first stage. When you receive a message, it goes through the same chain of stages in reverse order.

信頼性の担保

UDP はその性質上、TCP と比較して信頼性に欠けるが、パイプラインを使うことで通信に信頼性を持たせることができる。

var myPipeline = _driver.CreatePipeline(typeof(ReliableSequencedPipelineStage));
_driver.BeginSend(myPipeline, connection, out var writer);

指定したパイプラインがクライアント側とサーバ側で一致していないとエラーが返る。
また、クライアント側は、Connect する前にパイプラインを初期化しておく必要がある。

このスクラップは5ヶ月前にクローズされました