MRのPvPシューティングゲームをつくった

# MRのPvPシューティングゲームをつくった

MetaQuest 3 を装着した人間同士でPvPをするシューティングゲームを作ったので、役立ちそうな情報をまとめてみます！

※全て2025/01時点の情報です。

## どういうゲーム？
https://x.com/0osuu/status/1884172077592379702

## 目次
・[環境](#環境)
・[互いの位置を知る方法](#互いの位置を知る方法)
・[壁越しにオブジェクトを見えなくする](#壁越しにオブジェクトを見えなくする)
・[弾を当てた判定について](#弾を当てた判定について)
・[リアルタイムで当たり判定をつける](#リアルタイムで当たり判定をつける)
・[投げ物などの物理について](#投げ物などの物理について)


## 環境
Unity 6000.0.30f1 (BPR)
Meta SDK v71
MetaQuest 3 v72
Photon Fusion 2 ホストモード

## 互いの位置を知る方法
PvPゲームなので、互いのデバイス同士で位置を認識する必要があります。
これはMeta SDKのBuilding Blocksに含まれている、「Colocation」を使って実現できます。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1556297/2bf14548-8466-72e1-c811-d07b68ff42fb.png)

ColocationはNetcodeForGameObjectsとPhotonFusionに対応しています。
Building Blocksには含まれていないものの、Metaが公開している[PUN2のサンプル](https://developers.meta.com/horizon/documentation/unity/unity-ssa-sf/)もあります。

Colocationを使うにはプロジェクトの設定が必要です。
以下リンク先の「開発者ダッシュボードでアプリを登録し、プラットフォームの設定を更新する」手順を行ってください。

https://developers.meta.com/horizon/documentation/unity/unity-ssa-sf/#register-app-on-developer-dashboard-and-update-platform-settings

完了後、Colocationは空のシーンに置くだけで、パススルーの設定、セッション建てや参加処理をしてくれます。Building Blocksの「PlayerNameTag」や「Networked Grabbable Objects」などを追加で置くだけで、それぞれのHMD位置が正しく同期されていることが確認できます。

↓ColocationとPlayerNameTagを置いた状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1556297/c9355260-e9a7-bbdf-4490-43ea671d94d7.png)

#### Colocationで正しく位置を合わせるポイント
例えば「PlayerNameTag」を付けた場合には、パススルーで見えるHMDの真上にTagが存在するべきですが、全く異なる位置に出現してしまうことがあります。
このような位置ずれ時には以下を確認してみてください。

・部屋を建てた(ホスト)Questでスペースの設定(部屋の3Dスキャンみたいなやつ)がされていること。
・ホスト・ゲスト共に境界線の設定(床に線を引くやつ)がされていること。
・ホスト・ゲスト共に部屋を正しく認識できていること。
Questを壁向きに置いたままなどではQuestは自分の位置を認識できません。
体感ですが、Questに部屋を見渡させるようにゆっくり動かすと素早く位置合わせがされる印象です。

### ネットワークエンジン周り

Colocationでは、NGO,Fusion2(Host,Shared),Pun2の3つを試しましたが、Fusion2 HostModeが最も綺麗に動いたので私はこれを選択しました。

※NGOに関しては検証が雑だったかもしれません。ちゃんと動くかも！！！
※Fusion2 SharedではTick Rateを[変更できません。](https://doc.photonengine.com/ja-jp/fusion/current/manual/network-project-config)
自前で実装すれば恐らく上記以外の物でも動かせると思います。

#### ラグについて
以下、Photon Fusion2 HostModeでの動作について記載します。
ホストモードではP2P通信が可能であることと、ほぼ全ての通信がLAN内で完結するため、体感できるほどのラグは起こりません。

グレネードの爆発タイミングなどは完全に一致させるために、[TickTimer](https://doc.photonengine.com/ja-jp/fusion/current/manual/fusion-types/ticktimer)を使っています。

~~~
[Networked] TickTimer actionTimer { get ; set; }

// FixedUpdateNetwork()のが適切かも...?
void Update()
{
    if (actionTimer.Expired(Runner))
    {
        // アクション 爆発など
        Action();

        actionTimer = TickTimer.None;
    }
}

public void AfterSpawnd()
{
    // アクションタイマー 3秒後に爆発させる場合
    actionTimer = TickTimer.CreateFromSeconds(Runner, 3.0f);
}

~~~


#### インターネット接続が必要な場面
ルーム参加時とColocationの位置合わせ処理にのみインターネット接続を要します。
概ね3Mbps以上の速度が確保できていれば問題なかった印象です。

## 弾を当てた判定について
弾や投げ物は壁で防がれなければなりません。
これには「スペースの設定」でスキャンした部屋の3Dデータを使います。

私の実装時使っていたOVR Scene Anchorが現時点で非推奨になってしまったので、以下推測ですが、Building Blocksの「Scene Mesh」を使うことでスキャンした部屋のメッシュを呼び出せると思います。　

ここで読み込んだ部屋メッシュをMeshColliderとして使用します。
Questで一度にスキャンできる広さには限界があるため、広いスペースや複数の部屋を跨ぐ場合には、1エリアずつのスキャンが必要です。
この際、エリア間には必ず壁メッシュが出来てしまいます。
ゲーム的に、この部分を「通れるようにするなら」ゲーム内でこの壁を取り除く作業が必要となります。
そのため、部屋メッシュはホスト側で読み込みを行い、ホストがメッシュ編集をし終えた段階でゲストへ共有します。

本来ホストモードでは、当たり判定や物理挙動等は全てホスト側で行われることが期待されますが、後述するリアルタイムでの当たり判定等の要素により、今回は弾を撃ったクライアントで「当たった」かどうかを判定する形にしています。

## 壁越しにオブジェクトを見えなくする
壁の向こう側にいる相手が手にしている銃などの3Dモデルは[DepthApi](https://developers.meta.com/horizon/documentation/unity/unity-depthapi-overview/?locale=ja_JP)によって見えなくすることが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1556297/fb5270b9-3b54-c096-66ed-016cac09d89b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1556297/c520b3f4-1c28-2a9c-f530-d203b12c8fdb.png)

青い部分が壁裏に隠れると消えているのがわかります。
使用には少し条件があるので、詳細は[こちら](https://developers.meta.com/horizon/documentation/unity/unity-depthapi-occlusions-get-started)を参考にしてみてください。





## リアルタイムで当たり判定をつける
※Beta版と警告されています。今後破壊的な変更があるかもしれません。
※DepthApiが必要

https://x.com/0osuu/status/1866424801218490851/video/1

MetaSDK v71で追加された[EnvironmentRaycast](https://developers.meta.com/horizon/reference/mruk/v71/class_meta_x_r_environment_raycast_manager/)によって、現実の物体にヒットするRaycastを行うことが可能になりました。
これを使うことで、ドアや窓の開閉によって当たり判定を変化させることが出来ます。

現時点ではRaycastを行えるだけで、パーティクルのCollisionには使えません。
今回はCollision用のコライダーをいくつかプールし、Rayのヒット地点にコライダーを置いてパーティクルを消すようにしました。

ドアを閉めた直後などはRayが貫通してしまうことがありますが、数秒待てば正しくヒットするようになります。
厳密な当たり判定には使えないですが、カジュアルなゲームでの実験的機能としては非常に面白い機能です。

#### EnvironmentRaycastの使い方
1.Meta MR Utility Kit -> Samples -> Environment Panel Placement(Beta)をImportします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1556297/e494cb90-5908-a0a4-2483-902fe40eca7b.png)

2.シーンにEnvironmentRaycastManagerを配置します。
3.スクリプト上で以下を呼び出すと、hitにraycast結果が格納されます。
```
EnvironmentRaycastManager.Raycast(ray, out EnvironmentRaycastHit hit)
```

~~~
EnvironmentRaycastManager raycastManager; // シーンに配置したもの
Ray ray; // 何らかのRay

if(raycastManager.Raycast(ray, out EnvironmentRaycastHit hit))
{
    if(hit.status == EnvironmentRaycastHitStatus.Hit)
    {
        // 衝突座標を取得
        Vector3 hitPoint = hit.point;

        DoSomething(hitPoint);
    }
}
~~~
hit.statusがHitの時、hit.pointにはrayのヒット地点ワールド座標が入ります。
今回はこの地点にコライダーを移動させ、パーティクルの消滅地点として使用しました。

## 投げ物などの物理について
全ての物理計算はホスト側で行い、ゲストにはその位置を同期させる方式としました。
位置同期はNetworkTransformを使ったシンプルな方法です。

Photon Fusion 2にはNetworkRigidbodyという同期コンポーネントがあるようですが、これを使用した際にMeta Interaction SDKの一部機能が正しく動作しなくなってしまったためNetworkRigidbodyは使っていません。

https://x.com/0osuu/status/1851572688952988081


## さいごに
Colocationの動作について、かなり気を使ってあげないと簡単に位置ずれしてしまったりなど、まだ完璧ではない印象です。今後に期待してます。

また、ぱっと見複雑そうに見える機能でもBuilding Blocksによってシーンにポン置きで使えたりするので、ぜひ色々触ってみてください！