【Photon Fusion】ホストマイグレーションの話
はじめに
Photon FusionのHostMode
では、ホストがセッションから退出した時点でセッションは終了(残されたクライアントは全員解散)してしまいます。
その際に、ホストマイグレーション(Host Migration)が有効になっていれば、セッション内の別クライアントの1つを新しいホストとして、旧セッションからのスナップショット(ネットワークの状態)を引き継いだ新しいセッションでゲームを継続できるようになります。
詳細は公式ドキュメントとサンプルをご確認ください!と言いたい所ですが、結構わかりづらい所や落とし穴があるので、その辺りの補足をしつつ基本的な手順をまとめ直してみました。
開発環境
- Unity 2022.2.1f1
- Fusion SDK 1.1.5 F2 Build 643
準備
ホストマイグレーションは「開発終盤にちょっと設定するだけで、Fusionが裏側で全部いい感じに引き継ぎ処理を実行してくれるようになる」ものではないことに注意です。便利なAPIは用意されていますが、旧セッションから新セッションへどのデータをどう引き継いでゲームの状態を復元するか?などは、ゲーム開発者側が実装するものになっています。そのため、まずゲーム自体の仕様や実装がホストマイグレーションの実行が可能な形になっているか確認しましょう。
NetworkRunner
はPrefabから生成できるようにする
旧セッションを切断して新セッションへと接続する際に、旧セッションのNetworkRunner
は再利用できません。ホストマイグレーションが実行されるたびに、旧セッションのNetworkRunner
を停止した後、新セッションへと接続するためのNetworkRunner
を生成することになります。
ゲームの接続回りの実装は、柔軟にNetworkRunner
のインスタンスを生成して利用できるような仕組みにしておくと、ホストマイグレーションの実装も少し楽になるでしょう。
ConnectionToken
で識別できるようにする
プレイヤーはプレイヤーのIDとして利用できるplayer.PlayerId
はあくまでセッション内でユニークな値です。同じプレイヤーでも、旧セッションと新セッションでは(偶然一致するケースを除き)別のIDが割り当てられるので、ConnectionToken
を使ってプレイヤーを識別できるようにしましょう。
private NetworkRunner runner;
+ private byte[] connectionToken;
public async UniTask StartGame() {
+ // ConnectionTokenを一意な識別子として使うならGUIDで生成するのが簡単
+ connectionToken = Guid.NewGuid().ToByteArray();
runner = Instantiate(networkRunnerPrefab);
runner.ProvideInput = true;
var result = await runner.StartGame(new StartGameArgs {
GameMode = GameMode.AutoHostOrClient,
SessionName = "GameRoom",
Scene = SceneManager.GetActiveScene().buildIndex,
SceneManager = runner.GetComponent<NetworkSceneManagerDefault>(),
+ ConnectionToken = connectionToken
});
}
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) {
if (!runner.IsServer) { return; }
+ int token = new Guid(runner.GetPlayerConnectionToken(player)).GetHashCode();
runner.Spawn(playerAvatarPrefab, Vector3.zero, Quaternion.identity, player,
+ (_, obj) => { obj.GetComponent<PlayerAvatar>().Token = token; }
);
}
public class PlayerAvatar : NetworkBehaviour
{
+ [Networked] public int Token { get; set; }
}
Spawned()
を考慮する
ネットワークオブジェクト復元時のNetworkBehaviour
のSpawned()
は、新セッションでネットワークオブジェクトが復元された時も実行されます。ネットワークオブジェクトがホストマイグレーションで復元されたものかどうかはObject.IsResume
で判定できるので、誤ったタイミングで初期化処理などが実行されないようにしましょう。
public override void Spawned() {
+ if (!Object.IsResume) {
// 初期化処理
}
}
ゲームの状態はスナップショットから復元できるようにする
旧セッションからのスナップショットを引き継いだ新セッションを開始してゲームを継続できるようにするには、ゲーム途中のどんなタイミングのスナップショットからでも、ゲームの状態を正しく復元できるようにしておく必要があります。これは少し言い換えると、(実際にゲーム中の途中参加を許可するかどうかに関わらず)ゲームに途中参加しても同期がずれないような状態を常に維持しておく必要があるということでもあります。
例えば、(通信量の削減などのために)同期に重要な情報をネットワークプロパティを使わずにRPCのみでやり取りしていると、ホストマイグレーション実行時に重要な情報が復元されない可能性があるので要注意です。
Runner.Spawn()
からネットワークオブジェクトを生成する
設定
ホストマイグレーションを有効にするには、NetworkProjectConfig
のEnable Host Migration
にチェックを入れます。Host Migration Snapshot Interval
は、セッションが開始されてからホストマイグレーションが有効になるまでの時間になります。この時間が経過する前にホストがセッションから退出した場合は、ホストマイグレーションを有効にしていてもホストマイグレーションは実行されません。デフォルトは60秒、公式ドキュメントでは(多くの場合は)30秒が推奨されていますが、個人的には5~15秒くらいに短く設定する方が良さそうかなと思っています。
実装
OnHostMigration
ホストがセッションを退出すると、そのセッション内に残された全てのクライアントでINetworkRunnerCallbacks.OnHostMigration()
コールバックが呼ばれます。ここで旧セッションの切断と新セッションへの接続処理を行います。旧セッションからの引き継ぎが必要な情報は全てHostMigrationToken
に含まれているので、これをrunner.StartGame()
の引数に渡します。
public async void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) {
// 旧セッションのNetworkRunnerを停止する
await runner.Shutdown(true, ShutdownReason.HostMigration);
// 新セッションのNetworkRunnerを生成する
runner = Instantiate(networkRunnerPrefab);
runner.ProvideInput = true;
runner.AddCallbacks(this);
// 旧セッションの情報を渡して、新セッションに接続する
await runner.StartGame(new StartGameArgs {
SceneManager = runner.GetComponent<NetworkSceneManagerDefault>(),
HostMigrationToken = hostMigrationToken,
HostMigrationResume = HostMigrationResume,
ConnectionToken = connectionToken
});
}
INetworkRunnerCallbacks.OnShutdown()
でセッション終了時の処理を実装している場合は、旧セッションの切断時に誤った処理が実行されてしまう可能性があるので気を付けておきましょう。
public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) {
+ if (shutdownReason != ShutdownReason.HostMigration) {
// セッション通常終了時の処理
}
}
HostMigrationResume
新セッションに接続すると、ホストのみでHostMigrationResume
に指定したコールバックが呼ばれます。ホストはrunner.GetResumeSnapshotNetworkObjects()
から、旧セッションのスナップショット(全てのネットワークオブジェクトの状態)が取得できるので、これを元にしてネットワークオブジェクトを復元する処理を行います。
旧セッションのスナップショットには、旧セッションのホストが入力権限を持っているネットワークオブジェクト(例えばホストのアバター)なども含まれます。Fusionの仕様で、ホストのプレイヤーのIDは常に「セッションの最大接続人数 - 1」になっているので、これを利用して不要なネットワークオブジェクトが復元されないようにすると良いでしょう。
private static void HostMigrationResume(NetworkRunner runner) {
foreach (var resumeObj in runner.GetResumeSnapshotNetworkObjects()) {
// 旧セッションのホストのアバターは復元しない
if (resumeObj.TryGetBehaviour<PlayerAvatar>(out _)) {
int hostId = runner.SessionInfo.MaxPlayers - 1;
if (resumeObj.InputAuthority.PlayerId == hostId) { continue; }
}
// NetworkTransformやNetworkRigidbodyなどを利用している場合は、
// 親クラスのNetworkPositionRotationからpositionとrotationを取得できる
bool hasNetworkTransform = resumeObj.TryGetBehaviour<NetworkPositionRotation>(out var networkTransform);
var position = (hasNetworkTransform) ? networkTransform.ReadPosition() : Vector3.zero;
var rotation = (hasNetworkTransform) ? networkTransform.ReadRotation() : Quaternion.identity;
// 新セッションのネットワークオブジェクトを生成する
runner.Spawn(resumeObj, position, rotation, onBeforeSpawned: (_, newObj) => {
newObj.CopyStateFrom(resumeObj);
});
}
}
HostMigrationResume
のコールバックを実行している段階では、まだ新ホストには誰も接続していない状況なので、ネットワークオブジェクトの入力権限は(ホスト以外)復元できません。
クライアントの入力権限を復元するには、プレイヤーが参加した際のOnPlayerJoined()
コールバックで、参加したプレイヤーが入力権限を持つネットワークオブジェクトが存在していないか?を調べて、該当するネットワークオブジェクトの入力権限を更新する処理を追加しましょう。
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) {
if (!runner.IsServer) { return; }
int token = new Guid(runner.GetPlayerConnectionToken(player)).GetHashCode();
+ // アバターのリストを取得する
+ var avatarList = FindObjectsOfType<PlayerAvatar>();
+ // ConnectionTokenを比較して、参加したプレイヤーのアバターが生成済みか調べる
+ var newAvatar = avatarList.FirstOrDefault(avatar => avatar.Token == token);
+
+ if (newAvatar != null) {
+ // 既にアバターが生成されていたら、そのアバターの入力権限を更新する
+ newAvatar.Object.AssignInputAuthority(player);
} else {
// 新規のプレイヤーなら、通常通りアバターを生成する
runner.Spawn(playerAvatarPrefab, Vector3.zero, Quaternion.identity, player,
(_, obj) => { obj.GetComponent<PlayerAvatar>().Token = token; }
);
}
}
おわりに
いかがでしたか? ホストマイグレーションは、FusionのHostMode
でオンラインゲームを開発するなら是非おさえておきたい機能の一つですが、仕組みを理解してちゃんと動作させるまで中々苦労することになる機能だと思うので、この記事が何かしらの参考になれば幸いです。
ホストマイグレーションが有効ならばゲームのユーザー体験が向上する大きなメリットがあることは間違いありませんが、ホストマイグレーションのためにホストが必ずPhoton Cloudと通信を行うようになる分だけ通信コストや費用が増えてしまう可能性があるなどのデメリットも0ではないので、その辺りも考慮してホストマイグレーションの導入を決めていきましょう。
Discussion
参考にさせて頂いてます、このProjectデータは公開する予定はごさいますか?
ありがとうございます!
自身のProjectのデータを公開する予定はありませんが、この記事の内容は、以下の公式サンプルプロジェクトから簡単に確認・検証ができるかと思います。