Version: 5.4
전송 레이어 API 사용(Using the Transport Layer API)
모바일 디바이스용 네트워킹 팁(Networking Tips for Mobile devices)

Internet Services

We offer Internet services which complement our networking system to support your game throughout production and release. This includes a multiuser server service to allow your game to communicate across the internet. It provides the ability for users to create matches and advertise matches, list available matches and join matches.

멀티플레이어 서비스 설정

프로젝트를 등록해야 매치메이커와 인터넷 서비스를 사용할 수 있습니다. 오른쪽 상단 코너의 클라우드 아이콘이나 애플리케이션 메뉴에서 Window -> Unity Services로 가서 서비스 창을 열면 멀티플레이어 패널이 나타납니다. 이곳에 클라우드 멀티플레이어 웹사이트 링크가 있습니다. 직접 https://multiplayer.unity3d.com에 방문해도 됩니다. 그 곳에서 프로젝트 이름을 찾은 후 멀티플레이어 설정을 진행해야 합니다.

Unity 5.1.x 버전 참고: 이 Unity 버전에서 프로젝트 ID는 수동으로 설정되며, Edit -> Project Settings -> Player의 Player settings에 해당 필드가 있습니다. https://multiplayer.unity3d.com를 방문하여 프로젝트를 수동으로 설정하고 멀티플레이어 설정을 생성해야 합니다. 설정을 보면 ID가 나타날 것입니다. 이 ID는 현재 UPID라고 부르며, 12345678–1234–1234–1234–123456789ABC와 같은 형식을 가집니다.

매치메이킹 서비스

멀티플레이어 네트워킹 기능에는 공용 IP 주소 없이 인터넷을 통해 플레이어가 게임을 진행할 수 있도록 하는 서비스를 제공합니다. 사용자는 게임을 생성하고, 액티브 게임 리스트를 확인한 후 게임에 참여하거나 나갈 수 있습니다. 인터넷으로 게임을 하는 경우 네트워크 트래픽은 클라우드의 Unity가 호스트한 릴레이 서버로 전송될 뿐 클라이언트 간 직접적으로 전송되지는 않습니다. 이를 통해 방화벽이나 NAT 관련 문제를 피할 수 있으므로 플레이어가 거의 어디서든 게임에 참여할 수 있습니다.

매치메이킹 기능은 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 server)

릴레이 서버는 위에 언급한 바와 같이 매치메이커 서버와 긴밀하게 작동합니다. 고수준 클래스는 릴레이를 자동으로 처리하므로 추가 작업이 많이 필요하지 않습니다. 하지만 아래의 예시는 직접 NetworkTransport 클래스를 사용하여 로우 레벨 전송 레이어를 통해 매치메이커와 릴레이 서버를 사용할 수도 있다는 것을 보여줍니다.

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 사용(Using the Transport Layer API)
모바일 디바이스용 네트워킹 팁(Networking Tips for Mobile devices)