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);
}
}