Photon Fusion for UnityのWebGLビルドを1週間ゲームジャムで試して得た知見

# はじめに

ないちさんが運営するuniyroomというWEBサイトでは、年に数回、1週間でゲームを作って公開しプレイし合う「1週間ゲームジャム」というイベントが行われます。

https://unityroom.com/unity1weeks/56

参加するにはWebGL形式でビルドを行いアップロードする必要がありますが、Photon Fusionはリリース当初対応していませんでした。

しかし6月のアップデートでWebGLに対応したため、イベントに参加して実際にPhoton Fusionを使ったオンラインマルチプレイゲームを作ってみました。

本記事では、1週間ゲームジャムでPhoton Fusionを使って作品を作成し、WebGLで投稿するにあたって気づいたことをまとめました。

なおこの記事の内容は、2022年10月1日に行われたunity1week online共有会 #9というイベントで行ったLT、「Photon Fusionを使ってオンラインマルチプレイゲームを作った話」を修正・補足・編集したものです。

https://www.docswell.com/s/ni26mu/ZG79J5-2022-10-01-230000

https://lycoris102.notion.site/unity1week-online-09a391b94a9a4c3e8b1128157aa41fcb

### 前提記事

Photon Fusionの基本的な解説は別記事で行っています。詳しく知りたい方は以下のリンクからご参照下さい。

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

関連記事

https://qiita.com/tags/photonfusion

### 動作確認環境

Windows 10 Home 21H2
Unity 2020.3.27f1
Fusion SDK 1.1.3 F Build 586

# Photon Fusionを使うにあたって知っておいてほしいこと

一週間ゲームジャムで使ってみた所感の中で、以下の3つは特に重要ですが、特にPhoton Fusionを使ったことがない・あまり使ってない人は特に知っておいて欲しい事柄です。

### ①SDKとWebGLの相性は良好

作成したプロジェクトをWebGLへビルドしてunityroomで公開するまでの間、SDKに由来するエラーは1度も発生しませんでした。

ビルドのPlatformをWebGLにスイッチする以外、特に設定する項目もないため、気軽に試すことが出来ます。

### ②作りたいゲームに一番近い公式サンプルを活用する

Photon Fusionを初めて触る場合、新しい概念の習得やその実装・設定が必要になります。そのため慣れないうちは、新規プロジェクトを作成して0から必要なコンポーネントを追加していくといった手法は、正常動作までの道のりが長く大変です。

そのため、初めはチュートリアルで作成したプロジェクトや公式のサンプルをもとに作るのがお勧めです。

サンプルの一覧ページにはゲームのタイプ別にいくつも公開されています。一覧ページは現在英語版しかないようですが、一部の個別ページは日本語での解説があります。

https://www.photonengine.com/ja-jp/samples

### ③SDKは最新（Nightly）にする

Photon Fusionは積極的に開発が行われており、バージョンアップも頻繁に行われています。問題が起きた場合、SDKが更新されていないか確認するのもよいでしょう。

なお一般的にNightlyは先行版やβ版といった意味合いがありますが、別件で問い合わせたところPhoton FusionではNightlyが安定版扱いとのことでした。

# 投稿作品紹介

<iframe width="560" height="315" src="https://www.youtube.com/embed/nqm1Vu-z3mw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

引用元：ぱふもどきさん
1週間ゲームジャム全ゲーム実況

https://youtu.be/AoNC330W_yA?t=11843

### ゲームの概要

今回作成した作品は以下のような特徴があります。

- 全員で１つのワールドを開拓するオンラインマルチゲーム
- 資源を取得、納品することでプレイヤーがレベルアップ
- 資源を納品することで拠点を発展させる

### 移ろいゆく世界を体験してもらうために

このゲームで表現したかったのは、多くのプレイヤーの活動や風化によるワールドの変化です。それを実現するため、以下の要件を満たせる共有モードを使用しました。

- 全員同じワールドに接続する
- 誰もインしてない時は風化（リセット）する
- 途中入場、退場を自由にする
    - クライアントホスト型はワールドがホストに依存してしまう
    - サーバー型は導入コストが高い

# 作品で使用した技術について

## 共有モードについて

共有モードは、サーバー型やクライアントホスト型と同じくネットワークトポロジーのうちの一つで、以下のような特徴があります。

- 中央集権ではなく、分散管理
    - ルームに1人でもプレイヤーがいる場合ルームを維持可能
    - 状態権限も分散管理
- サーバー設置コストが不要
- 一部使えないFusionの機能あり
    - ラグ補正ヒットボックス

### 状態権限の考慮

状態権限はオブジェクトの状態を変更する権限のことを指し、最大で１つのインスタンス（プレイヤー、サーバー）がその権限を持ちます。

状態権限がない場合、メソッドの実行（岩の採掘、草の劣化）や値（資源量の変更）を同期することが出来ません。なおその場合でも実行自体は出来てしまうため、実装中は問題なかったがいざマルチプレイテストをしたら同期できない、となってしまわないよう注意する必要があります。

![2022-10-10 15.37.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/a1afd8f4-456e-3b0a-8e5c-2335f060cd06.png)

共有モードにおいて、状態権限はオブジェクトをスポーンさせた人が持つため、作りによっては誰が権限をもつか事前に判断できません。そのため、なにか処理を行ったり値を変更する場合、対象の状態権限に関わらず処理ができるようにする必要があります。

### 権限問題の解決方法

共有モードで起きる上記のような権限問題は、主に以下の2つの方法で解決できます。

- 状態権限の移譲
    - 権限の破棄　Object.ReleaseStateAuthority
    - 権限の要求　Object.RequestStateAuthority
    - 取得前に、権限を所有している側が破棄しておく必要がある
