Transport Layer API の使用
モバイルデバイスでのネットワーク Tips

インターネット サービス

Unity では、ゲームの制作から発売までの一貫したサポートを実現するため、インターネットサービスとしてネットワーキングシステムを提供しています。これには、ゲームのインターネット通信を可能にするマルチユーザー・サーバー・サービスが含まれます。これによりユーザーは、マッチの作成・宣伝、空いているマッチの一覧、マッチとの合流を行うことが可能になります。

マルチプレイヤーのセットアップ

マッチメーカーやインターネットサービスを利用する前に、最初にプロジェクトを登録する必要があります。Services ウィンドウ(右上にある雲のアイコンをクリック)を開いて、マルチプレイヤーの画面を表示します。そこでは、マルチプレイヤーのウェブサイトへのリンクを見つけることができます(直接サイトへ行きたい場合は https://multiplayer.unity3d.com になります)。マルチプレイヤーのサイトでプロジェクト名を探し、マルチプレイヤー用の構成をセットアップします。

Unity 5.1.x での注意: プロジェクト ID を手動で設定する必要があり、PlayerSettings(Edit -> Project Settings -> Player)にプロジェクト ID を入力する場所があります。https://multiplayer.unity3d.com へと行き、プロジェクトを手動でセットアップしマルチプレイヤー用の構成を作成してください。エディターで設定する必要のある ID は、設定画面にあります(UPID と呼ばれており、12345678–1234–1234–1234–123456789ABC というような形式です)。

マッチメイキング サービス

マルチプレイヤーのネットワーキング機能には、IP アドレスを使用することなくプレイヤーがインターネットを介して一緒にプレイするための各種サービスが含まれます。ユーザーはゲームを作成したり、アクティブなゲームの一覧を取得したり、ゲームに参加したり退出したりすることができます。インターネットを介してプレイする際、ネットワーク トラフィックは、直接クライアント同士で繋がるのではなく、Unity がクラウドでホストするリレーサーバーを経由します。これによってファイアウォールや NATs の問題が回避され、プレイヤーはほぼどこからでも参加することが可能になります。

マッチメイキング機能は、特殊スクリプト NetworkMatch によって使用可能です。これは UnityEngine.Networking.Match の名前空間にあります。LLAPI にはリレーサーバーを使用する機能が備わっていますが、マッチメーカーはそれをより使い易くします。これを使用するには、NetworkMatch からスクリプトを派生させて管理オブジェクトに添付します。下記の例では、マッチの作成、マッチの一覧表示、マッチとの合流を行っています。

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.Types;
using UnityEngine.Networking.Match;
using System.Collections.Generic;

public class HostGame : MonoBehaviour
{
    List<MatchDesc> matchList = new List<MatchDesc>();
    bool matchCreated;
    NetworkMatch networkMatch;

    void Awake()
    {
        networkMatch = gameObject.AddComponent<NetworkMatch>();
    }

    void OnGUI()
    {
        // You would normally not join a match you created yourself but this is possible here for demonstration purposes.
        if(GUILayout.Button("Create Room"))
        {
            CreateMatchRequest create = new CreateMatchRequest();
            create.name = "NewRoom";
            create.size = 4;
            create.advertise = true;
            create.password = "";

            networkMatch.CreateMatch(create, OnMatchCreate);
        }

        if (GUILayout.Button("List rooms"))
        {
            networkMatch.ListMatches(0, 20, "", OnMatchList);
        }

        if (matchList.Count > 0)
        {
            GUILayout.Label("Current rooms");
        }
        foreach (var match in matchList)
        {
            if (GUILayout.Button(match.name))
            {
                networkMatch.JoinMatch(match.networkId, "", OnMatchJoined);
            }
        }
    }

    public void OnMatchCreate(CreateMatchResponse matchResponse)
    {
        if (matchResponse.success)
        {
            Debug.Log("Create match succeeded");
            matchCreated = true;
            Utility.SetAccessTokenForNetwork(matchResponse.networkId, new NetworkAccessToken(matchResponse.accessTokenString));
            NetworkServer.Listen(new MatchInfo(matchResponse), 9000);
        }
        else
        {
            Debug.LogError ("Create match failed");
        }
    }

    public void OnMatchList(ListMatchResponse matchListResponse)
    {
        if (matchListResponse.success && matchListResponse.matches != null)
        {
            networkMatch.JoinMatch(matchListResponse.matches[0].networkId, "", OnMatchJoined);
        }
    }

    public void OnMatchJoined(JoinMatchResponse matchJoin)
    {
        if (matchJoin.success)
        {
            Debug.Log("Join match succeeded");
            if (matchCreated)
            {
                Debug.LogWarning("Match already set up, aborting...");
                return;
            }
            Utility.SetAccessTokenForNetwork(matchJoin.networkId, new NetworkAccessToken(matchJoin.accessTokenString));
            NetworkClient myClient = new NetworkClient();
            myClient.RegisterHandler(MsgType.Connect, OnConnected);
            myClient.Connect(new MatchInfo(matchJoin));
        }
        else
        {
            Debug.LogError("Join match failed");
        }
    }

    public void OnConnected(NetworkMessage msg)
    {
        Debug.Log("Connected!");
    }
}

このスクリプトにはマッチメーカーを、Unity のパブリックのマッチメーカーサーバーに向けます。これはマッチの作成、一覧表示と合流用のベースクラス関数を呼び出します。CreateMatch でマッチを作成、JoinMatch でマッチに合流、ListMatches でマッチメーカーサーバーに登録されたマッチを一覧表示します。内部的には、NetworkMatch はウェブサービスを使用してマッチを確立し、処理が完了すると所定のコールバック関数が実行されます。(例えばマッチの作成の場合は OnMatchCreate。)

直接接続ではなくリレーサーバーを使用するには、シングルトンの NetworkMatch.matchSingleton を追加する必要があります。これによりシステムに、ゲームに接続する際に直接接続ではなくリレーサーバーを使用するように指示がされます。結果、その後クライアントが実際にゲームに接続した際に、選択されたマッチに応じた適切なリレーサーバーが自動的に使用されるようになります。

リレーサーバー

リレーサーバーは上記のとおり、マッチメーカーサーバーと密接に連携しています。リレーは高レベルのクラスによって自動的に行われます。ユーザーはほとんど余計な作業をしなくて済みます。ただし、ここにあげる例のように、 NetworkTransport クラスを直接使用することで、低レベル Transport Layer によってマッチメーカーとリレーサーバーを使用する方法もあります。

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.Types;
using UnityEngine.Networking.Match;
using System.Collections.Generic;

public class SimpleSetup : MonoBehaviour
{
    // Matchmaker related
    List<MatchDesc> m_MatchList = new List<MatchDesc>();
    bool m_MatchCreated;
    bool m_MatchJoined;
    MatchInfo m_MatchInfo;
    string m_MatchName = "NewRoom";
    NetworkMatch m_NetworkMatch;

    // Connection/communication related
    int m_HostId = -1;
    // On the server there will be multiple connections, on the client this will only contain one ID
    List<int> m_ConnectionIds = new List<int>();
    byte[] m_ReceiveBuffer;
    string m_NetworkMessage = "Hello world";
    string m_LastReceivedMessage = "";
    NetworkWriter m_Writer;
    NetworkReader m_Reader;
    bool m_ConnectionEstablished;

    const int k_ServerPort = 25000;
    const int k_MaxMessageSize = 65535;

    void Awake()
    {
        m_NetworkMatch = gameObject.AddComponent<NetworkMatch>();
    }

    void Start()
    {
        m_ReceiveBuffer = new byte[k_MaxMessageSize];
        m_Writer = new NetworkWriter();
        // While testing with multiple standalone players on one machine this will need to be enabled
        Application.runInBackground = true;
    }

    void OnApplicationQuit()
    {
        NetworkTransport.Shutdown();
    }

    void OnGUI()
    {
        if (string.IsNullOrEmpty(Application.cloudProjectId))
            GUILayout.Label("You must set up the project first. See the Multiplayer tab in the Service Window");
        else
            GUILayout.Label("Cloud Project ID: " + Application.cloudProjectId);

        if (m_MatchJoined)
            GUILayout.Label("Match joined '" + m_MatchName + "' on Matchmaker server");
        else if (m_MatchCreated)
            GUILayout.Label("Match '" + m_MatchName + "' created on Matchmaker server");

        GUILayout.Label("Connection Established: " + m_ConnectionEstablished);

        if (m_MatchCreated || m_MatchJoined)
        {
            GUILayout.Label("Relay Server: " + m_MatchInfo.address + ":" + m_MatchInfo.port);
            GUILayout.Label("NetworkID: " + m_MatchInfo.networkId + " NodeID: " + m_MatchInfo.nodeId);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Outgoing message:");
            m_NetworkMessage = GUILayout.TextField(m_NetworkMessage);
            GUILayout.EndHorizontal();
            GUILayout.Label("Last incoming message: " + m_LastReceivedMessage);

            if (m_ConnectionEstablished && GUILayout.Button("Send message"))
            {
                m_Writer.SeekZero();
                m_Writer.Write(m_NetworkMessage);
                byte error;
                for (int i = 0; i < m_ConnectionIds.Count; ++i)
                {
                    NetworkTransport.Send(m_HostId, 
                        m_ConnectionIds[i], 0, m_Writer.AsArray(), m_Writer.Position, out error);
                    if ((NetworkError)error != NetworkError.Ok)
                        Debug.LogError("Failed to send message: " + (NetworkError)error);
                }
            }

            if (GUILayout.Button("Shutdown"))
            {
                m_NetworkMatch.DropConnection(m_MatchInfo.networkId, 
                    m_MatchInfo.nodeId, OnConnectionDropped);
            }
        }
        else
        {
            if (GUILayout.Button("Create Room"))
            {
                m_NetworkMatch.CreateMatch(m_MatchName, 4, true, "", OnMatchCreate);
            }

            if (GUILayout.Button("Join first found match"))
            {
                m_NetworkMatch.ListMatches(0, 1, "", (response) => {
                    if (response.success && response.matches.Count > 0)
                        m_NetworkMatch.JoinMatch (response.matches [0].networkId, "", OnMatchJoined);   
                });
            }
                
            if (GUILayout.Button ("List rooms"))
            {
                m_NetworkMatch.ListMatches (0, 20, "", OnMatchList);
            }

            if (m_MatchList.Count > 0)
            {
                GUILayout.Label ("Current rooms:");
            }
            foreach (var match in m_MatchList) 
            {
                if (GUILayout.Button (match.name))
                {
                    m_NetworkMatch.JoinMatch(match.networkId, "", OnMatchJoined);
                }
            }
        }
    }

    public void OnConnectionDropped(BasicResponse callback)
    {
        Debug.Log("Connection has been dropped on matchmaker server");
        NetworkTransport.Shutdown();
        m_HostId = -1;
        m_ConnectionIds.Clear();
        m_MatchInfo = null;
        m_MatchCreated = false;
        m_MatchJoined = false;
        m_ConnectionEstablished = false;
    }

    public void OnMatchCreate(CreateMatchResponse matchResponse)
    {
        if (matchResponse.success)
        {
            Debug.Log("Create match succeeded");
            Utility.SetAccessTokenForNetwork(matchResponse.networkId, 
                new NetworkAccessToken(matchResponse.accessTokenString));

            m_MatchCreated = true;
            m_MatchInfo = new MatchInfo(matchResponse);

            StartServer(matchResponse.address, matchResponse.port, matchResponse.networkId, 
                matchResponse.nodeId);
        }
        else
        {
            Debug.LogError ("Create match failed");
        }
    }

    public void OnMatchList(ListMatchResponse matchListResponse)
    {
        if (matchListResponse.success && matchListResponse.matches != null)
        {
            m_MatchList = matchListResponse.matches;
        }
    }

    // When we've joined a match we connect to the server/host
    public void OnMatchJoined(JoinMatchResponse matchJoin)
    {
        if (matchJoin.success)
        {
            Debug.Log("Join match succeeded");
            Utility.SetAccessTokenForNetwork(matchJoin.networkId, 
                new NetworkAccessToken(matchJoin.accessTokenString));

            m_MatchJoined = true;
            m_MatchInfo = new MatchInfo(matchJoin);

            Debug.Log ("Connecting to Address:" + matchJoin.address + 
                " Port:" + matchJoin.port + 
                " NetworKID: " + matchJoin.networkId + 
                " NodeID: " + matchJoin.nodeId);
            ConnectThroughRelay(matchJoin.address, matchJoin.port, matchJoin.networkId, 
                matchJoin.nodeId);
        }
        else
        {
            Debug.LogError("Join match failed");
        }
    }

    void SetupHost(bool isServer)
    {
        Debug.Log("Initializing network transport");
        NetworkTransport.Init();
        var config = new ConnectionConfig();
        config.AddChannel(QosType.Reliable);
        config.AddChannel(QosType.Unreliable);
        var topology = new HostTopology(config, 4);
        if (isServer)
            m_HostId = NetworkTransport.AddHost(topology, k_ServerPort);
        else
            m_HostId = NetworkTransport.AddHost(topology);
    }

    void StartServer(string relayIp, int relayPort, NetworkID networkId, NodeID nodeId)
    {
        SetupHost(true);

        byte error;
        NetworkTransport.ConnectAsNetworkHost(
            m_HostId, relayIp, relayPort, networkId, Utility.GetSourceID(), nodeId, out error);
    }

    void ConnectThroughRelay(string relayIp, int relayPort, NetworkID networkId, NodeID nodeId)
    {
        SetupHost(false);

        byte error;
        NetworkTransport.ConnectToNetworkPeer(
            m_HostId, relayIp, relayPort, 0, 0, networkId, Utility.GetSourceID(), nodeId, out error);
    }

    void Update()
    {
        if (m_HostId == -1)
            return;

        var networkEvent = NetworkEventType.Nothing;
        int connectionId;
        int channelId;
        int receivedSize;
        byte error;

        // Get events from the relay connection
        networkEvent = NetworkTransport.ReceiveRelayEventFromHost (m_HostId, out error);
        if (networkEvent == NetworkEventType.ConnectEvent)
            Debug.Log ("Relay server connected");
        if (networkEvent == NetworkEventType.DisconnectEvent)
            Debug.Log ("Relay server disconnected");

        do
        {
            // Get events from the server/client game connection
            networkEvent = NetworkTransport.ReceiveFromHost(m_HostId, out connectionId, out channelId, 
                m_ReceiveBuffer, (int)m_ReceiveBuffer.Length, out receivedSize, out error);
            if ((NetworkError)error != NetworkError.Ok)
            {
                Debug.LogError("Error while receiveing network message: " + (NetworkError)error);
            }

            switch (networkEvent)
            {
                case NetworkEventType.ConnectEvent:
                {
                    Debug.Log("Connected through relay, ConnectionID:" + connectionId + 
                        " ChannelID:" + channelId);
                    m_ConnectionEstablished = true;
                    m_ConnectionIds.Add(connectionId);
                    break;
                }
                case NetworkEventType.DataEvent:
                {
                    Debug.Log("Data event, ConnectionID:" + connectionId + 
                        " ChannelID: " + channelId +
                        " Received Size: " + receivedSize);
                    m_Reader = new NetworkReader(m_ReceiveBuffer);
                    m_LastReceivedMessage = m_Reader.ReadString();
                    break;
                }
                case NetworkEventType.DisconnectEvent:
                {
                    Debug.Log("Connection disconnected, ConnectionID:" + connectionId);
                    break;
                }
                case NetworkEventType.Nothing:
                break;
            }
        } while (networkEvent != NetworkEventType.Nothing);
    }
}
Transport Layer API の使用
モバイルデバイスでのネットワーク Tips