MPPM(Multiplayer Play Mode)とは
MPPMは、1つのUnityエディタ上で複数のプレイヤーを同時に起動・実行できる機能です。
個別にビルドを行うことなく、ネットワーク同期や接続挙動を即座に検証できるため、マルチプレイヤー開発の検証効率を大きく向上させます。
開発初期からデバッグまで活用できる、実践的で信頼性の高いローカルテスト手法です。
やりたいこと
MPPMを使ってPlayerを4つ起動させる
起動させるシーンはMyGameTitleScene
- 条件
- 同時に4つ起動せず、mainEdtior(Player1)から起動しPlayer2,Player3,Player4の順に起動する
- MyGameTitleSceneで実行される、アセットダウンロード処理完了後、次のPlayerを起動させる
環境
Unity6000.3.2f
MPPM v2.0.1
MackBook Pro(M4 Max) macOS 15.5
Run In Background
まずは、MPPMで使用するのに必須?の Run In Backgroundにチェックを入れましょう。
Unityメニュー Edit -> Project Settings...から変更できます。
シナリオの作成
シナリオとは起動する各Playerの設定(Tagなど)をまとめたもの。
Unityメニュー Window -> Multiplayer -> Play Mode Scenarioからシナリオを作成する。
Initial SceneにMppmStartSceneを設定する。(後で説明)
Additional Editor InstancesでPlayer2,3,4を追加。
各EdtiorのStream Logs To Main Editorにチェック入れておくと、MainEditorにDebug.Logに出力が転送されて便利。
Log Colorも各Editor毎に変更しておくと見やすい。
シナリオのInitial Scene
作成したシナリオは再生ボタンの横のボタンで選択ができます。
その状態で再生ボタンをクリックすると、Initial Sceneに設定したシーンが最初にロードされます。
Initial SceneでMyGameTitleSceneのロードタイミングを制御する
MppmStartScene( Initial Scene )にはMppmStartコンポーネント(MppmStart.cs)が設定されており、MyGameTitleSceneのロードタイミングを制御します。
public class MppmStart : MonoBehaviour
{
:
private void Start() => StartAsync().Forget();
private async UniTask StartAsync()
{
// 初期化処理
Initialize();
// 自分の順番が来るまで待機
await WaitForMyTurnAsync();
// シーン遷移
MoveMyGameTitleScene();
}
:
}
WaitForMyTurnAsync 自分の順番が来るまで待機
自分の順番が来るまでの判定は、シンプルでLibrary/VP/MppmStart/START_P{PlayerIndex}のファイルが作成されるまで待機します。
MainEditor(Player1)の場合は待機せず、即シーン遷移させています。
Library/VP/MppmStart/START_P2 // Player2を起動トリガー用ファイル
Library/VP/MppmStart/START_P3 // Player3を起動トリガー用ファイル
Library/VP/MppmStart/START_P4 // Player4を起動トリガー用ファイル
UnityEditor停止時の解放処理(Release)で起動トリガー用ファイルは削除していますが、コード説明は省略
public class MppmStart : MonoBehaviour
{
:
private async UniTask WaitForMyTurnAsync()
{
// MainEditorの場合は待機しない(MainEditorはPlayerIndex==1)
if (MppmHelper.IsMainEditor) return;
// 自分のPlayerIndexの順番が来るまで待機する
var startPlayerFile = Path.Combine(VpMppmStartDirPath, $"START_P{MppmHelper.PlayerIndex}");
Debug.Log($"Waiting for my turn. Checking for file: {startPlayerFile}");
while (true)
{
// VP/MppmStart/"START_P{MppmHelper.PlayerIndex}"ファイルが存在するか確認する
if (File.Exists(startPlayerFile))
{
Debug.Log($"It's my turn! Found file: {startPlayerFile}");
return;
}
// 500ms待機して再度確認
await UniTask.Delay(500);
}
}
:
}
MppmHelper
MPPMのCurrentPlayerでMainEditor判定は可能ですが、PlayerIndexは取得できないので作成。
#nullable enable
public static class MppmHelper
{
:
private const string NameCommandLineArg = "-name";
private static int? _playerIndex;
public static int PlayerIndex
{
get
{
if (_playerIndex.HasValue) return _playerIndex.Value;
// MainEditorの場合はplayerIndexは1とする
if (IsMainEditor)
{
_playerIndex = 1;
return _playerIndex.Value;
}
// Environment.GetCommandLineArgsの -name "Player 2" プレイヤー名からIndexを取得する
var result = HasCommandLineArgument(NameCommandLineArg);
var playerName = result.nextArg;
if (playerName == null || playerName.StartsWith("Player ") == false)
{
_playerIndex = -1;
return _playerIndex.Value;
}
var indexStr = playerName.Substring("Player ".Length);
if (int.TryParse(indexStr, out int index))
{
_playerIndex = index;
return _playerIndex.Value;
}
_playerIndex = -1;
return _playerIndex.Value;
}
}
:
}
起動トリガー用ファイルの作成
MyGameTitleSceneがロードされると、MyGameTitleコンポーネントの処理が実行されます。
アセットダウンロード完了後に MppmStart.StartNextPlayerメソッドを呼び出すことで、次のPlayer用の起動トリガーファイルが作成されます。
public class MyGameTitle : MonoBehaviour
{
:
private void Start() => StartAsync().Forget();
private async UniTask StartAsync()
{
// アセットのダウンロード処理
await DownloadHeavyAssetDownloadAsync();
#if UNITY_EDITOR
// 次のプレイヤーの処理を開始させる
MppmStart.StartNextPlayer();
#endif
}
:
}
public class MppmStart : MonoBehaviour
{
:
public static void StartNextPlayer()
{
if (IsStarted == false) return;
// 次のPlayerを開始させるために
// VP/MppmStart/"START_P{MppmHelper.PlayerIndex + 1}"ファイルを作成
var nextPlayerIndex = MppmHelper.PlayerIndex + 1;
var startPlayerFile = Path.Combine(VpMppmStartDirPath, $"START_P{nextPlayerIndex}");
if (File.Exists(startPlayerFile)) return;
var dir = Path.GetDirectoryName(startPlayerFile);
if (Directory.Exists(dir) == false) Directory.CreateDirectory(dir!);
File.WriteAllText(startPlayerFile, "START");
}
:
}
シナリオ選択してPlay!!
無事意図した動作をすることができました。
まとめ
ロードタイミングの制御をどうするのか悩みました。
最初はmainEditorをサーバーにした方法も検討しましたが、なんだか大げさすぎるのでやめ、最終的にシンプルなファイルによる制御になりました。





