マルチプレイゲームには様々な方法があります。ここではサーバー・クライアント方式で構成されるゲームを作るにはどのような手順が必要になるかをご紹介したいと思います。今回のシリーズではUnityサービスで整えたいと思います。
.
事前準備
Matchmakerを利用するには、事前にGame Server Hostingサービスにマシンなどを登録しておく必要があります。今回はこちらのリンクでのサーバー・クライアントアプリが作れている状態で作業を開始します。
サーバー・クライアント型のアプリを作るために、Netcode for Gameobjectというものを使っています。下準備かなり必要ですが頑張ってください。
その他関連記事
Matchmakerを連携させる
サービスを利用するにはUnityのアカウントが必要になります。最近はUnity使うのに必須なのでそこまで重要ではないかも。
UnityHubに対象のアカウントでログインを行う
マルチプレイ等のサービスを利用するにはUnityIDのアカウントが必要になります。作成後はUnityHubに使いたいアカウントでログインを行います。
アプリの連携
プロジェクトを作成したら、プロジェクトセッティングを開きましょう。
Serviceタブからアプリを作成するか、既存のアプリから連携したいものを選択することができます。
- 組織:Organization
- プロジェクト
ProjectSettingsから作る場合、プロジェクトの名前がそのまま反映されるので、プロジェクトの名前をいい感じにしておきましょう。
ダッシュボードに移動して、機能を連携
サービス連携ができたらダッシュボードに移動して、Matchmakerが使えるようにします。プロジェクト設定からDashboardをクリックするとリダイレクトします。(移動先でサインインのボタンを押す必要がある場合も)
プロジェクト一覧から選択すると、画面下の方にいろんなサービスを有効にするボタンがあります。ここではMatchmakerを起動させます。この画面などはバージョン変更で変わります。2023-12時点では下画面のような表示でした。
起動ボタンを押すと、Matchmakerのセットアップが開始されます。
Matchmakerのセットアップ
今回はレートを考慮しない1vs1のマッチングを実装します。
Integrate Matchmakerボタンを押します。
ここでは次の設定を行ってください
- Game Engine:Unity
- Integration method:SDK
Step2は飛ばしてOK。プロジェクト作成が先か、先にダッシュボードにプロジェクト登録が先かの違いです。
Step3ではMatchmakerを使うためのパッケージ導入方法を教えてくれます。Webの操作でパッケージがインストールされるわけではないのでご注意。下図の赤い囲みをクリックしてコピーするか、「com.unity.services.matchmaker」をコピーしてからUnityに戻ります。
Unityに戻ったら、パッケージマネージャーを開き、Matchmakerのパッケージを導入しましょう。
- Window>Package Managerからパッケージマネージャーを開きます
- 左上のプラスボタンから「Add package by name」を選択
- 「com.unity.services.matchmaker」を入力してAddボタンを押す
Addボタンを押すとインストールするかしないかの確認はなく、インストールされます。下図のようにMatchmaker画面が表示され、Removeボタンになっていれば導入成功で
Integrate Matchmakerの設定が終わったら、Queueを作成します。読み方は「キュー」。キューは郵便で言うところのハガキや封筒みたいなものです。
- Queue name:ユニークなキューの名前(Queue-1vs1)
- Maximum players on a ticket:チケットあたりのプレイヤーの最大数(1)
Pool(プール)を作成します。プールはマッチングするためのグループ分けのようなものを行うのに使います。
- Pool Details
- Pool name:TestPool01(ユニークなプールの名前)
- Queue:Queue-1vs1(利用したいキューを選択)
- Pool type:Default pool
- Timeout:60(タイムアウトまでの必要時間)
- Next
- Hosting settings
- こちらの項目は、ホスティングサービスの設定を行っていないと完了できません。ご注意!
- Hostring type:Game Server Hosting(Multiplay)
- Fleet:Fleet01(作成済みの中から選択)
- Build configuration:BuildConf01(作成済みの中から選択)
- Default QoS Region:Asia(日本がターゲットなら)
- Next
- Rules
- Logic builderで操作を行う
- Match definition
- Name:Rule-1vs1
- Backfill enabled:False
- Team definitions
- Team 1
- Team name:Team01
- Team count
- Team count min:2
- Team count max:2
- Player count
- Player count min:1
- Player count max:1
- Team01 Rules:操作なし
- Team 1
- Create
以上でMatchmakerのQueue作成は終了です。マッチングタイプによっては構成が変わりますが、まずは1vs1のマッチングを作りましょう。
クライアントがマッチングするための処理を追加
マッチメーカーでのマッチングを行うには、いくつかの手順を踏む必要があります。ここでは実装順にソースコードを提示します。最終的なコードは最後の方に記載する予定ですので、完成形と見比べながら進めてください。
マッチメーカーを利用する前に認証を行う
Matchmakerを使うためには、Unityサービスの認証を行う必要があります。ソースコードはNetworkManagerUIを使いまわしますが、Hostボタンなどは不要になるので削除します。基本的にクライアント専用のプログラムです。
using UnityEngine;
using UnityEngine.UI;
using Unity.Netcode;
using System;
using Unity.Netcode.Transports.UTP;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
public class NetworkManagerUI : MonoBehaviour
{
[SerializeField] private Button clientButton;
private void Awake()
{
clientButton.interactable = false;
clientButton.onClick.AddListener(() =>
{
// マッチングを開始する処理を記載する
});
}
async private void Start()
{
bool isClient = true;
var args = System.Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "-dedicatedServer")
{
isClient = false;
}
}
if (isClient)
{
await UnityServices.InitializeAsync();
await AuthenticationService.Instance.SignInAnonymouslyAsync();
clientButton.interactable = true;
}
}
}
機能としてはクライアントボタンが押せるようになるのは認証完了後。今回は匿名ユーザーとしてサインインします。
マッチング開始処理:チケット作成など
まずはボタンがを押してマッチングを開始します。マッチングにはチケットというものが必要で、その発行作業を行います。今回は使わないですが、スキルマッチやランクマッチを行う場合にパラメータを入れる方法も少しだけ追記します。
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using Unity.Netcode;
using System;
using Unity.Netcode.Transports.UTP;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Matchmaker;
using Unity.Services.Matchmaker.Models;
using StatusOptions = Unity.Services.Matchmaker.Models.MultiplayAssignment.StatusOptions;
public class NetworkManagerUI : MonoBehaviour
{
[SerializeField] private Button clientButton;
private void Awake()
{
clientButton.interactable = false;
clientButton.onClick.AddListener(() =>
{
// マッチングを開始する処理を記載する
CreateTicket();
clientButton.interactable = false;
});
}
async private void Start()
{
bool isClient = true;
var args = System.Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "-dedicatedServer")
{
isClient = false;
}
}
if (isClient)
{
await UnityServices.InitializeAsync();
await AuthenticationService.Instance.SignInAnonymouslyAsync();
clientButton.interactable = true;
}
}
private async void CreateTicket()
{
var options = new CreateTicketOptions("Queue-1vs1");
var players = new List<Player>
{
new Player(AuthenticationService.Instance.PlayerId,
new MatchmakingPlayerData
{
skill = 100
})
};
var ticketResponse = await MatchmakerService.Instance.CreateTicketAsync(players, options);
string ticketID = ticketResponse.Id;
Debug.Log($"Created ticket {ticketID}");
PollTicketStatus(ticketID);
}
private async void PollTicketStatus(string ticketID)
{
MultiplayAssignment multiplayAssignment = null;
bool isAssigning = true;
bool isFound = false;
do
{
await Task.Delay(1000);
var ticketStatus = await MatchmakerService.Instance.GetTicketAsync(ticketID);
if (ticketStatus == null)
{
Debug.Log("Ticket not found");
continue;
}
if (ticketStatus.Type == typeof(MultiplayAssignment))
{
multiplayAssignment = ticketStatus.Value as MultiplayAssignment;
switch (multiplayAssignment.Status)
{
case StatusOptions.Found:
Debug.Log("Found match");
isAssigning = false;
isFound = true;
TicketAssigned(multiplayAssignment);
break;
case StatusOptions.InProgress:
break;
case StatusOptions.Failed:
isAssigning = false;
Debug.Log($"Failed to get ticket status. Error:{multiplayAssignment.Message}");
break;
case StatusOptions.Timeout:
isAssigning = false;
Debug.Log($"Ticket timed out. Error:{multiplayAssignment.Message}");
break;
default:
throw new InvalidOperationException();
}
}
}
while (isAssigning);
clientButton.interactable = !isFound;
}
private void TicketAssigned(MultiplayAssignment multiplayAssignment)
{
Debug.Log($"Ticket assigned:{multiplayAssignment.Ip}:{multiplayAssignment.Port}");
NetworkManager.Singleton.GetComponent<UnityTransport>().
SetConnectionData(
multiplayAssignment.Ip,
(ushort)multiplayAssignment.Port);
NetworkManager.Singleton.StartClient();
}
[Serializable]
public class MatchmakingPlayerData
{
public int skill;
}
}
- CreateTicketメソッド
- マッチングを行うきっかけの処理
- CreateTicketOptionsに作成したMatchmakerのQueueの名前を指定します。
- チケットにはプレイヤーをリスト形式で登録
- リストで登録するのは、ロビーやフレンドなどで複数人でマッチングするゲームなどで利用します
- MatchmakerService.Instance.CreateTicketAsyncでチケットを発券してもらう
- 申込用紙に氏名などを入力して、受付に渡してからチケットを受け取る感じですね。
- 実際のチケットをゲットするのはここ
- 受け取ったチケットのIDを見ながら割り当てを待つためにPollTicketStatusを呼び出す
- PollTicketStatusメソッド
- 発券されたチケットのIDを見ながら自分の割り当て状況を監視する
- 電光掲示板に自分のIDのチケットが表示されるの待ってる感じ
- switch文ではアサイン結果のStatusを見ることで、結果を知ることが出来る
- StatusOptions.Found
- 割り当て成功したのでTicketAssignedを呼び出してクライアントとして接続
- StatusOptions.Found以外は割り当て失敗
- 原因を調査する必要あり
- サーバーが立ち上がってない場合はTimeoutがよく起こります
- ボタンが押せるようになるので再チャレンジなど
- TicketAssignedメソッド
- ConnectionDataにIPとPort番号を指定してクライアントスタート
- IPとPort番号はMultiplayAssignmentの中に入っているものを使う
動作確認
プログラムが作成できたら接続出来るか確認してみましょう。ここではWindowsでの開発を想定します。
- アプリケーションをビルド
- Windowsにスイッチプラットフォーム(サーバーをビルドしたあとだと変わっているため)
- プラットフォームを切替後、Build
- 別フォルダにexeファイルができたことを確認
- アプリを起動
- 作成されたexeファイルを起動
- 起動後クライアントボタンが押せるようになることを確認
- すぐにクライアントボタンを押さずに一度待機
- Unityエディタを再生
- こちらも起動だけできたら一度待機
- 上記でボタンが押せることが確認できていたらこちらも同じ状態のはず
- 作成されたexeファイルを起動
- Clientボタンを押して接続
- exeファイル版、Unityエディタ版両方のクライアントボタンを1回ずつ押す
- このとき極力時間差が発生しないようにしてください
- タイムアウトが発生すると接続が行われません
- 同時に接続される
- エディタ側のログを見ると、Created ticket~のログが少し表示された後、Found match~が表示されます
- ゲーム画面にキャラが表示され、exe版とエディタ版それぞれでキャラクターを動かすことが出来るようになります
- exeファイル版、Unityエディタ版両方のクライアントボタンを1回ずつ押す
コメント