メインコンテンツまでスキップ

Unity ピアツーピア

Last updated on January 17, 2025

概要

AccelByte Gaming Services (AGS) Unity 用ピアツーピア (P2P) を使用すると、プレイヤーは AccelByteNetworkTransportManager をメインレイヤーとして持つ AccelByte TURN サーバーを使用して、非公開ネットワーク全体で簡単に ピアツーピア接続を確立できます。Unity.Netcode.NetworkTransport インターフェイスを採用し、特定の初期化を行い、AccelByte サービスを使用して実行できます。初期化後、簡単に、Unity Netcode for GameObject に準拠して使用できます。

前提条件

このチュートリアルでは、以下を使用していることを前提としています。

  • AGS Unity SDK バージョン 15.15.0
  • AGS P2P Unity バージョン 0.2.5
  • Unity Netcode バージョン 1.0.0
備考

他のバージョンは、互換性がない、または 現在のドキュメントと実装する手順が異なる場合があります。

クイックリファレンス

リファレンス

using AccelByte.Api;
using AccelByte.Core;
using AccelByte.Models;
using Unity.Netcode;
using Unity.WebRTC;

WebRTC の初期化

WebRTC.Initialize(type: WebRTC.HardwareEncoderSupport() ? EncoderType.Hardware : EncoderType.Software, limitTextureSize: true, enableNativeLog: true, nativeLoggingSeverity: NativeLoggingSeverity.LS_ERROR);

AccelByte API クライアントの初期化

var apiClient = AccelByteSDK.GetClientRegistry().GetApi();

AccelByte SDK サービス (MultiRegistry) の初期化

user = apiClient.GetApi<User, UserApi>();
lobby = apiClient.GetApi<Lobby, LobbyApi>();
sessionBrowser = apiClient.GetApi<SessionBrowser, SessionBrowserApi>();

ネットワークトランスポートマネージャーの初期化

AccelByteNetworkTransportManager transportManager;
transportManager.Initialize(apiClient);

セッションの作成

// configure session setting
SessionBrowserGameSetting setting = new SessionBrowserGameSetting();
setting.mode = "game_mode";
setting.map_name = "default_mapname";
setting.num_bot = 0;
setting.max_player = 1;
setting.max_internal_player = 1;
setting.current_player = 1;
setting.current_internal_player = 1;
setting.allow_join_in_progress = true;
setting.password = "session_password";
setting.settings = new JObject();

// create session
transportManager.SetSessionBrowserCreationRequest(setting, "host_username", "game_version");
transportManager.StartServer();

セッションの検索

// configure sessions' query filter
SessionBrowserQuerySessionFilter sessionFilter = new SessionBrowserQuerySessionFilter
{
sessionType = SessionType.p2p,
gameMode = gameMode
};

// get all sessions with SessionBrowser API
sessionBrowser.GetGameSessions(sessionFilter, result =>
{
if (result.IsError || result.Value == null || result.Value.sessions == null || result.Value.sessions.Length == 0)
{
Debug.Log($"Get Game Sessions failed : {result.Error.Code}, Description : {result.Error.Message}");
}
else
{
Debug.Log("Get Game Sessions successful");
}
});

セッションへの参加

transportManager.SetTargetHostUserId(targetHostUserID);
transportManager.StartClient();

セッションからの離脱

transportManager.Shutdown();

データの送信

byte[] decodedMessage = Encoding.ASCII.GetBytes(messageData);
transportManager.Send(clientId, new ArraySegment<byte>(decodedMessage), NetworkDelivery.Reliable);

イベントのポーリング

while (transportManager != null && transportManager?.PollEvent(out clientId, out payload, out pollReceiveTime) != NetworkEvent.Nothing)
{
string content = Encoding.ASCII.GetString(payload.Array);
}

クイックスタートガイド

このチュートリアルでは、TURN サーバーのセットアップ方法と P2P サービスの実装方法を説明します。

プラグインの設定

  1. Package Manager に AccelByte Unity SDK と AccelByte Unity ネットワーキングが追加されていることを確認してください。

  2. AccelByte Unity SDK を設定します。困った時は Unity SDK ガイドに従ってください。

  3. TURN サーバーを設定するには、Unity Editor を開き、[Menu (メニュー)]タブから[AccelByte] → [Edit Settings (設定を編集)]を選択します。

    Unity-P2P-Getting-Started

  4. 設定で TURN 関連の値を入力します。

    Unity-P2P-Getting-Started

    AccelByteSDKConfig.json からこれらの値を設定することもできます。:

    "UseTurnManager": true

P2P の主な機能

