Chapter 18

🌶 その他の機能と実践Tips

o8que
o8que
2021.03.13に更新

開発用GUIコンポーネント

PUN2には、現在のネットワーク状態を可視化するPhotonStatsGuiコンポーネント、回線が不安定なプレイヤーの動作をテストしたりするPhotonLagSimulationGuiコンポーネントがあらかじめ用意されています。Stats(レンダリング統計)やプロファイラーでは確認できないような、ネットワークに起因する問題のデバッグや最適化に役立つでしょう。

https://doc.photonengine.com/ja-jp/pun/current/troubleshooting/photon-stats-gui
https://doc.photonengine.com/ja-jp/pun/current/troubleshooting/photon-lag-simulation-gui

ブラウザのタブの非アクティブ検出(WebGLビルド時)

WebGLビルドを実行しているブラウザのタブが非アクティブになると、ゲームのフレームレートは著しく低下します。これがオンラインゲームだった場合には、他プレイヤー側で致命的な同期ズレが発生してしまうなどの悪影響を及ぼす可能性があります。

この問題を回避するお手軽な方法は、フレームレートの低下を検出してルームから退出する、またはサーバーから切断する処理を行うことです。フレームレートはTime.deltaTimeから計算できますが、一瞬だけ負荷が高い処理が発生した場合などに誤検出してしまうことがあるため、Time.deltaTimeが滑らかに補正されているTime.smoothDeltaTimeを使いましょう。

LeaveRoomWhenInactive.cs
using Photon.Pun;
using UnityEngine;

public class LeaveRoomWhenInactive : MonoBehaviour
{
    private void Update() {
        // ルームに参加している間、平均フレームレートが著しく低下したら、ルームから退出する
        if (PhotonNetwork.InRoom) {
            if (Time.smoothDeltaTime > 0.4f) {
                PhotonNetwork.LeaveRoom();
            }
        }
    }
}

公式ドキュメントでも、上記以外の様々な方法が紹介されているので参考にすると良いでしょう。

https://doc.photonengine.com/ja-jp/pun/current/demos-and-tutorials/webgl-tabsinbackground

ネットワークカリング

基本的にオブジェクト同期やRPCは同じルームへ参加しているプレイヤー全員と同期が行われるため、必ずしも全員に送信する必要がないデータを同期する場合には、無駄な通信が発生してしまうことになります。これは、サーバー側のロジックで適切なプレイヤーのみに送信できれば理想ですが、Photon Cloudを使う場合には、サーバー側の処理を入れることができないので、その簡易的な代替としてインタレストグループ(Interest Groups)という機能を利用できます。

インタレストグループの受信設定

プレイヤーはPhotonNetwork.SetInterestGroups()で、受信対象のグループの追加と削除を行うことができます。グループIDはbyte型の値で、1~255の範囲で複数選択できます。グループID0は、ルームへ参加しているプレイヤー全員が自動的に受信対象として追加されるデフォルトのグループで、設定を変更(削除)することはできません。

PhotonNetwork.SetInterestGroups(
    new byte[] { 1, 2, 3 }, // グループID1~3を受信対象から削除する
    new byte[] { 4, 5, 6 } // グループID4~6を受信対象に追加する
);

引数をnullにすると「グループ指定無し」、Array.Empty<byte>()にすると「全てのグループを指定」になります。内部的に引数のグループIDの配列は、HashSet<byte>型コレクションで整理されてから送信されるので、仮にグループIDが重複しても問題はありません。追加と削除の配列の間でグループIDが重複した場合は、追加が優先されます。

// 全てのグループを受信対象から削除する
PhotonNetwork.SetInterestGroups(Array.Empty<byte>(), null);
// グループID1~3を受信対象に追加し、それ以外の全てのグループを受信対象から削除する
PhotonNetwork.SetInterestGroups(Array.Empty<byte>(), new byte[] { 1, 2, 3 });
// グループID4~6を受信対象に追加し、グループID1~3を受信対象から削除する
PhotonNetwork.SetInterestGroups(new byte[] { 1, 2, 3, 4, 5 }, new byte[] { 4, 5, 6, 4, 5, 6 });

インタレストグループの送信設定

オブジェクト同期やRPCは、送信される(送信データを作成される)時にPhotonView.Groupで指定されているグループIDが送信対象に設定されます。グループID0を指定すると、ルームへ参加しているプレイヤー全員に送信することになります。(PhotonView.Groupは、送信時にグループIDを設定するためだけの値で、変更することで通信は発生しません)

// グループID1を受信対象に追加しているプレイヤーのみで、オブジェクト同期が行われる
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
    if (stream.IsWriting) {
        photonView.Group = 1;
        stream.SendNext(transform.position);
    } else {
        transform.position = (Vector3)stream.ReceiveNext();
    }
}
// グループID1を受信対象に追加しているプレイヤーのみで、RPCが実行される
public void FireProjectile(float angle) {
    photonView.Group = 1;
    photonView.RPC(nameof(RPCFireProjectile), RpcTarget.All, angle);
}

ネットワークオブジェクトを生成する際にグループIDを指定することもできます。グループIDを指定して生成されるインスタンスは、後からインタレストグループの受信設定を変更しても、自動的に生成・削除されることはありません。

// グループID1を受信対象に追加しているプレイヤーのみで、ネットワークオブジェクトのインスタンスが生成される
PhotonNetwork.Instantiate("NetworkedObject", Vector3.zero, Quaternion.identity, 1);

実装例

1km四方のマップを、100m四方のエリアに分割して、それぞれにグループID1~100を割り当てます。プレイヤーは座標を更新するたびに、自身がいるエリアのグループIDを計算して、変更があれば更新するようにします。これで、同じエリアにいるプレイヤー同士のみが座標の更新を同期するようになります。

using Photon.Pun;
using UnityEngine;

public class GamePlayer : MonoBehaviourPunCallbacks
{
    private void Update() {
        if (photonView.IsMine) {
            var direction = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")).normalized;
            var dv = 6f * Time.deltaTime * direction;
            transform.Translate(dv);

            var areaPosition = new Vector2Int(
                Mathf.Clamp(Mathf.FloorToInt((transform.position.x + 500f) / 100f), 0, 9),
                Mathf.Clamp(Mathf.FloorToInt((transform.position.y + 500f) / 100f), 0, 9)
            );
            byte currentGroup = (byte)(areaPosition.x + 10 * areaPosition.y + 1);
            if (currentGroup != photonView.Group) {
                PhotonNetwork.SetInterestGroups(
                    new byte[] { photonView.Group },
                    new byte[] { currentGroup }
                );
                photonView.Group = currentGroup;
            }
        }
    }
}

これはサンプル用の単純な例です。実用的には、エリアの境界で急に同期が途切れないように周囲のエリアのグループIDを受信対象に含めたり、もっとちゃんとした空間分割でグループIDを割り当てたりする必要があるでしょう。さらにその場合には、最大255グループという数が制限になってしまうこともあるため、かなり大変な実装になることが予想されます。