- RPCメソッドを活用する
    - 次項で解説

権限の移譲については、権限の所有者が事前に破棄を行う必要があるため、頻繁な操作には向かないでしょう。

今回のケースではRPCメソッドを使用しました。

## RPCによるデータ同期時の権限問題解決

データを共有したいが権限の問題が出る場面があります。本作品では、以下のような仕様があり、そこでRPCメソッドを使用しその問題を解決しています。

- プレイヤーがアクションをすると資源量が減り、消滅する岩や木オブジェクト
- プレイヤーが通過すると枯れていく草ブロック

### RPCとは

- 遠隔手続き呼び出し。Remote Procedure Callの略。
    - Photon Fusionに限らず、ネットワーク上で動くプログラムでは基礎となる方法
- Fusionにおける特徴
    - パケットロスが少ない
    - 権限問題を解決できる
    - ゲーム内時間に対して正確ではない
- RPC呼び出し回数は制限があるので注意
    - 1ルームにつき500回/秒

### 実装方法

実装も呼び出しも手軽に可能です。NetworkBehaviourを継承したクラス内で以下のような実装を行えば、通常のメソッドと同じように呼び出すことができます。
![2022-10-10 13.17.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/3539ae52-2439-5083-3730-3991dcedeb68.png)

### 使用例

「プレイヤーが通過すると草の耐久が減り、いずれ枯れる（消滅する）」という実装例を紹介します。処理の流れは以下の通り。

- ブロックとプレイヤーの Colliderが接触
- OnTriggerEnterが呼ばれる
- RPCを実行する
- 同期される

![QuantumFrontier_Glass_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/cce33324-1c3c-9ce0-5f1e-d93a4fc4fdd4.gif)


```glass.cs
//草の耐久値
//[Networked]は同期用のPhoton Fusionのアトリビュート
//OnTriggerEnterはUnityのもの
[Networked] public int glassNum { get; set; }
private void OnTriggerEnter(Collider other){
	if(glassNum > 0) RPC_ChangeGlass();
}

[Rpc(RpcSources.All, RpcTargets.All)]
public void RPC_ChangeGlass(){
	glassNum -= 1;
	if(glassNum <= 0) GlassObj.SetActive(false);
}
```

## プレイヤーをスポーンするタイミング

ゲームを起動してからプレイヤーオブジェクトをスポーンさせるまでの流れをシンプルにすると、以下のようになります。

1. クライアントの起動
    - ローカル用シーンを読み込む
    - オフラインゲームと同じ
2. PhotonServerへ接続処理を行う
    - NetworkRunner.StartGame
3. （オンライン用シーンを読み込み）
4. プレイヤーオブジェクトをスポーン

このタイミングにスポーンさせるのに便利なコールバックが2つ用意されています。

ちなみに同期させたいオブジェクトのスポーンに際し、通常Unityで使われるInstantiateは使えないため、Runner.Spawnを使う必要があります。

### 実装方法①　Spawned

Spawnedは、NetworkRunner.StartGameを実行した後、これを実装したオブジェクトがスポーンした時に呼ばれるコールバックです。

自分自身をスポーンさせる権限をもつ共有モードでこの方法を使います。実装方法は以下の2種類があります。

- SimulationBehaviourを継承、ISpawnedを実装
- NetworkBehaviourを継承、Spawnedをoverride

### 実装方法②　OnPlayerJoined

OnPlayerJoinedは、プレイヤーがジョインした時に呼ばれるコールバックです。

自分自身をスポーンしたい場合、シーンの読み込み後にNetworkRunner.StartGameを実行する必要があるため、主にサーバーやホストが使用します。実装方法は以下の2種類があります。

- NetworkBehaviourを継承しINetworkRunnerCallbacksを実装
- OnPlayerJoinedを実装しRunner.AddCallbacksで登録

## 量産オブジェクトを作る

ある程度実装ができると、機能は同じでも見た目違うバリエーションが欲しくなることがあります。Photon Fusionでは同期するために必要なコンポーネントが複数あり、複数セットアップする場合手間が大きくなりがちです。

そこで、Unityの機能であるRequireComponentを使います。このアトリビュートは指定したコンポーネントを自動でアタッチしてくれる便利な機能で、必須コンポーネントのつけ忘れや誤って削除するミスを防いでくれます。
![2022-10-10 17.39.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/9b7f02e4-b977-4596-3236-1d60bdc33ed6.png)

今回の例ではアセットストアで購入した複数のオブジェクトがあり、以下の４つのコンポーネントをそれぞれ追加する必要がありました。
RockManager.csのクラス名の前にRequireComponentで自身以外の3つを指定することで、RockManager.csをアタッチすることで残り3つを自動でアタッチしてくれます。

- NetworkObject（Fusionの同期用）
- NetworkTransform（Fusionの同期用）
- BoxCollider
- 独自スクリプト（RockManager）
    - [RequireComponent(typeof(NetworkObject))]
    - [RequireComponent(typeof(NetworkTransform))]
    - [RequireComponent(typeof(BoxCollider))]

# まとめ
Photon FusionはWebGLビルドに正式対応しその相性もよく、今回特に問題は起きませんでした。公式サンプルも豊富なため、1週間ゲームジャムでオンラインマルチプレイゲームを作るのも現実的な範囲だと思います。
ネットワーク・トポロジーの共有モードについては、今回の作例のようにゲームによっては最適解になりえます。その際、同期処理の権限問題には注意が必要です。
逆にラグ補正ヒットボックスを使いたい場合やワールドを維持し続けたい場合などは不適当なため、サーバー型やクライアントホスト型を検討するとよいでしょう。
RPCは権限問題の解決に有用ですが、呼び出し回数制限があるため、使い所は考える必要があります。