このガイドでは、P2P 機能を使用するための認証サービスを既に実装していることを前提としています。

  1. CustomGameHandler.cs という新しいスクリプトを作成し、次の AccelByte ライブラリをスクリプトの上部に追加します。

    using AccelByte.Api;
    using AccelByte.Core;
    using AccelByte.Models;
    using Unity.Netcode;
    using Unity.WebRTC;
  2. 接続のために WebRTC ライブラリを初期化し、WebRTC の破棄関数 OnApplicationQuit() を呼び出します。

    void Start()
    {
    WebRTC.Initialize(type: WebRTC.HardwareEncoderSupport() ? EncoderType.Hardware : EncoderType.Software, limitTextureSize: true, enableNativeLog: true, nativeLoggingSeverity: NativeLoggingSeverity.LS_ERROR);
    }

    private void OnApplicationQuit()
    {
    WebRTC.Dispose();
    }
  3. AccelByte の SDK と Unity ネットワーキングを使用できるようにするには、API クライアントを作成し、SDK のユーザーロビーSessionBrowser API を初期化する必要があります。

    private ApiClient apiClient;
    private Lobby lobby;
    private SessionBrowser sessionBrowser;

    private void Start()
    {
    ...
    // configure API Client for player
    var apiClient = AccelByteSDK.GetClientRegistry().GetApi();
    var lobby = apiClient.GetLobby();
    var sessionBrowser = apiClient.GetSessionBrowser();
    }
  4. AccelByteNetworkTransportManager のローカル変数を準備し、AccelByte の API クライアントでトランスポートマネージャーを初期化する関数を作成します。

    private AccelByteNetworkTransportManager transportManager;

    private void InitializeNetworkManager()
    {
    if (transportManager == null)
    {
    transportManager = gameObject.AddComponent<AccelByteNetworkTransportManager>();
    }
    transportManager.Initialize(apiClient);
    }
    NOTE

    シーンのシングルトンに Unity Netcode NetworkManager がある場合は、コンポーネントの参照を NetworkTransport 選択に渡せます。 Unity-P2P-Getting-Started

  5. ロビーサービスを実装する関数を作成し、lobby.Connected 告知イベントを使用してロビーが接続されたときに InitializeNetworkManager() 関数を呼び出します。ログイン IAM 実装からログイン試行が成功したときに、この関数を呼び出します。

    public void ConnectLobby()
    {
    lobby.Connected += () => InitializeNetworkManager();
    if (!lobby.IsConnected)
    {
    lobby.Connect();
    }
    }
  6. P2P セッション機能に移動し、CustomGamesHandler.cs に新しい関数をいくつか作成します。

    • セッションの作成

      ホストとして、最初に SetSessionBrowserCreationRequest() 関数を使用してセッション設定を指定してから、StartHost() 関数を呼び出してセッションを作成し、サーバーを起動する必要があります。

      public void CreateSession()
      {
      SessionBrowserGameSetting setting = new SessionBrowserGameSetting();
      setting.mode = "gamemode_name";
      setting.map_name = "default";
      setting.max_player = 2;
      setting.current_player = 1;
      setting.allow_join_in_progress = true;

      // create a session with SessionBrowser API
      transportManager.SetSessionBrowserCreationRequest(setting, userSession.userName, gameVersion);
      transportManager.StartServer();
      }

      セッションを作成するために必要な設定のプロパティ:

      • mode:セッションに使用されるゲームモードの名前
      • map_name:セッションに使用されるマップの名前
      • max_player:セッションのアクティブなプレイヤーの最大数
      • current_player:セッションにおける現在のプレイヤーの合計数

      セッション設定におけるその他のオプション設定プロパティ:

      • num_bot:セッションにおける既存のボットの合計数
      • max_internal_player:セッションの内部/非アクティブプレイヤーの最大数
      • current_internal_player:セッションにおける現在の内部/非アクティブプレイヤーの合計数
      • password:セッションのパスワード
      • settings:任意のカスタムデータを保存するための空の JsonObject
      NOTE

      NetworkManager がある場合は、NetworkManager.Singleton.StartHost() を直接呼び出せます。 これでホストとサーバーが起動します。

    • セッションの検索

      クライアントとして利用可能なすべてのセッションを取得するには、SessionBrowserGetGameSessions() 関数を使用します。この関数を使用するには、GetGameSessions() 関数のパラメータとして必要なクエリフィルターを最初に作成します。

      public void FindSessions()
      {
      // set filter for the GetGameSessions query result
      SessionBrowserQuerySessionFilter sessionFilter = new SessionBrowserQuerySessionFilter();
      sessionFilter.sessionType = SessionType.p2p;

      // get all sessions with SessionBrowser API
      sessionBrowser.GetGameSessions(sessionFilter, result =>
      {
      if (result.IsError || result.Value.sessions == null)
      {
      Debug.Log($"Get Game Sessions failed");
      }
      else
      {
      // loop all available sessions
      foreach (SessionBrowserData sessionData in result.Value.sessions)
      {
      Debug.Log($"Session host id: {sessionData.user_id}");
      }
      }
      });
      }
    • セッションへの参加

      クライアントとしてセッションに参加するには、SessionBrowserData から取得できるホストユーザー ID を設定してから、StartClient() を呼び出して接続に参加する必要があります。

      public void JoinSession(string targetHostUserID, string sessionId, string sessionPassword = "")
      {
      // join p2p connection to session's host
      Debug.Log($"Connecting to: {targetHostUserID}");
      transportManager.SetTargetHostUserId(targetHostUserID);
      transportManager.StartClient();
      }
      NOTE

      NetworkManager がある場合は、直接 NetworkManager.Singleton.StartClient() 関数を呼び出せます。

    • セッションからの離脱

      ホストまたはクライアントとしてセッションを離脱するには、Shutdown() 関数を呼び出すだけです。この関数は、プレイヤーがホストまたはクライアントであるかどうかを自動的にチェックし、以下のように動作します。

      • プレイヤーがホストの場合は、接続を閉じてセッションを削除する。
      • プレイヤーがクライアントの場合、ホストから切断する。
      private void LeaveSession()
      {
      transportManager.Shutdown();
      }
      NOTE

      NetworkManager がある場合は、直接 NetworkManager.Singleton.Shutdown() 関数を呼び出せます。

  7. ホストとクライアント間の通信を確立するには、Send() 関数を使用してデータを送信します。この関数には、送信するターゲットクライアント IDメッセージデータ (文字列) が必要です。

    public void SendData(ulong clientId, string messageData)
    {
    byte[] decodedMessage = Encoding.ASCII.GetBytes(messageData);
    transportManager.Send(clientId, new ArraySegment<byte>(decodedMessage), NetworkDelivery.Reliable);
    }
    IMPORTANT

    ArraySegment<byte> の長さ制限により、短いメッセージのみ送受信できます。

    より複雑なデータを送信する場合は、代わりに NetcodeCustom Messaging を使用することをお勧めします。

  8. 他のクライアントがリアルタイムで送信したデータを受信するには、PollEvent() 関数を Update() 関数に追加します。

    void Update()
    {
    float pollReceiveTime = 0.0f;
    ArraySegment<byte> payload = new ArraySegment<byte>();
    ulong clientId = 0;

    while (transportManager != null && transportManager?.PollEvent(out clientId, out payload, out pollReceiveTime) != NetworkEvent.Nothing)
    {
    string content = Encoding.ASCII.GetString(payload.Array);
    Debug.Log($"[ReadyMatch] Received Data! {clientId} => {content}");
    }
    }

    Unity の HandleRawTransportPoll() 関数と同様に、NetworkEvent の各タイプを処理し、それに応じてペイロードを処理する必要があります。

  9. ネットワークトランスポートの接続更新に関する告知を受け取りたい場合は、TransportEventDelegate を定義し、次の NetworkEvent を呼び出す OnTransportEvent にサブスクライブします。 Data:クライアントがデータを受信したとき Connect:クライアントがサーバーに接続したとき Disconnect:クライアントがサーバーから切断されたとき Nothing:新しいイベントがなく、何もトリガーされず、何も受信していない

    **transportManager.Initialize()** を呼び出した後、いつでもこのデリゲートを定義できます。

    ```cpp
    private void InitializeNetworkManager()
    {
    ...

    // Initialize TransportEventDelegate
    NetworkTransport.TransportEventDelegate action = (NetworkEvent eventType, ulong clientId, ArraySegment<byte> payload, float receiveTime) =>
    {
    switch (eventType)
    {
    case NetworkEvent.Data:
    break;
    case NetworkEvent.Connect:
    Debug.Log($"Connection established with clientid: {clientId}");
    break;
    case NetworkEvent.Disconnect:
    Debug.Log($"Disconnected from clientid: {clientId}");
    break;
    case NetworkEvent.Nothing:
    break;
    };
    };
    transportManager.OnTransportEvent += action;
    }
    ```

    :::warning IMPORTANT
    `TransportEventDelegate` から受信した `clientId` は `NetworkManager` の `clientId` と異なるので、注意してください。

    `TransportEventDelegate` の `clientId` は `AccelByteUnityICE` `PeerID` に由来しますが、`NetworkManager` `clientId` はデフォルトで `NetworkManager` から生成されます。
    :::

    10.現在のセッション ID を取得するには、ホスト側から次の関数を使用します。

    ```cpp
    transportManager.GetHostedSessionID();
    ```
