트랜스포트 레이어 API의 사용
모바일 기기에서의 네트워킹

인터넷 서비스

Unity는 게임 제작에서 출시까지 일관된 지원을 실현하기 위해 우리 네트워킹 시스템을 보완하는 인터넷 서비스를 제공합니다. 여기에는 게임의 인터넷 통신을 가능하게 하는 멀티유저 서버 서비스가 포함되어 있습니다. 이를 통해 유저는 매치 생성, 홍보, 이용가능한 매치 리스트업, 매치 조인을 할 수 있습니다.

멀티플레이어 서비스 설정

매치메이커 또는 인터넷 서비스를 사용하기 전에, 여러분은 먼저 프로젝트를 등록해야 합니다. Multiplayer 패널에 보이는 Services Window (cloud icon in upper right corner)를 보시기 바랍니다. 거기서 클라우드 멀티플레이어 웹사이트 링크를 찾을 수 있습니다 (직접 https://multiplayer.unity3d.com를 방문할 수도 있습니다). 프로젝트 이름을 찾고, 해당 프로젝트에 대한 멀티플레이어 환경을 설정하세요.

5.1.x 버전 유의사항: 해당 버전에서, 프로젝트 ID는 수동으로 설정됩니다. Player settings(Edit -> Project Settings -> Player) 메뉴에서 ID 필드를 찾을 수 있습니다. https://multiplayer.unity3d.com를 방문하여 여러분의 프로젝트를 수동으로 설정하고 멀티플레이어 환경설정을 생성하세요. 여러분은 환경설정 뷰를 통해 해당 ID를 확인할 수 있습니다. (it’s called UPID right now in a 12345678–1234–1234–1234–123456789ABC format).

매치메이킹(Matchmaking) 서비스

멀티플레이어 네트워킹 기능에는 공인 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을 추가해야 합니다. 이를 통해 시스템에 게임에 접속할 때 직접 연결이 아닌 릴레이 서버를 사용하도록 지시합니다. 그 결과, 다음에 클라이언트가 실제로 게임에 접속했을 때, 선택된 매치에 따라 적절한 릴레이 서버가 자동으로 사용됩니다.

릴레이(Relay) 서버

The relay server works closely with the matchmaker server, as mentioned above. The higher level classes handle the relay for you automatically. There is not much extra work for you to do. Here however is an example which shows how you could use the matchmaker and relay server through the lower level transport layer, using the NetworkTransport class directly.

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);
    }
}
트랜스포트 레이어 API의 사용
모바일 기기에서의 네트워킹