IVRC2024_Hataage

Network Discovery

Netcode for GameObjects (通称NGO) でマルチプレイを行う際、LAN内のホストに接続するためにはホスト端末のIPアドレスとポート番号 (デフォルト: 7777) を指定する必要がある。

しかし、ホスト端末のIPアドレスは (多くの場合は) DHCPによる動的IPアドレス割り当てによって都度変更されるため、マルチプレイの前にわざわざ確認する必要がある。

Windowsであればコマンドプロンプトから ipconfig コマンドを打てばいいが、モバイル端末 (Meta Quest も含む) ではWi-Fi設定などを漁って目視で確認しなければならないため不便である。

そこで、LAN内で NGO のホストをしている端末を自動で検索する仕組みとして、Unity Technologies が公開している Netcode for GameObjects Community Extensions から、Runtime/NetworkDiscovery を利用した。

Netcode for GameObjects Community Extensions

Network Discovery のサンプル

Network Discovery を実装するためのサンプルとして,Netcode for GameObjects Community Extensions から取り込んだ Runtime/NetworkDiscoveryExampleNetworkDiscovery.csExampleNetworkDiscoveryHud.cs がある。

NGO を利用する際に用意した Network Manager がついているオブジェクトにこれら2つのコンポーネントを追加することで使える.

これらのコンポーネントをつけた状態でホストまたはサーバを開始すると,”Stop Server Discovery” というボタンが画面左に表示される.これが出ている間はLAN内から発見できるということである.ボタンを押すことで公開状態を切り替えられる.

ホストまたはサーバが発見可能な状態でクライアントを開始すると,”Discover Servers” というボタンが表示される.これを押すとLAN内の発見可能なホストの名前とIPアドレスが表示されるので,接続したいものをクリックすることで,その接続先の情報が Network ManagerUnity Transport の接続情報として登録され,クライアントとして接続される.

ちなみに,公開されている ExampleNetworkDiscoveryHud.cs ではフォントサイズが小さかったり,IPアドレスの表示が見にくかったりするので,以下のようにスクリプトを書き換えると使いやすいかもしれない.

using System.Collections.Generic;
using System.Net;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using Object = UnityEngine.Object;

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Events;
#endif

[RequireComponent(typeof(ExampleNetworkDiscovery))]
[RequireComponent(typeof(NetworkManager))]
public class ExampleNetworkDiscoveryHud : MonoBehaviour
{
    [SerializeField, HideInInspector]
    ExampleNetworkDiscovery m_Discovery;
    
    NetworkManager m_NetworkManager;

    Dictionary<IPAddress, DiscoveryResponseData> discoveredServers = new Dictionary<IPAddress, DiscoveryResponseData>();

    public Vector2 DrawOffset = new Vector2(10, 210);

    // UI設定
    [SerializeField] private int UI_fontSize = 50; // フォントサイズ
    [SerializeField] private int UI_buttonWidth = 500; // ボタンの横幅

    void Awake()
    {
        m_Discovery = GetComponent<ExampleNetworkDiscovery>();
        m_NetworkManager = GetComponent<NetworkManager>();
    }

#if UNITY_EDITOR
    void OnValidate()
    {
        if (m_Discovery == null) // This will only happen once because m_Discovery is a serialize field
        {
            m_Discovery = GetComponent<ExampleNetworkDiscovery>();
            UnityEventTools.AddPersistentListener(m_Discovery.OnServerFound, OnServerFound);
            Undo.RecordObjects(new Object[] { this, m_Discovery}, "Set NetworkDiscovery");
        }
    }
#endif

    void OnServerFound(IPEndPoint sender, DiscoveryResponseData response)
    {
        discoveredServers[sender.Address] = response;
    }

    void OnGUI()
    {
        // カスタムGUIスタイルを作成
        GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
        buttonStyle.fontSize = UI_fontSize; // 文字サイズを設定
        buttonStyle.fixedWidth = UI_buttonWidth; // ボタンの幅を設定
        // buttonStyle.fixedHeight = UI_buttonHeight; // ボタンの高さを設定

        GUILayout.BeginArea(new Rect(DrawOffset, new Vector2(500, 1200)));

        if (m_NetworkManager.IsServer || m_NetworkManager.IsClient)
        {
            if (m_NetworkManager.IsServer)
            {
                ServerControlsGUI(buttonStyle);
            }
        }
        else
        {
            ClientSearchGUI(buttonStyle);
        }

        GUILayout.EndArea();
    }

    void ClientSearchGUI(GUIStyle buttonStyle)
    {
        if (m_Discovery.IsRunning)
        {
            if (GUILayout.Button("Stop Client Discovery", buttonStyle))
            {
                m_Discovery.StopDiscovery();
                discoveredServers.Clear();
            }

            if (GUILayout.Button("Refresh List", buttonStyle))
            {
                discoveredServers.Clear();
                m_Discovery.ClientBroadcast(new DiscoveryBroadcastData());
            }

            GUILayout.Space(40);

        foreach (var discoveredServer in discoveredServers)
        {
            // ボタンのテキストを組み立て
            string buttonText = $"{discoveredServer.Value.ServerName}\n{discoveredServer.Key.ToString()}";

            // ボタンの領域を作成
            Rect buttonRect = GUILayoutUtility.GetRect(new GUIContent(buttonText), buttonStyle);
            if (GUI.Button(buttonRect, ""))
            {
                UnityTransport transport = (UnityTransport)m_NetworkManager.NetworkConfig.NetworkTransport;
                transport.SetConnectionData(discoveredServer.Key.ToString(), discoveredServer.Value.Port);
                m_NetworkManager.StartClient();
            }

            // ボタンの上にテキストを描画
            GUI.Label(buttonRect, buttonText, buttonStyle);
        }
        }
        else
        {
            if (GUILayout.Button("Discover Servers", buttonStyle))
            {
                m_Discovery.StartClient();
                m_Discovery.ClientBroadcast(new DiscoveryBroadcastData());
            }
        }
    }

    void ServerControlsGUI(GUIStyle buttonStyle)
    {
        if (m_Discovery.IsRunning)
        {
            if (GUILayout.Button("Stop Server Discovery", buttonStyle))
            {
                m_Discovery.StopDiscovery();
            }
        }
        else
        {
            if (GUILayout.Button("Start Server Discovery", buttonStyle))
            {
                m_Discovery.StartServer();
            }
        }
    }

}

VRでの実装

サンプルの実装ではGUIのボタンをスクリプトで作成し、それをクリックすることで操作していた。しかしこのボタンはVRのコントローラーやハンドトラッキングでの操作ができない。

そこで今回の制作では、これを基にしてVRコントローラー (Meta Quest 3 付属の Touch Plus Controller) を用いたログイン操作を行えるようなスクリプトを作成した。

また、さらなる操作の簡略化として、「クライアントがホストを検索→その一覧から接続先を選ぶ」という手順ではなく、検索した中から一番目のホストに自動的に接続するような仕組みも用意した。

それぞれを紹介する。

コントローラによるログイン操作

今回は以下の要件に基づいたスクリプトを作成した。