Photon Fusion for Unityのオブジェクトインタレスト機能の基礎と注意点

# はじめに

オンラインマルチプレイヤーゲームにおいて、プレイヤー以外にも同期する必要のあるオブジェクトはアイテムやNPC、環境オブジェクトなど多くあります。

その一例として、前回の記事「[Photon Fusion for UnityのサンプルSocial HubとProjectilesをあわせて物理シューターを作る　その２](https://qiita.com/nimushiki/items/02f3376e2ec547fb9719)」では、大量のコイン型物理オブジェクトを扱う時に必要な物理設定について紹介しました。

しかし当然ですが同期オブジェクトが多いほど通信量が増え、動作遅延の一因となります。その対策の一つとして、Photon Fusionにはインタレストマネジメントという仕組みがあります。

本記事ではそのインタレストマネジメントの使い方や実装例について紹介します。

### 前提記事

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

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

### 動作確認環境

Windows 11 Home 22H2
Unity 2022.3.2f1
Fusion SDK 1.1.6 F Build 696

# インタレストマネジメントとは

インタレストマネジメントとは、同期の可否をコントロールするデータカリング機能です。

不要なオブジェクトの同期を減らすことによるネットワークのトラフィック削減や、任意のデータへのアクセス制限（個人やチーム内専用の情報など）に使うことができます。

## レプリケーションシステム

前提の知識としてレプリケーションシステムがあります。以下の過去記事や公式マニュアルでも同様の解説があります。

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

https://doc.photonengine.com/ja-jp/fusion/current/manual/interest-management

Photon Fusionではスナップショットというデータ群を通信することで同期をしています。

レプリケーションシステムは、そのスナップショットのレプリカ（複製）をどう作るか、またそのレプリカをどうマージ（ゲームプレイに反映）していくか、という一連の処理を指します。同期オブジェクトが多ければ多いほどそのスナップショットのデータ、つまり通信量が増えてしまうため、不要なデータを間引くための仕組みとなります。

そのレプリカの作り方については、レプリケーションモードの項目で以下の2種類から選択可能です。

### Delta Snapshots（デルタスナップショット）
- 完全なスナップショットを転送する
- プレイヤーや同期オブジェクト数が少ない、テンポが早いゲームに向く
    - 格闘ゲーム、チームFPS
### Eventual Consistency（結果整合性）
- 自分の周辺や特定のオブジェクトに限定したスナップショットを転送する
    - 専用の設定（関心領域など）が追加される
- 範囲が広い、同期オブジェクトが多い、仕組みが複雑なゲームに向く
    - MMO、サバイバル、バトルロイヤル
- 各プレイヤーの状態の同期権限の管理方式
    - Server Auth（サーバー型、プレイヤーホスト型の場合）
        - 単一のマシンが行うため、正確
    - Client Auth（共有モードの場合）
        - 各自で行うため、同期時に競合が起きる可能性がある

## オブジェクトインタレスト

インタレストマネジメントの具体的なデータカリングの仕組みの1つ目は、オブジェクトインタレストです。オプションでは誰がそのオブジェクトを同期（情報の取得）をするか、という設定ができます。それぞれ以下のような特徴があります。

- AreaOfInterest
    - AOIが設定されたオブジェクトのその範囲に入った場合に同期
    - プレイヤー（NetworkCharacterController）や大抵のオブジェクト
- AllPlayers
    - すべてのプレイヤーが同期
    - レイドボス、GM、NPC
    - 別名　グローバルオブジェクト
- ExplicitPlayers
    - 専用フラグを立てられたプレイヤーのみが同期
    - イベントキャラ、チーム内情報
    - 別名　カスタムインタレストマネジメント

なお、どのオプションが最適かは対象のオブジェクトの性質やゲームの構造に依るため、上記はあくまで一例です。

## インタレストグループ

もう一つのデータカリングの仕組みはインタレストグループです。

これはInterest Groupsを設定した任意のネットワークプロパティー毎に、同期の可否を指定することができる仕組みです。オブジェクト自体は同期したいが特定のプロパティは同期したくない（キャラステータス、スコア、チーム状況など）場合に活用できます。

なおデータのカリングはオブジェクトインタレストが優先されます。つまりオブジェクトそのものが同期されていない場合は、ネットワークプロパティーの同期も行われません。

# 設定方法

## Network Project Configでの設定

まずはレプリケーションモードの設定です。

Network Project Config > Replication Modeの項目でEventual Consistencyモードにすると、すぐ下のObject Interestオプションが有効化されます。

![Replication Modeの項目](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/f67990b1-322f-90f8-e169-33fa865d07f8.png)

さらにそのオプションをオンにすると、NetworkObjectコンポーネントにInterest Management Settingsの項目が出現します。

![Interest Management Settingsの項](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/7e6993fa-c915-3a79-4034-92bb77679b12.png)


## 対象のNetworkObjectでの設定

次にオブジェクトインタレストの設定をします。

設定を行いたいオブジェクトのNetworkObjectコンポーネントのInterest Management Settingsで、用途にあったObject Interestを選びます。

AreaOfInterestを選択した場合はAOI Position Sourceを設定します。これは自身が他者（プレイヤー）のAOI範囲に入っているかどうかの判定をするための基準点で、主に自分自身を指定します。

## AOI範囲を設定

プレイヤーに対してAOIの範囲を持たせるにはRunner.AddPlayerAreaOfInterestメソッドを使います。

メソッドでは対象のプレイヤー・位置・サイズを指定し、中心位置は自身から離れてても問題ありません。

```csharp
public override void FixedUpdateNetwork()
{
    if (Object.IsProxy == true)
        return;

    if (Runner.IsLastTick == true && Object.HasStateAuthority == true)
    {
        basePosition = transform.position;
        baseDirection = transform.forward;

        Runner.AddPlayerAreaOfInterest(Object.InputAuthority
            , basePosition 
            , centerExtent);
        Runner.AddPlayerAreaOfInterest(Object.InputAuthority
            , basePosition + extent1stOffset
            , extent1st);
        Runner.AddPlayerAreaOfInterest(Object.InputAuthority
            , basePosition + extent2ndOffset
            , extent2nd);
    }
}
```

## Interest Groupsの設定

Interest Groupsの設定をするには、まず同期オブジェクト（NetworkObjectコンポーネントがついたオブジェクト）を用意します。

そのオブジェクト以下にあるスクリプト中で、Networkedアトリビュートのオプションを使い任意のネットワークプロパティーにインタレストグループを割り当てます。

[Networked(group: "hoge")]

次に、同じくオブジェクト以下のスクリプトで、対象のプレイヤーがどのグループの値を同期するかを設定します。

Object.SetInterestGroup()またはRunner.SetInterestGroup()

もう一つの設定方法として、NetworkObjectコンポーネントのインスペクターにDefault Interest Groupsというのがあります。ここにコード中で割り当てたインタレストグループを追加することで、全てのプレイヤーに対し該当のグループを同期させる機能があります。

以上で設定は終わりです。

## Interest Groupsの注意点

Default Interest GroupsのデフォルトはList is Emptyですが、その場合すべてのグループを同期してしまうため、空でいいので1つは追加しておく必要があります。

SetInterestGroupメソッドやDefault Interest Groupsで同期させたグループについて、恐らくですが実行中に解除する方法はなさそうです。

また同一NetworkObject内ならスクリプトをまたいでも有効で、さらにグループ名が同じならプロパティ名が違っても有効なため、注意が必要です。

なおネットワークトポロジーがホストモードの場合、ホストは自身が同期の基準となるためすべての情報を取得している状態になります。

# 重要な現在の仕様

現在、AOI範囲に関する機能は最低限となっており、主に以下の３つの点について注意が必要です。

１つ目は範囲についてです。

Runner.AddPlayerAreaOfInterestで指定した値の2倍の長さを一辺とした正方形を底面にもつ、Y方向無限の四角柱形状に限定されます。そのため、縦スクロール型シューティングのようなXY平面上で動作しY方向に長いゲームの場合、AOI範囲を有効に使うことができません。
なおAOI範囲を描画するオプションはなく、参考画像中ではGizmos.DrawWireCubeメソッドを使い描画しています。

２つ目はAOI範囲の回転です。

Runner.AddPlayerAreaOfInterestを実行したコンポーネントや対象になったプレイヤーのTransformに関わらず、AOI範囲は回転することはなく、角度を指定することもできません。

以下の画像はカメラにAOI範囲を追従させた場合、Z方向を見た時とZX方向を見た時の様子です。

![Z方向を見た時](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d03497fb-aeeb-3537-e5b8-b2e72c79bb6c.png)

![ZX方向を見た時](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d0fded33-5df8-f31c-d7ae-7b005dbcfb6b.png)


３つ目は同期の停止や再開時の挙動です。

対象のオブジェクトがAOI範囲外に出て同期が停止した時、ゲームシーン内ではそのままの状態が維持されています。つまりAOI範囲外で移動し再度AOI範囲内に入った時、ワープするような挙動になります。そのため、視界内でAOI範囲の出入りがある場合は注意が必要です。

# 実装例

## AOIによるカリング

AOIの特に有効な使い方がカリングです。

カメラに映る範囲をAOIで覆う事により、オクルージョンカリング機能のように視界外のオブジェクトの同期を自動的に停止させて通信データの削減をすることができます。

同期オブジェクトが多い場合はパフォーマンスの改善に有効な手段の一つとなります。

https://doc.photonengine.com/ja-jp/fusion/current/game-samples/fusion-br/performance-optimizations

### 見下ろし型のゲーム

AOI範囲の中心は基本的にプレイヤーキャラクターになります。

前述した注意すべき仕様はほぼ影響がないため、プレイヤーを中心としたAOIを設定するだけで大きな問題もなくカリングの効果を得ることができます。

### FPS,TPS型のゲーム

前述した現在の仕様があるため、FPSやTPSのようにカメラを自由に動かせるタイプの場合は工夫が必要です。カメラの視界を完全に覆うようにAOIを設置する必要がありますが、AOI範囲は回転しないため、斜め方向を向いた時に非常に大きな隙間が発生します。

そのため画像のように大きめのAOI範囲を設定したりお互いの範囲を重複させるような配置にする必要があります。

![Z方向を見た時](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d03497fb-aeeb-3537-e5b8-b2e72c79bb6c.png)

```csharp
public override void FixedUpdateNetwork()
{
    if (Object.IsProxy == true)
        return;

    if (Runner.IsLastTick == true && Object.HasStateAuthority == true)
    {
        basePosition = transform.position;
        baseDirection = transform.forward;

        Runner.AddPlayerAreaOfInterest(Object.InputAuthority
            , basePosition 
            , centerExtent);
        Runner.AddPlayerAreaOfInterest(Object.InputAuthority
            , basePosition + extent1stOffset
            , extent1st);
        Runner.AddPlayerAreaOfInterest(Object.InputAuthority
            , basePosition + extent2ndOffset
            , extent2nd);
    }
}
```

さらなる最適化として、以下のような項目が挙げられます。

- AOI範囲の数や配置、回転時の軌道などを調整
- 視界を基準とした事前計算（物陰の物体）
- カメラではなくフィールドにAOIを設置
- 範囲外オブジェクトからの発射体を手動で追加

なお繰り返しになりますが、前述の通りAOIの機能については拡張が予想されるため、現在の仕様を前提とした過度な最適化はしないほうが良いかもしれません。

## アクセス制限

個人やチーム内など特定の範囲でしか知る必要のない情報については、主にExplicitPlayersオプションやInterest Groupsを使ってアクセス制限を設定する事ができます。

ゲームの見た目上の差異がでる実装ではないのと、データ量や処理負荷の軽減という点においてはAOIカリングほどインパクトはありません。しかし相手の情報（手持ちアイテムや残り体力など）を盗むといったチートには効果が期待ができそうです。

## 公式サンプルBR200の実装例

BR200は、Unity社とExitGames社の2社が共同開発した、200人同時参加可能なバトルロイヤルゲームの公式サンプルプロジェクトです。

200人同時対戦実現のためのパフォーマンスチューニングとして、上記以外の実装例がありますのでそれを軽く紹介します。

https://doc.photonengine.com/ja-jp/fusion/current/game-samples/fusion-br/overview

https://doc.photonengine.com/ja-jp/fusion/current/game-samples/fusion-br/performance-optimizations

- StaticNetworkTransform
    
    NetworkTransformの代わりに使い、スポーン時に１度だけ位置と回転を同期します。
    
    宝箱など移動する必要のないオブジェクトに最適です。
    
- NetworkAreaOfInterestProxy
    
    位置と回転を同期しないがAOIによるカリングはしたいオブジェクト、主にプレイヤーの武器など状況によって出し入れするようなものに使います。
    
    これはプレイヤー同様にカリングをしたいが、位置や回転はプレイヤー依存のためです。
    
- NetworkCulling
    
    一定時間以上更新（同期）のない場合にフラグを立てるユーティリティーです。
    
    AOI範囲外で移動して再度範囲内に入った場合など、再同期時はワープするような挙動になります。実装例ではそれが画面に映らないよう、プレイヤーにアタッチしてフラグによりレンダーなどの処理を停止させています。