NOTE

現在のセッション IDセッションの作成プロセスが終了した後に追加されるため、場合によってはセッション ID を取得するために 1 フレーム待機する必要があります。

11.関数をテストするには、ロビーに接続した後に P2P 関数を呼び出します。必ずログイン試行が成功した後に ConnectLobby() 関数を呼び出しておいてください。

```cpp
public void ConnectLobby()
{
...
// Call the P2P Session Functions here accordingly
CreateSession("session_name", "");
}
```

Play (プレイ) をクリックすると、関数が実行されていることがわかります。完了したら、このデバッグチェックをスクリプトから削除してください。

お疲れさまでした。P2P セッション機能の実装方法を理解しました。

ウィジェットとコード実装の手順例に進んでください。

ステップバイステップガイド

UI 実装

  1. 空のオブジェクト (AccelByteHandler など) を作成し、AccelByte サービスのスクリプトハンドラのすべてのスクリプトコンポーネントを保持します。

  2. カスタムゲームページの場合は、新しいパネルまたはシーンを作成します。

    カスタムゲームページには表示するものがたくさんあるため、ページを 2 つのセクションに分割します。

    • 左パネル

      • サーバーリストスクロールビュー、利用可能なすべてのセッションを一覧表示

      Unity-P2P-Getting-Started

    • 右パネル

      • サーバー名入力フィールド
      • サーバーパスワード入力フィールド
      • サーバーを作成ボタン
      • リストを更新ボタン、サーバーリストを更新

      Unity-P2P-Getting-Started

  3. また、次のオブジェクトを持つパネルを作成して、セッションパスワードを入力するためのポップアップを準備します。

    • 参加パスワード入力フィールド

    • 確認ボタン

      Unity-P2P-Getting-Started

  4. 次のオブジェクトを持つ新しいパネルプレハブを作成して、セッション情報の表示を処理するためのサーバー情報パネルのプレハブを準備します。

    • サーバー名テキスト
    • ルームのアクセシビリティテキスト
    • ゲームモードテキスト
    • サーバー容量テキスト
    • 参加ボタン、選択したサーバーに参加

    Unity-P2P-Getting-Started

  5. セッションルームページの場合は、新しいパネルを作成し、次のオブジェクトが含まれるようにします。

    • サーバー名テキスト

    • 2 チームリストパネル、チーム A またはチーム B の各プレイヤー情報を保存

    • 準備完了ボタン、プレイヤーの準備完了同意を確認

    • 離脱ボタン、セッションを離脱

      Unity-P2P-Getting-Started

  6. プレイヤー準備完了パネルのプレハブを用意して、プレイヤー情報と準備完了同意の表示を処理します。次のオブジェクトを持つパネルプレハブを作成します。

    • 表示名テキスト

    • 準備完了同意トグル

      Unity-P2P-Getting-Started

コードの実装

このセクションに従って、チュートリアルプロジェクトに基づいてコードを実装する方法を確認できます。 この例では、Network ManagerAccelByteNetworkTransportManager を共に使用します。

IMPORTANT

このセクションを続行するには、ログインおよびロビーサービスを実装する必要があります。 認証の実装については説明しませんが、

こちらからアクセスできるサンプルプロジェクト内で実装する方法を確認できます。 :::

