Photon Fusion for Unityでマッチングシステムを実装する～基礎編

# はじめに

オンラインマルチプレイゲームを作る場合、プレイヤー同士のマッチングシステムを用意したい場合がほとんどだと思います。

Photon Fusionには、そのためにMatchmakingAPIというものが用意されています。

本記事では過去記事で紹介した公式サンプルを元に、MatchmakingAPIの基礎的な内容を紹介します。

### 前提記事

Photon Fusionの基本的な解説は以下の記事で行っていますので、そちらを参照下さい。

https://qiita.com/nimushiki/items/d6fe4f0cbe0437677fa8

### 動作確認環境

Windows 11 Home 22H2
Unity 2022.3.2f1
Fusion SDK 1.1.8 F Build 725

# MatchmakingAPIとは

プレイヤー同士をマッチングさせるための情報のやりとりや、同一セッションに接続しゲームを開始するための機能で、以下の３つが主に使われます。

- NetworkRunner.StartGame
    
    基本的なメソッド
    
    プレイヤー同士の情報が通信できるセッションを作成、接続する
    
- NetworkRunner.JoinSessionLobby
    
    セッションを検索するためのロビーを作成、接続する
    
- SessionInfo.UpdateCustomProperties
    
    参加セッションの情報を更新する
    

また重要な用語として以下が挙げられます。

- Game Session
    
    ゲームを行うための部屋のようなもの
    
    複数作成可能
    
    Lobbyの中に存在し、同Lobby内のsessionの情報を取得可能
    
- Lobby
    
    セッションに入る前の状態
    
    複数作成可能
    
    Lobby内に作られている全てのsessionの情報を取得可能
    
    他のプレイヤーの情報は取得できない
    

以下の画像は上記のメソッドや用語がどのような関係にあるかを表した図です。

![MatchmakingAPI概略図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/e98401bc-7b80-8c47-c159-ab1ea280c84f.png)


# MatchmakingAPIの使い方

先の画像からもわかるように、処理の流れは以下のようになっています

## ロビーに接続する

まずはNetworkRunner.JoinSessionLobbyメソッドを使いロビーに接続します。ランダムマッチやフレンドからの招待など、セッションを探す必要がなければこの工程は飛ばして構いません。

```RoomDataManager.cs
//オリジナルのスクリプト
//INetworkRunnerCallbacksを継承

private void Awake()
{
    _runner = GetComponent<NetworkRunner>();
    Connect();
}

private async void Connect()
{
    _runner.AddCallbacks(this);
    var result = await _runner.JoinSessionLobby(SessionLobby.Shared,"MyCustomLobby");

    if (result.Ok) {
        // all good
        Debug.Log("JoinSessionLobby succeeded");
    } else {
        Debug.LogError($"Failed to Start: {result.ShutdownReason}");
    }
}
```

ロビーに接続したあとは目的のセッションを探し、次の工程へ進みます。

セッション情報はロビーにいる限り、セッション情報が更新されるたびにOnSessionListUpdatedコールバックで取得する事ができます。実装については後述します。

## セッションに接続

