Photon Fusion for Unityのラグ補償ヒットボックスを実装＆テストする

# はじめに

Photon Fusionの強力な新機能として、ラグ補償ヒットボックスというものがあります。その名の通り、オンラインゲームでは必ず存在する通信の遅延（＝ラグ）の影響を軽減するためのヒットボックスです。
しかし実装に当たっては、Unity標準のColliderとは異なる部分もあり、また実際に効果があるのか分かりにくい機能です。そこで、実装を行って実際にどのような挙動になるか調査をしました。
本記事では、Photon Fusionに備わるラグ補償ヒットボックスの実装例と、その機能の検証結果を紹介します。

### 前提記事

Photon Fusionの基本的な内容やラグ補償ヒットボックスの概要の解説は別記事で行っています。また、検証にはマルチピアモードを利用していますので、詳しく知りたい方は以下のリンクからご参照下さい。

[Photon Fusion for Unityの導入手順とPUN2との機能比較](https://qiita.com/nimushiki/items/d6fe4f0cbe0437677fa8)
[Photon Fusion for Unityの新機能　描画補間、ラグ補償、同期範囲設定](https://qiita.com/nimushiki/items/c82763817a8c7c4f2278)
[Photon Fusion for Unityでマルチピアモードを利用したデバッグの始め方](https://qiita.com/nimushiki/items/d683b3a17a6431f12b01)

### 動作確認環境

Windows 10 Home 21H2
Unity 2020.3.27f1
Fusion SDK 1.1.1 F Build 512

# ラグ補償ヒットボックスとは

オンラインゲームにおいて通信の遅延は必ず存在するため、各クライアント間で見えているゲーム内時間が異なってきます。ラグ補償ヒットボックスはそれを解消するための仕組みを備えたヒットボックスです。
詳しい解説は以下の記事を参照ください。

[Photon Fusion for Unityの新機能　描画補間、ラグ補償、同期範囲設定](https://qiita.com/nimushiki/items/c82763817a8c7c4f2278)

主に以下の要素を用いて実装を行います。
- Hitboxコンポーネント
  ラグ補償ヒットボックスを提供する。UnityのColliderのようなもの
- HitboxRootコンポーネント
  自身以下のHitboxをまとめて管理する。必須。
- HitboxManagerコンポーネント
  デバッグ用データを取得しインスペクターに表示する  
- Runner.LagCompensation.Raycast()
  Hitboxを検出するための専用レイキャスト

# 実装
3Dフィールドを歩き回り、ボタンクリックで弾を飛ばして相手に当てる、というサンプルを元に解説します。
HitboxはUnity標準のコライダーのような衝突検出はできないため、専用のレイキャストで判定を取る必要があります。
プレイヤーから放たれた弾は、飛んでいる間常に弾速から1フレームで進む長さを計算し、その長さ分のレイを飛ばし続けています。そのレイによってHitboxを検出し、衝突の判定を処理します。
また専用レイキャストの判定処理は重いため、レイヤーを利用して処理のフィルタリングをします。
実装ポイントは以下の通りです。
- プレイヤーのHitbox、プレイヤーのCollider、障害物のColliderでレイヤーを分けておく
- HitboxのルートにはHitboxRootコンポーネントをアタッチする
- 弾は1フレームで進む分のレイを飛ばし続ける
- Hitboxを検出したら着弾処理

![キャラクターコライダーのレイヤー設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/ddc90e87-df07-795d-8e93-12c3aed5887a.png)
![キャラクターHitboxのレイヤー設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/dc464a21-ec7c-7862-2b94-351921b75708.png)
![障害物のレイヤー設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/cb655561-09b3-ab99-032f-8e14fda048c5.png)

弾にアタッチするスクリプト

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;

public class ball : NetworkBehaviour
{
    [Networked] private TickTimer life { get; set; }
    [Networked] public float lifeTime { get; set; }
    [Networked] public float Speed { get; set; }

    public LayerMask hitMask;

    //生成したタイミングで実行
	public void Init()
    {
        life = TickTimer.CreateFromSeconds(Runner, lifeTime);
    }

    //Fhoton用のUpdate
    public override void FixedUpdateNetwork()
    {
        if (life.Expired(Runner))
        {
            Runner.Despawn(Object);
        }
        else
        {
            transform.position += Speed * transform.forward * Runner.DeltaTime;
            HitAction();
        }
    }

    private void HitAction()
    {
        Runner.LagCompensation.Raycast(
            transform.position + transform.forward * 0.15f,
            transform.forward,
            Speed * Runner.DeltaTime, //1フレームに進む距離分のレイを飛ばす
            Object.InputAuthority,
            out var hit,
            hitMask.value, //判定を行うレイヤーを制限する
            HitOptions.None);

        Debug.DrawRay(
            transform.position + transform.forward * 0.15f,
            transform.forward * Speed * Runner.DeltaTime * 5,
            Color.red, Runner.DeltaTime);

	      //着弾処理 
        if (hit.GameObject != null)
        {
            var target = hit.GameObject.GetComponent<ICanTakeDamage>();
            NetworkObject nobj = hit.GameObject.GetComponent<NetworkObject>();
            Debug.Log("[nimu] " + nobj.InputAuthority + " dameged by " + nobj.Runner.Simulation.LocalPlayer + " at " + nobj.Runner.Simulation.Tick);
            if (target != null) {
                target.ApplyDamage(transform.forward.normalized ,1, Object.InputAuthority);
                Runner.Despawn(Object);
            }
        }
    }
}
```

# 検証

## マルチピアモードについて

マルチピアモードは、一つのUnityエディタ内で複数のクライアント（≒ピア）を動かすための仕組みです。
ラグ補償ヒットボックスには直接関係ありませんが、ピア間の遅延を設定することができるため検証に利用しています。
詳しく知りたい方は以下の記事で解説してありますのでご参照ください。

[Photon Fusion for Unityでマルチピアモードを利用したデバッグの始め方](https://qiita.com/nimushiki/items/d683b3a17a6431f12b01)

## 検証の環境について

検証では以下のように設定をしました。検証のため、レイヤーは推奨される設計とは異なる設定になっていますのでご注意ください。

- カメラを各ピアに設置して画面を分割、CullingMaskで表示をコントロール
- プレイヤーのレイヤーを、各ピアで分ける（Player0、Player1
- Network Conditionsで遅延を最大にする（0.5sec）
- 当たり判定を可視化する半透明のキューブHitEffectを実装

![画面分活用のカメラ設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/994f4fb9-8b6e-1b98-c4ad-0bbcecc9bb10.png)
![プレイヤー0のレイヤー設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/765a3534-17dc-0031-20ae-8747cab4e297.png)
![プレイヤー1のレイヤー設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/95f8d7c0-b012-1389-af2b-e29104dc6a79.png)
![遅延の設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/8928a723-7ff5-559f-f543-d90eb0d18977.png)

## 検証動画の解説

検証結果の動画の見方を解説します。

- マルチピアモードにより、2プレイヤー分のカメラを表示
    - 左側はPlayer0、右側はPlayer1の視点
- Sceneビューには右のプレイヤーを拡大表示
- 黄色い枠は当たり判定のHitbox（ラグ補償ヒットボックス）
    - 弾が当たった瞬間、赤い半透明のHitEffectを表示
- プレイヤーが可能な操作は移動と弾の発射
    - 入力は両プレイヤー同時に反映される
- 移動開始時とヒット時にログを出力するように設定
- [Tick:xxxx]の表記はゲーム内経過時間のこと（1Tick≒1/60sec）

![動画の見方](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/fd6651c1-5222-10f6-b9d8-7562fa52e98d.png)

動画はPlayer0から放たれた弾をPlayer1が避ける状況を想定した、ラグ補償ヒットボックスの検証結果です。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/deac4097-083f-dcd4-0c43-793e7553948c.gif)

移動操作の入力は同時に行われており、Player0とPlayer1はそれぞれのプレイヤーから見れば、どちらも同時に移動を始めている事がわかります。しかしマルチピアモードによって遅延を0.5secに設定しているため、相手は少し遅れて動き出す状態になっています。
この状況でPlayer0から見た場合、Player1は遅延によりまだ移動を始めていないため弾が当たっている状態です。Player1はPlayer0から放たれた弾を直前に回避しているにもかかわらず衝突判定が出現しています。
ゲームの体験として、当たっているように見えるのに当たっていない、というのは非常にストレスであり、避けたい状態です。つまりここで優先すべきは当てる側のPlayer0です。そのためPlayer0のゲーム内状況をもとに当たり判定を処理します。
この例では弾速が非常に遅い、見やすい、3人称視点である、遅延0.5sec=ping500というそもそも考慮しなくてもいいレベルの遅延という、被弾側も状況を認識しやすい設定ため優先度が分かりにくいかもしれません。FPSでスナイパーライフルを使用した状況を考えれば、圧倒的に当てる側が優先であると感じられるでしょう。
当てる側に判定の主導権があるため、一見チートに弱いように思えます。しかしラグ補償ヒットボックスは位置情報履歴を持っており、当たる側が判定を検証することができるため、その心配はありません。
このように、ラグ状況下でも当てる側の表示に準拠した信頼性の高い判定を行うのが、ラグ補償ヒットボックスです。

## 検証動画の注意点

一つ注意点として、動画には想定外の挙動が含まれています。
弾がPlayer0に到達した時点でPlayer1のカメラにHitEffectが一瞬だけ表示され、その後再度表示されていますが、ここは最初から表示されないか表示されっぱなしのどちらかが正しいように思えます。
この点については開発元に確認中ですので、判明次第追記を行います。

# 補足
ラグ補償ヒットボックスは判定をレイキャストに依存する関係から大きな当たり判定は苦手になります。そういった場合はColliderでまず大雑把な判定を取得し、そこからレイキャストを利用する、といった工夫が必要になるでしょう。