セッションの作成フォーム

  1. NetworkTransport として AccelByteNetworkTransportManager と一緒に、NetworkManager スクリプトを含む NetworkManager という名前のゲームオブジェクトがコンポーネントにあることを確認してください。

  2. CustomGamesHandler.cs を開き、次のライブラリをスクリプトの上部に追加します。

    using UnityEngine.UI;
    using Newtonsoft.Json.Linq;
    using System.Collections.Generic;
  3. CustomGamesHandler クラスにセッションの作成の UI 参照を追加します。NetworkManager を使用しているので、transportManager 参照を追加するとアクセスしやすくなります。

    public GameObject CustomGamesWindow;

    [Header("Host a Server")]
    [SerializeField]
    private GameObject sessionPasswordPanel;
    [SerializeField]
    private Toggle publicToggle;
    [SerializeField]
    private Toggle privateToggle;
    [SerializeField]
    private InputField sessionNameInputField;
    [SerializeField]
    private InputField sessionPasswordInputField;
    [SerializeField]
    private Button createServerButton;
  4. 保存してエディタに戻ります。AccelByteHandler GameObject で、CustomGamesHandler スクリプトをコンポーネントとして追加し、対応するオブジェクトを公開変数にドラッグします。

    Unity-P2P-Getting-Started

  5. CustomGamesHandler.cs に戻り、セッションと UI を作成するために必要なデータを保存する次の変数を追加します。

    public PublicUserData userSession;

    private const string gameVersion = "1.0.0";
    private const string gameMode = "1vs1";
    private string roomAccessibility = "public";

    private static bool isInitialized = false;
  6. ユーザー API を初期化して、後で CustomGamesHandler.cs でユーザーデータを取得します。

    private User user;

    private void Start()
    {
    ...
    user = apiClient.GetApi<User, UserApi>();
    }
  7. CustomGamesHandler.cs 内のすべての UI 状態とリスナーを設定する関数を作成します。

    public void Setup()
    {
    CustomGamesWindow.SetActive(true);

    if (!isInitialized)
    {
    isInitialized = true;

    // setup HostServerPanel's UIs
    publicToggle.onValueChanged.AddListener((bool on) =>
    {
    sessionPasswordPanel.SetActive(false);
    roomAccessibility = "public";
    });
    privateToggle.onValueChanged.AddListener((bool on) =>
    {
    sessionPasswordPanel.SetActive(true);
    roomAccessibility = "private";
    });
    createServerButton.onClick.AddListener(() =>
    {
    CreateSession(sessionNameInputField.text, sessionPasswordInputField.text);

    // reset HostServerPanel's UIs
    sessionNameInputField.text = "";
    sessionPasswordInputField.text = "";
    publicToggle.isOn = true;
    });
    }
  8. 現在のユーザーデータも必要なので、Setup() 関数内に次のコードを追加します。

    public void Setup()
    {
    ...
    // get current user data
    user.GetUserByUserId(user.Session.UserId, userResult =>
    {
    if (!userResult.IsError)
    {
    userSession = userResult.Value;

    Debug.Log($"current userid: {userSession.userId}");
    }
    });
    }
  9. ロビーに接続する前に Setup() 関数を呼び出します。

    ```cpp
    public void ConnectLobby()
    {
    Setup();
    ...
    }
    ```

    10.今度は、CreateSession() 関数を変更して、プレイヤーが UI から設定したセッション名セッションパスワードルームアクセシビリティを保存しましょう。

    ```cpp
    public void CreateSession(string sessionName, string sessionPassword)
    {
    ...
    setting.mode = gameMode;
    ...

    // set password if the room accessibility is private
    if (roomAccessibility == "private")
    {
    setting.password = sessionPassword;
    }

    // store session name and room accessibility data to settings JSON Object
    JObject settingJsonObject = new JObject();
    settingJsonObject.Add("session_name", sessionName);
    settingJsonObject.Add("room_accessibility", roomAccessibility);
    setting.settings = settingJsonObject;

    // create a session with SessionBrowser API
    transportManager.SetSessionBrowserCreationRequest(setting, userSession.userName, gameVersion);
    ...
    }
    ```

利用可能なすべてのセッションの一覧表示

  1. サーバー情報パネルのプレハブを更新するには、ServerInfoPanel.cs というスクリプトを作成し、スクリプトの上部に次のライブラリを追加します。

    using AccelByte.Models;
    using UnityEngine;
    using UnityEngine.UI;
  2. ServerInfoPanel.cs にセッションデータを保存するために、サーバー情報パネルの UI 参照といくつかのローカル変数を追加します。

    [SerializeField]
    private Text sessionNameText;
    [SerializeField]
    private Text roomAccessibilityText;
    [SerializeField]
    private Text gameModeText;
    [SerializeField]
    private Text serverCapacityText;

    private string hostUserId;
    private string roomAccessibility;
  3. ServerInfoPanel.cs で新しい関数を作成して、セッションデータを UI に更新します。

    public void Create(SessionBrowserData sessionData)
    {
    Debug.Log("[ServerInfo] Updating Server Info data...");

    hostUserId = sessionData.user_id;
    roomAccessibility = sessionData.game_session_setting.settings.GetValue("room_accessibility").ToString();

    // update data to Text UIs
    sessionNameText.text = sessionData.game_session_setting.settings.GetValue("session_name").ToString();
    gameModeText.text = sessionData.game_session_setting.mode;
    roomAccessibilityText.text = roomAccessibility;

    int currentPlayers = sessionData.players.Length;
    int maxPlayers = sessionData.game_session_setting.max_player;
    string capacity = currentPlayers.ToString() + "/" + maxPlayers.ToString();
    serverCapacityText.text = capacity;
    }
  4. 保存してエディタを開き、ServerInfoPanel スクリプトをプレハブ GameObject のコンポーネントとして追加します。これらのオブジェクトを参照として公開変数へドラッグします。

    Unity-P2P-Getting-Started

  5. CustomGamesHandler.cs に戻り、親トランスフォームのすべての子を破棄して UI をリセットする関数を作成します。

    private void LoopThroughTransformAndDestroy(Transform parent, Transform doNotRemove = null)
    {
    //Loop through all the children and add them to a List to then be deleted
    List<GameObject> toBeDeleted = new List<GameObject>();
    foreach (Transform t in parent)
    {
    //except the Do Not Remove transform if there is one
    if (t != doNotRemove)
    {
    toBeDeleted.Add(t.gameObject);
    }
    }
    //Loop through list and Delete all Children
    for (int i = 0; i < toBeDeleted.Count; i++)
    {
    Destroy(toBeDeleted[i]);
    }
    }
  6. CustomGamesHandler クラスに セッションの一覧表示の UI 参照をすべて追加します。

    [SerializeField]
    private Button refreshListButton;
    [SerializeField]
    private Button backToLobbyButton;

    [Header("Server List")]
    [SerializeField]
    private Transform serverListContent;
    [SerializeField]
    private Transform serverListPlaceholderText;
    [SerializeField]
    private GameObject serverInfoPrefab;
  7. 保存してエディタに戻ります。AccelByteHandler GameObject で、以下のオブジェクトを公開変数にドラッグします。

    Unity-P2P-Getting-Started

  8. CustomGamesHandler.cs に戻ります。この Setup() 関数で、セッションのリスト表示の UI 状態とリスナーを設定します。

    public void Setup()
    {
    ...
    if (!isInitialized)
    {
    ...
    // setup ButtonPanel's UIs
    refreshListButton.onClick.AddListener(() => FindSessions());
    backToLobbyButton.onClick.AddListener(() =>
    {
    CustomGamesWindow.SetActive(false);
    GetComponent<AuthenticationHandler>().OnLogoutClicked();
    });
    }

    // refresh the server list on opening the Custom Games page
    refreshListButton.onClick.Invoke();
    ...
    }
  9. セッションリストデータを表示するためにプレハブをインスタンス化するように FindSessions() 関数を変更します。

    public void FindSessions()
    {
    ...
    if (result.IsError || result.Value.sessions == null)
    {
    ...
    LoopThroughTransformAndDestroy(serverListContent, serverListPlaceholderText);
    serverListPlaceholderText.gameObject.SetActive(true);
    }
    else
    {
    ...
    serverListPlaceholderText.gameObject.SetActive(false);
    LoopThroughTransformAndDestroy(serverListContent, serverListPlaceholderText);

    foreach (SessionBrowserData sessionData in result.Value.sessions)
    {
    Debug.Log($"[CustomGames] session name: {sessionData.game_session_setting.settings.GetValue("session_name")} || {sessionData.session_id}");

    ServerInfoPanel infoPanel = Instantiate(serverInfoPrefab, serverListContent).GetComponent<ServerInfoPanel>();
    infoPanel.Create(sessionData);
    }
    }
    ...
    }

セッションへの参加

  1. CustomGamesHandler.cs にインスタンスを作成し、プレハブが後からこのクラスにアクセスできるようにします。

    /// Private Instance
    static CustomGamesHandler _instance;
    /// The Instance Getter
    public static CustomGamesHandler Instance => _instance;

    private void Awake()
    {
    //Check if another Instance is already created, and if so delete this one, otherwise destroy the object
    if (_instance != null && _instance != this)
    {
    Destroy(this);
    return;
    }
    else
    {
    _instance = this;
    }
    }
  2. CustomGamesHandler.cs' にセッションへの参加の UI 参照を追加します。

    [Header("Join Session")]
    [SerializeField]
    private GameObject joinPasswordPopUpPanel;
    [SerializeField]
    private InputField joinPasswordInputField;
    [SerializeField]
    private Button confirmPasswordButton;
    [SerializeField]
    private Button exitButton;
  3. ファイルを保存し、エディタに移動します。AccelByteHandler GameObject の Custom Games Handler コンポーネント下で、対応するオブジェクトを参照として変数にドラッグします。

    Unity-P2P-Getting-Started

  4. CustomGamesHandler.cs で、Setup() 関数内の exitButton リスナーを設定します。

    public void Setup()
    {
    ...
    if (!isInitialized)
    {
    ...
    // setup JoinPassword's UIs
    exitButton.onClick.AddListener(() =>
    {
    joinPasswordPopUpPanel.SetActive(false);
    });
    ...
    }
  5. 新しい関数を作成して、セッションが非公開公開かを確認します。非公開の場合は、参加パスワードポップアップパネルを表示して、セッションのパスワードを確認します。

    public void CheckSessionPassword(string targetHostUserId, string sessionId, string roomAccessibility)
    {
    if (roomAccessibility == "private")
    {
    // reset JoinPasswordPanel
    joinPasswordInputField.text = "";
    confirmPasswordButton.onClick.RemoveAllListeners();

    joinPasswordPopUpPanel.SetActive(true);

    Debug.Log("[CustomGames] Room session is private");
    confirmPasswordButton.onClick.AddListener(() =>
    {
    JoinSession(targetHostUserId, sessionId, joinPasswordInputField.text);
    joinPasswordPopUpPanel.SetActive(false);
    });
    }
    else
    {
    Debug.Log("[CustomGames] Room session is public");
    JoinSession(targetHostUserId, sessionId);
    }
    }
  6. 次に、選択したセッションに参加できるようにサーバー情報パネルを準備しましょう。ServerInfoPanel.cs を開き、参加ボタンの UI 参照を追加します。

    [SerializeField]
    private Button joinButton;
  7. ServerInfoPanel.csCreate() 関数を変更して、参加ボタンのリスナーを設定します。また、currentPlayers および maxPlayers 変数値に基づいて、セッションが満員の場合は、ボタンを無効にします。

    public void Create(SessionBrowserData sessionData)
    {
    ...
    // add listener to button
    joinButton.onClick.AddListener(() =>
    {
    CustomGamesHandler.Instance.CheckSessionPassword(hostUserId, sessionData.session_id, roomAccessibility);
    });

    // disable button if room session is full
    if (currentPlayers == maxPlayers)
    {
    joinButton.interactable = false;
    }
    else
    {
    joinButton.interactable = true;
    }
    }

現在のセッション ID の取得

  1. SessionRoomHandler.cs という新しいスクリプトを作成し、次のライブラリをスクリプト内に追加します。

    using AccelByte.Api;
    using AccelByte.Core;
    using System.Collections.Generic;
    using Unity.Netcode;
    using UnityEngine.UI;
  2. SessionRoomHandler.cs に次の UI 参照を追加します。

    public GameObject SessionRoomWindow;

    [Header("Network Related")]
    [SerializeField]
    private AccelByteNetworkTransportManager transportManager;
  3. 保存してエディタに戻り、AccelByteHandler GameObject に SessionRoomHandler スクリプトをコンポーネントとして追加します。また、これらのオブジェクトを対応する変数にドラッグします。

    Unity-P2P-Getting-Started

  4. SessionRoomHandler.cs で、Start() 関数内の SDK の API クライアントとセッションブラウザを定義します。

    private ApiClient apiClient;
    private SessionBrowser sessionBrowser;

    private void Start()
    {
    // configure API Client for player
    var apiClient = AccelByteSDK.GetClientRegistry().GetApi();
    var sessionBrowser = apiClient.GetSessionBrowser();
    }
  5. これらのローカル変数を追加して、SessionRoomHandler.csセッション ID の関連データを保存します。

    private string currentSessionId;
    private static bool isEmptySessionId = false;
  6. SessionRoomHandler.cs に新しい関数を作成して、クライアント側からセッション ID を設定します。

    public void SetSessionId(string sessionId)
    {
    currentSessionId = sessionId;
    }
  7. CustomGamesHandler.cs を開き、SetSessionId() 関数を呼び出して、セッションに参加した後の JoinSession() 結果データからセッション ID を設定します。

    public void JoinSession(string targetHostUserID, string sessionId, string sessionPassword = "")
    {
    ...
    if (!result.IsError)
    {
    ...
    NetworkManager.Singleton.StartClient();

    // set the current session id from client side
    GetComponent<SessionRoomHandler>().SetSessionId(sessionId);
    ...
    }
  8. SessionRoomHandler.cs に戻り、currentSessionId がまだ空であるか、有効なセッション ID が既にあるかを確認するために新しい関数を準備し、存在する場合はセッションデータを取得します。今のところ、GetGameSession() をデフォルトに設定し、これを使用して後で UI を更新します。

    private void GetGameSession()
    {
    // Avoid sending empty session id
    if (string.IsNullOrEmpty(currentSessionId))
    {
    isEmptySessionId = true;
    return;
    }

    sessionBrowser.GetGameSession(currentSessionId, result =>
    {
    if (!result.IsError)
    {
    Debug.Log($"[SessionRoom] Get game session success ||");
    }
    else
    {
    Debug.Log($"[SessionRoom] Get game session failed, Error: {result.Error.Code}, Description: {result.Error.Message}");
    }
    });
    }
  9. AccelByteNetworkTransportManagerGetHostedSessionID() 関数は、セッションの作成プロセスがまだ完了していない場合にも空を返す可能性があります。そのため、SessionRoomHandler.csFixedUpdate() 関数内で GetHostedSessionID() が空を返さなくなるまで、何らかのチェックを行ってセッション ID を設定します。

    ```cpp
    private void FixedUpdate()
    {
    if (isEmptySessionId && currentSessionId != transportManager.GetHostedSessionID())
    {
    isEmptySessionId = false;

    // set current session id
    currentSessionId = transportManager.GetHostedSessionID();
    GetGameSession();
    }
    }
    ```

    10.SessionRoomPanel を設定する関数を作成します。現時点では、セッション ID を取得することに集中し、残りの UI は後で設定します。

    ```cpp
    public void Setup()
    {
    if (NetworkManager.Singleton.IsHost)
    {
    currentSessionId = transportManager.GetHostedSessionID();
    }
    GetGameSession();
    }
    ```

セッションデータの表示

  1. プレイヤー準備完了パネルプレハブを更新するには、PlayerReadyPanel.cs という新しいスクリプトを作成し、スクリプトの上部にこのライブラリを追加します。

    using UnityEngine.UI;
  2. PlayerReadyPanel クラスにプレイヤー準備完了パネルの UI 参照をすべて追加します。

    [SerializeField]
    private HorizontalLayoutGroup playerReadyHorizLayoutGroup;

    [SerializeField]
    private Text displayNameText;
    [SerializeField]
    private Toggle readyConsentToggle;
  3. PlayerReadyPanel.csPlayerInfo データに基づいて UI を更新する関数を作成します。

    public void UpdatePlayerInfo(PlayerInfo playerInfo)
    {
    displayNameText.text = playerInfo.displayName.ToString();
    readyConsentToggle.isOn = playerInfo.isReady;
    }
  4. 異なる配置の 2 チームがあるため、PlayerReadyPanel.cs で相手チームの配置を逆にする関数を作成します。

    public void SetReverseArrangementPanel(bool isReverse)
    {
    playerReadyHorizLayoutGroup.reverseArrangement = isReverse;
    }
  5. スクリプトを保存してエディタに移動します。PlayerReadyPanel スクリプトをプレハブ GameObject に追加し、これらのオブジェクトを対応する公開変数にドラッグします。

    Unity-P2P-Getting-Started

  6. SessionRoomHandler.cs に戻り、SessionRoomHandler クラスが MonoBehaviour の代わりに NetworkBehaviour を継承していることを確認してください。これは、サーバーとクライアント間でデータを共有するために必要になります。

    public class SessionRoomHandler : NetworkBehaviour
    ...
  7. SessionRoomHandler.csセッションルームの UI 参照の残りを追加します。

    [SerializeField]
    private Text serverNameText;
    [SerializeField]
    private GameObject playerReadyPrefab;

    [SerializeField]
    private Transform teamAPanel;
    [SerializeField]
    private Transform teamBPanel;

    [SerializeField]
    private Button startButton;
    [SerializeField]
    private Button readyButton;
    [SerializeField]
    private Button leaveButton;
  8. スクリプトを保存し、エディタの AccelByteHandler GameObject の Session Room Handler コンポーネント下で、これらの対応するオブジェクトを公開変数にドラッグします。

    Unity-P2P-Getting-Started

  9. SessionRoomHandler.cs で、Setup() 関数を変更して UI 状態を設定します。

    ```cpp
    public void Setup()
    {
    SessionRoomWindow.SetActive(true);
    if (NetworkManager.Singleton.IsHost)
    {
    startButton.gameObject.SetActive(true);
    exitGameplayButton.gameObject.SetActive(true);
    }
    else
    {
    startButton.gameObject.SetActive(false);
    exitGameplayButton.gameObject.SetActive(false);
    }

    readyButton.interactable = true;

    // reset and display the session name
    serverNameText.text = "";
    ...
    }
    ```

    10.PlayerInfo という構造体を用意して、プレイヤーのデータを保存します。INetworkSerializableIEquatable<> インターフェイスクラスを継承していることを確認し、この構造体と NetworkList を使用してリストデータを共有できるようにします。

    ```cpp
    [Serializable]
    public struct PlayerInfo : INetworkSerializable, IEquatable<PlayerInfo>
    {
    public ulong clientId;
    public FixedString64Bytes userId;
    public FixedString64Bytes displayName;
    public bool isReady;

    public PlayerInfo(ulong playerClientId, FixedString64Bytes playerUserId, FixedString64Bytes playerDisplayName, bool playerIsReady)
    {
    clientId = playerClientId;
    userId = playerUserId;
    displayName = playerDisplayName;
    isReady = playerIsReady;
    }

    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
    serializer.SerializeValue(ref clientId);
    serializer.SerializeValue(ref userId);
    serializer.SerializeValue(ref displayName);
    serializer.SerializeValue(ref isReady);
    }

    public bool Equals(PlayerInfo other)
    {
    return clientId == other.clientId && userId == other.userId && displayName == other.displayName && isReady == other.isReady;
    }
    }
    ```

    11.SessionRoomHandler.cs で、NetworkList を追加して、サーバーとクライアント間で共有されるセッションのプレイヤーデータを保存し、Start() 内のリストを初期化します。

    ```cpp
    private NetworkList<PlayerInfo> playerInfoList;

    private void Start()
    {
    ...
    playerInfoList = new NetworkList<PlayerInfo>();
    }
    ```

    12.playerInfoList のデータはサーバー側から追加されるため、クライアントは、接続されたときにプレイヤーのデータをサーバーに送信する必要があります。これを行うには、CustomGamesHandler.cs でサーバーに登録リクエストメッセージを送信するコールバック関数を作成し、接続されているクライアントでビューを SessionRoomPanel に変更します。

    ```cpp
    private void OnClientConnected(ulong clientId)
    {
    // if the local client is the client that just connected
    if (clientId == NetworkManager.Singleton.LocalClient.ClientId)
    {
    if (!NetworkManager.Singleton.IsHost)
    {
    // send data with MessageName "Register" using Custom Messaging
    string messageJson = userSession.userId + ":" + userSession.displayName;
    FastBufferWriter writer = new FastBufferWriter(1100, Unity.Collections.Allocator.Temp);
    writer.WriteValueSafe(messageJson);
    NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage("Register", NetworkManager.ServerClientId, writer);
    }

    // move to SessionRoomPanel
    CustomGamesWindow.SetActive(false);
    GetComponent<SessionRoomHandler>().Setup();
    }
    }
    ```

    13.NetworkManagerOnClientConnectedCallback を定義し、イベントが CustomGamesHandler.csInitializeNetworkManager() 関数内でトリガーされたときに OnClientConnected() を呼び出します。

    ```cpp
    private void InitializeNetworkManager()
    {
    ...
    // Initialize NetworkManager Callback
    NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
    }
    ```

    14.SessionRoomHandler.cs“Register” MessageName を登録し、クライアントのメッセージを受信できるようにレシーバーハンドラを定義します。サーバーがいつでもメッセージを受信できるようにするには、NetworkBehaviourOnNetworkSpawn() 上書き関数内に登録プロセスを追加します。

    ```cpp
    public override void OnNetworkSpawn()
    {
    // if received a message with MessageName = "Register", register player to session
    NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("Register", (senderId, messagePayload) =>
    {
    messagePayload.ReadValueSafe(out string receivedMessageContent);

    // Get player data from string, format "userid:displayname"
    if (receivedMessageContent.Split(':').Length > 1)
    {
    string senderUserId = receivedMessageContent.Split(':')[0];
    string senderDisplayName = receivedMessageContent.Split(':')[1];

    // Add client's player data to list, and register to session
    playerInfoList.Add(new PlayerInfo(senderId, senderUserId, senderDisplayName, false));
    RegisterPlayerToSession(currentSessionId, senderUserId);
    }
    });
    }
    ```

    15.サーバーはホストクライアントでもあるため、SessionRoomHandler.cs で起動したサーバーのプレイヤーリストにホストのプレイヤーデータを追加するコールバック関数を作成します。

    ```cpp
    public void OnServerStarted()
    {
    playerInfoList.Add(new PlayerInfo
    (
    NetworkManager.ServerClientId,
    GetComponent<CustomGamesHandler>().userSession.userId,
    GetComponent<CustomGamesHandler>().userSession.displayName,
    false
    ));
    }
    ```

    16.PlayerReadyPanel プレハブをインスタンス化して、すべてのプレイヤーデータを表示する別のコールバック関数を作成します。この関数は、PlayerInfoList 値が更新されると呼び出されます。

    ```cpp
    private const int maxPlayer = 2;

    public void OnPlayerInfoListChanged(NetworkListEvent<PlayerInfo> playerInfoEvent)
    {
    LoopThroughTransformAndDestroy(teamAPanel);
    LoopThroughTransformAndDestroy(teamBPanel);

    // current display list only for 1vs1 mode
    for (int i = 0; i < maxPlayer; i++)
    {
    if (playerInfoList.Count > i)
    {
    PlayerReadyPanel playerPanel = null;
    if (i == 0)
    {
    playerPanel = Instantiate(playerReadyPrefab, teamAPanel).GetComponent<PlayerReadyPanel>();
    playerPanel.SetReverseArrangementPanel(false);
    }
    else
    {
    playerPanel = Instantiate(playerReadyPrefab, teamBPanel).GetComponent<PlayerReadyPanel>();
    playerPanel.SetReverseArrangementPanel(true);
    }
    playerPanel.UpdatePlayerInfo(playerInfoList[i]);
    }
    }
    }
    ```

    17.次に、OnNetworkSpawn() 上書き関数内で PlayerInfoListOnListChanged および NetworkManagerOnServerStarted コールバックイベントにサブスクライブするために作成した、すべてのコールバック関数を定義します。

    ```cpp
    public override void OnNetworkSpawn()
    {
    ...

    if (NetworkManager.Singleton.IsClient)
    {
    playerInfoList.OnListChanged += OnPlayerInfoListChanged;
    }
    if (NetworkManager.Singleton.IsServer)
    {
    NetworkManager.Singleton.OnServerStarted += OnServerStarted;
    }
    }
    ```

準備完了同意

  1. PlayerInfo 構造体には既に使用できる isReady ブールプロパティがあるので、必要なのは playerInfoList からプレイヤーの isReady 値を更新することだけです。 SessionRoomHandler.cs で、新しい ServerRPC 関数を作成します。この関数は、サーバーがクライアントのプレイヤー準備完了同意を更新するようリクエストされた場合に呼び出されます。

    [ServerRpc(RequireOwnership = false)]
    private void ToggleReadyServerRpc(ServerRpcParams serverRpcParams = default)
    {
    for (int i = 0; i < playerInfoList.Count; i++)
    {
    if (playerInfoList[i].clientId == serverRpcParams.Receive.SenderClientId)
    {
    playerInfoList[i] = new PlayerInfo(
    playerInfoList[i].clientId,
    playerInfoList[i].userId,
    playerInfoList[i].displayName,
    !playerInfoList[i].isReady
    );
    }
    }
    }
    NOTE

    playerInfoList を更新すると、OnPlayerInfoListChanged コールバックイベントがトリガーされ、UI が単独で更新されます。

  2. SessionRoomHandler.csSetup() 関数を変更し、readyButton のリスナーを追加して、クライアントが準備完了の同意を確認したときに ToggleReadyServerRpc() を呼び出します。 また、ボタンリスナーが既に追加されているかどうかを確認するのに役立つ変数を追加します。

    private bool isInitialized = false;
    ...

    public void Setup()
    {
    ...

    // Setup UI listeners
    if (!isInitialized)
    {
    isInitialized = true;

    readyButton.onClick.AddListener(() =>
    {
    readyButton.interactable = false;
    ToggleReadyServerRpc();
    });
    }
    }

ゲームの開始

  1. (任意) SessionRoomHandler.cs で、ゲームプレイのダミー UI 参照を追加します。

    [Header("TEMPORARY (soon to be deleted)")]
    public GameObject GameplayWindow;
    public Button exitGameplayButton;

    また、エディタの AccelByteHandler GameObject で、これらのオブジェクトを公開変数にドラッグします。

    Unity-P2P-Getting-Started

  2. ゲームが既に開始されているかどうかを確認するために使用されるゲームステータスを保存するために、SessionRoomHandler.csNetworkVariable を追加します。

    private NetworkVariable<bool> isGameStarted;
  3. サーバーがゲームの開始プロセスを実行するための ServerRPC 関数を作成し、isGameStarted 値を変更して他のクライアントに告知します。

    [ServerRpc(RequireOwnership = false)]
    private void StartGameServerRpc(ServerRpcParams serverRpcParams = default)
    {
    isGameStarted.Value = true;

    // Reset ready state for all player
    for (int i = 0; i < playerInfoList.Count; i++)
    {
    playerInfoList[i] = new PlayerInfo(
    playerInfoList[i].clientId,
    playerInfoList[i].userId,
    playerInfoList[i].displayName,
    false
    );
    }
    }
    IMPORTANT

    ゲームプレイに別のシーンがある場合は、 NetworkManager が提供する SceneManager を使用して、すべてのクライアントのシーンを直接変更できます。

    これを行うには、isGameStarted 値を変更する代わりに StartGameServerRpc() 関数内に次のコードを追加します。

    NetworkManager.Singleton.SceneManager.LoadScene("<<GameSceneName>>", UnityEngine.SceneManagement.LoadSceneMode.Single);
  4. Setup() 関数内で、startButton のリスナーを追加して StartGameServerRpc() 関数を呼び出します。この例では、ゲームの状態にダミーのゲームプレイパネルを使用しているため、SessionRoomHandler.cs でも GameplayPanel の UI リスナーを初期化します。

    public void Setup()
    {
    ...

    // Setup UI listeners
    if (!isInitialized)
    {
    ...

    startButton.onClick.AddListener(() =>
    {
    StartGameServerRpc();
    });

    // Setup GameplayPanel's Exit Button listener
    exitGameplayButton.onClick.AddListener(() =>
    {
    isGameStarted.Value = false;
    });
    }
    }
  5. isGameStarted 値が true に設定されている場合、クライアントの現在のビューを変更するために呼び出されるコールバック関数を SessionRoomHandler.cs に作成します。

    public void OnIsGameStartedValueChanged(bool oldValue, bool newValue)
    {
    if (newValue)
    {
    // move to next panel
    SessionRoomWindow.SetActive(false);
    GameplayWindow.SetActive(true);
    }
    else
    {
    // move to next panel
    GameplayWindow.SetActive(false);
    Setup();
    }
    }
  6. SessionRoomHandler.csOnNetworkSpawn() 上書き関数内で、OnIsGameStartedValueChanged()isGameStartedOnValueChanged コールバックイベントにサブスクライブします。

    public override void OnNetworkSpawn()
    {
    ...

    if (NetworkManager.Singleton.IsClient)
    {
    ...
    isGameStarted.OnValueChanged += OnIsGameStartedValueChanged;
    }
    ...

セッションからの離脱

  1. CustomGamesHandler.cs から LeaveSession() 関数を削除します。SessionRoomHandler.cs で、新しい LeaveSession() を作成し、サーバー/ホストインスタンスを停止する代わりに、NetworkManager の Shutdown() も使用します。

    private void LeaveSession()
    {
    // stop the client and disconnect from server
    NetworkManager.Singleton.Shutdown();

    // Clear player info list
    playerInfoList.Clear();

    // change view to CustomGamesPanel
    SessionRoomWindow.SetActive(false);
    GetComponent<CustomGamesHandler>().Setup();
    }
  2. クライアントがセッションを離脱すると、サーバーはプレイヤーをセッションから登録解除する必要があります。そのため、SessionRoomHandler.cs に関数を作成して、登録解除を処理します。

    private void UnregisterPlayerFromSession(string sessionId, string userId)
    {
    sessionBrowser.UnregisterPlayer(sessionId, userId, result =>
    {
    if (!result.IsError)
    {
    Debug.Log($"Unregister player {userId} to session success");
    }
    else
    {
    Debug.Log($"Failed to unregister player from session, Error: {result.Error.Code}, Description: {result.Error.Message}");
    }
    });
    }
  3. SessionRoomHandler.cs にコールバック関数を作成します。この関数は、プレイヤーをセッションから登録解除し、playerInfoList からプレイヤーデータを削除します。切断されたのがホストの場合、クライアントは LeaveSession() を呼び出してクライアントインスタンスを停止し、サーバーから切断する必要があります。

    public void OnClientDisconnected(ulong clientId)
    {
    // if the current player is also the server
    if (IsServer)
    {
    // remove client's player data from player list
    for (int i = 0; i < playerInfoList.Count; i++)
    {
    if (playerInfoList[i].clientId == clientId)
    {
    UnregisterPlayerFromSession(currentSessionId, playerInfoList[i].userId.ToString());
    playerInfoList.RemoveAt(i);
    break;
    }
    }
    }

    // if the current player is the client
    if (IsClient)
    {
    // if the client who disconnected is the server/host
    if (clientId == NetworkManager.ServerClientId)
    {
    LeaveSession();
    }
    }
    }
  4. SessionRoomHandler.csOnNetworkSpawn() 上書き関数内で OnClientDisconnectCallback のコールバック関数を定義します。

    public override void OnNetworkSpawn()
    {
    ...
    NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
    }
  5. SessionRoomHandler.cs のネットワーク消滅のコールバックイベントからすべてのコールバック関数をリセットしてサブスクライブを解除します。

    public override void OnNetworkDespawn()
    {
    NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler("Register");

    playerInfoList.OnListChanged -= OnPlayerInfoListChanged;
    isGameStarted.OnValueChanged -= OnIsGameStartedValueChanged;

    NetworkManager.Singleton.OnServerStarted -= OnServerStarted;
    NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnected;
    }

完全なコード

  1. AuthenticationHandler
  2. CustomGamesHandler
  3. SessionRoomPanel
  4. PlayerReadyPanel
  5. ServerInfoPanel
  6. Data