接続するセッションを特定できたらNetworkRunner.StartGameメソッドでセッションへ参加します。GameMode、セッション名の有無、セッションの有無、これらの組み合わせで接続の挙動が変わってくるため、詳細は[公式マニュアル](https://doc.photonengine.com/ja-jp/fusion/current/manual/matchmaking#rq8uakufac2dld81)を参照ください。

StartGameの主な引数は以下のような意味を持ちます。

- GameMode
    
    ネットワーク・トポロジーを選択する
    
- SessionName
    
    セッションに名前を設定する
    
    セッション名はRegion内でユニークな値
    
- SessionProperties
    
    セッションに関するメタデータ
    
    Session Lobbyから常に見れる
    
- CustomLobbyName
    
    Lobbyに名前をつける
    
    他のLobbyの情報は見えない
    

## セッション・ロビーから抜ける

ゲームを終えた場合や再マッチングを行う場合など、セッションやロビーから抜けたい場面では以下の方法が使えます。

- 別のセッションへ行く
    
    NetworkRunner.StartGame
    
- ロビーに戻る
    
    NetworkRunner.JoinSessionLobby
    
- ローカルに戻る
    
    NetworkRunner.Shutdown
    

# MatchmakingAPIの実装例

マッチングにまつわる機能には、セッション検索やランダムマッチ、プライベートマッチなど色々なものがあります。ここでは代表的な機能を紹介します。

なお、コードの一部は公式サンプルのSocialHubを利用しています。

https://doc.photonengine.com/ja-jp/fusion/current/technical-samples/fusion-social-hub

なおSocialHubの活用＆改造例は過去記事を参考にできます。

https://qiita.com/nimushiki/items/f753196ff9014e080ab6

## ランダムマッチ

公式サンプルではこの方式を多く用いています。

StartGameメソッドにおいて、セッション名を指定しなければ同名ロビー内のランダムなセッションに接続します。

```csharp
await runner.StartGame(new StartGameArgs() {
	GameMode = GameMode.AutoHostOrClient, // or GameMode.Shared
});
```

## セッション情報の追加

遊び方や目的、対戦レギュレーションなどが複数ある場合、適切にマッチングを行うためにその情報をセッションが保持している必要が出てきます。

SessionPropertyはそのためのオプションで、情報を設定するには2つ方法があります。

- StartGame時にSessionPropertyに追加する
- セッション内でSessionInfo.UpdateCustomPropertiesメソッドで変更する

この情報を使うことで、マッチング機能を拡張したり、ランダムマッチに応用することもできます。

サンプルでは、各情報は事前にConnectionDataを経由し他の場所で設定しています。

```ConnectionManager.cs
//公式サンプルSocialHub

public async Task ConnectToRunner(ConnectionData connectionData, Action<NetworkRunner> onInitialized = default, Action<ShutdownReason> onFailed = default)
{

//略

var sessionProperties = new Dictionary<string, SessionProperty>()
	  { { "ID", (int)connectionData.ID },
	      {"Target", connectionData.Target.ToString() },
	       {"Rate", connectionData.Rate.ToString() },                
	  };

//略

var startResult = await connection.Runner.StartGame(new StartGameArgs()
	  {
	      GameMode = gameMode,
	      SessionProperties = sessionProperties,
	      DisableClientSessionCreation = false,
	      Scene = scene,
	      PlayerCount = connectionData.MaxClients,
	      Initialized = onInitialized,
	      SceneManager = sceneManager,
	      ObjectPool = connection.Runner.GetComponent<INetworkObjectPool>(),
	  });
}
```

## セッション一覧の表示や検索

ロビーに参加することでOnSessionListUpdatedコールバックによりセッションリストを取得することができます。このコールバックは、同一ロビー内に存在するセッションが更新される度に受け取ります。

リストには設定したセッション情報のSessionNameやSessionPropertiesが含まれているため、セッションの一覧を表示したりフィルタしたり、セッションに参加することができます。

![実装例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/6336279a-4429-0e50-e928-107e7a4a5eff.png)

```RoomDataManager.cs
//オリジナルのスクリプト
//INetworkRunnerCallbacksを継承

public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList)
{
    //前略
	//初期化やフィルタリングを行う
    
    //リストデータをUIオブジェクトに流す
    for (int i = 0; i < sessionList.Count; i++)
    {
        var session = sessionList[i];
        roomDataList[i].SetData(session.Properties["ID"].PropertyValue.ToString()
            ,session.PlayerCount + "/" + session.MaxPlayers
            ,session.Name);
        roomDataList[i].GetComponent<Button>().onClick.RemoveAllListeners();
        roomDataList[i].GetComponent<Button>().onClick.AddListener(()=>Launch(session.Name));
        roomDataList[i].SetActive(true);
    }
}
```

なおこの機能を実装するにあたって注意することが２つあります。

１つ目は、ロビーに入っている状態もCCU（concurrent user、同時接続数）にカウントされてしまうことです。セッションに参加した状態でNetworkRunnerを生成しロビーにも参加することができますが、この場合一人で2CCUとして計算されます。

無料プランでは20CCU、ひとつ上のプランでも100CCUの制限があるため、適宜接続を切る必要があります。

2つ目はセッション情報のトラフィックです。ロビーにいる間、セッション情報に変更があるたびにOnSessionListUpdatedが呼ばれるため、ユーザー（セッション数、ロビーの人数）が増えるとトラフィックが大きくなってしまいます。そのためSessionPropertyはなるべく小さくなるようにしたほうが良いでしょう。

また、そもそもマッチングすることのない母集団、例えばプライベートマッチをしたい人、ランクマッチをしたい人、特殊ルールでランダムマッチしたい人がいた場合、予めCustomLobbyNameによって分けておく方が良いかもしれません。