今更ながらPUN2でマルチプレイを実装してみる

[PONOS Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ponos "Qiita Home") 23日目の記事です。
昨日は[@ANIZA_15](https://qiita.com/ANIZA_15)さんの「LocoをCloud Runで実行する」でした。

## はじめに
ゲーム制作をしていると、「一人で遊ぶゲームだけじゃなくてオンラインで遊ぶゲームを作ってみたい！」と誰しも一度くらいは考えたことがあるのではないでしょうか。しかし、実際のところは「ネットワークに関する知識がないから作れない...」と制作に踏み切れずにいた人は多いと思います。
私自身、一人で遊ぶゲームはある程度作れるものの、ネットワークに関する知識はまだまだで、マルチプレイのゲームの制作にいまいち踏み切れていませんでした。
そんな時に見つけたのが「PUN2」というネットワークエンジンです。
今回はそんな私がPUN2を用いてマルチプレイを今更ながら実装してみたというお話です。

## PUN2とは
そもそも「PUN2とはなんぞや」ということですが、正式名称を「Photon Unity Networking」といい、上記にもあるとおりマルチプレイゲームを制作することができるネットワークエンジンのことです。
搭載されているものとしてサーバー、クライアントSDKがあり、一通りマルチプレイゲームを実装するのに必要なものが揃っています。

## 実装物
今回サンプルとして実装したものはこんな感じのものです。
![sample (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991131/b42eaee0-1268-63a4-482f-9d485978401f.gif)
矢印のついたプレイヤーの操作ができて、右下のInputFieldに入力したメッセージを相手に送信できるといったとてもシンプルなものです。

## 開発環境
Unity : 2019 4.37f1


## 前準備
公式サイト：[PUN2](https://www.photonengine.com/ja-jp/pun) 
PUN2を使用したゲームを作るにはPUN2のアカウントが必要なので公式サイトのリンクからアカウントを作成します。
アカウントが作成できたら以下の手順で前準備を進めていきます。
#### 1.アプリの作成
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991131/dd6887e9-db1a-99f0-6187-b3c136e27324.png)

基本的には画像のように設定すれば大丈夫です。アプリケーション名は制作するアプリの名前を入れてください。今回はPUN2Sampleとしています。
アプリの作成が完了するとアプリケーションIDというものが発行されるのでこちらを控えておきます。
#### 2.Unityにパッケージを導入
続いて、Unityに次のパッケージを導入します。
[PUN 2 - FREE](https://assetstore.unity.com/packages/tools/network/pun-2-free-119922?locale=ja-JP)
インポートすると下記のウィンドウが表示されるので控えておいたアプリケーションIDを`AppId or Email`の項目に入力します。（アカウントに関連する項目なので念のため隠しています。）
![図2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991131/39b87b57-70a7-ae74-a66e-95b5c2ee9f83.jpeg)

#### 3.Unity側の設定
セットアップが完了すると`PhotonServerSettings`というファイルが生成されるので、そのファイルを下記の画像のように設定します。
![図3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991131/039dda47-bac5-b071-b40a-6badc117f957.png)
以上で前準備は完了です。これで制作を進めていく環境が整いました。

## プレイヤーの実装

PUN2では同期させるオブジェクトのことを「ネットワークオブジェクト」といいます。
今回はプレイヤーの動きを同期させたいのでプレイヤーをネットワークオブジェクトとして実装していきます。
#### 1.プレイヤーとなるオブジェクトを作成

Spriteを新規作成し、`PhotonView`、`PhotonTransformView`という二つのコンポーネントをアタッチします。そして`PhotonTransformView`コンポーネントの`SyncronizeOptions`で同期させたいTransformの値を設定します。今回は位置と回転の値を同期させたかったので以下のようになりました。
![図4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991131/2bdabc14-33d4-ec2a-c01c-431c5023e037.png)

#### 2.作成したオブジェクトを操作するスクリプトの作成
プレイヤーオブジェクトが作成できたら操作するためのスクリプトを作成します。
ソースコードは以下です。（一部抜粋）
``` C#:PlayerSample.cs
using UnityEngine;
using Photon.Pun;

public class PlayerSample : MonoBehaviourPun
{
    [SerializeField] GameObject allow = default;
    
    void Start()
    {
        if(photonView.IsMine)
        {
            allow.gameObject.SetActive(true);
        }
    }

    void Update()
    {
        if (!photonView.IsMine) return;
        // ここから移動処理
        // キーの入力によってTransformの値を計算する処理を入れていますが、長いので割愛
    }
```
まず、`Photon.Pun`をusingし、継承するクラスを`MonoBehaviourPun`に変更しています。
これがないとPhotonEngineの機能を使用することができません。
続いて`photonView.IsMine`というプロパティですが、このプロパティが所有者が自分かどうかを保持しており、`if (PhotonView.IsMine)`で所有者が自分の場合にのみ処理を行なうといったことをしています。
`if (PhotonView.IsMine)`の記述がないとどのネットワークオブジェクトも操作できてしまう...といったことが発生してしまうので、ネットワークオブジェクトとして実装する以上、この記述は必要です。

あとは先ほど作成したプレイヤーオブジェクトにこのスクリプトをアタッチし、Resourcesフォルダに入れてPrefab化します。（後述する手順の場合、Resourcesに入れないと生成することができないので注意）
ここまでできたらプレイヤーオブジェクトの作成は完了です。

# マルチプレイの実装
PUN2のマルチプレイの仕組みはまず、サーバー（ロビー）に接続しその後にそれぞれの部屋（ルーム）に入室して同じルームの人と遊ぶといった仕組みになっているようです。ここではサーバーに接続後、それぞれのルームに入室し先ほど作成したプレイヤーを生成するというところまで実装していきます。

#### サーバーに接続、部屋に入室するマネージャークラスを実装する
実際にクライアントがゲーム起動後にサーバーに接続し、部屋に入室する処理を実装していきます。
ソースコードは以下です。
``` C#:NetworkManagerSample.cs

using UnityEngine;
using Photon.Pun;     
using Photon.Realtime;

public class NetworkManagerSample : MonoBehaviourPunCallbacks
{
    void Start()
    {
        PhotonNetwork.ConnectUsingSettings();
    }

    public override void OnConnectedToMaster()
    {
        PhotonNetwork.JoinRandomRoom(); 
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        var option = new RoomOptions();
        option.MaxPlayers = 2;      
        PhotonNetwork.CreateRoom(null,option);
    }

    public override void OnJoinedRoom()
    {
        Vector3 pos = new Vector3(Random.Range(-3.0f, 3.0f),Random.Range( -3.0f, 3.0f), 0.0f);
        GameObject player = PhotonNetwork.Instantiate("Player", pos, Quaternion.identity);
    }
}
```
まず、`Photon.Pun`と`Photon.RealTime`をusingし、継承`MonoBehaviourPunCallBacks`に変更しています。プレイヤースクリプト同様、これをしないとPUN2の機能を使用することができません。
ここからはPUN2が提供している関数の説明です。
##### PhotonNetwork.ConnectUsingSettings()
サーバーへの接続を試みる関数です。

##### OnConnectedToMaster()
サーバーに接続できた時に呼ばれるコールバックです。
ここでは`PhotonNetwork.JoinRandomRoom()`関数でランダムな部屋に入室するようにしています。

##### OnJoinRandomFailed(short returnCode, string message)
ランダムな部屋に入室できなかった時に呼ばれるコールバックです。
ここでは`var option = new RoomOptions()`で部屋の詳細を作成・設定し、作成した詳細の部屋を新規作成するようにしています。（今回は部屋の最大人数を2人に設定しています。）
##### OnJoinedRoom()
部屋に入室できた時に呼ばれるコールバックです。
ここでは先ほど作成したプレイヤーオブジェクトをランダムな位置に作成するようにしています。
生成するときは`PhotonNetwork.Instantiate()`を使用して生成します。

あとはこのスクリプトを空のオブジェクトにアタッチしてシーン上に配置すればマルチプレイの実装は完了です。


## メッセージを送信する
マルチプレイの実装までで実際に操作ができオブジェクトの同期をとることはできているのではないかと思います。
ここではさらにプラスαの要素として相手にメッセージを送信・受信する部分を実装していきます。
#### メッセージを入力・表示・送受信するクラスを実装する
InputFieldに入力されたメッセージを相手に送信する処理を実装していきます。
ソースコードは以下です。
```C#:MessageManagerSample.cs
using UnityEngine;
using Photon.Pun;      
using Photon.Realtime; 

public class MessageManagerSample : MonoBehaviourPun
{
    [SerializeField] InputField inputField = default;
    [SerializeField] Text receiveText = default;
  
    void Start()
    {
        receiveText.text = "";
    }

    void SetReceiveText(string message)
    {
        receiveText.text = message;
        StartCoroutine(nameof(HideAfterSeconds), 3.0f);
    }

    IEnumerator HideMessageAfterSeconds(float time)
    {
        yield return new WaitForSeconds(time);
        receiveText.text = "";
    }

    public void SendText(string message)
    {
        photonView.RPC(nameof(ReceiveText), RpcTarget.Others, message);
    }

    [PunRPC]
    void ReceiveText(string message)
    {
        SetReceiveText(message);
    }
```
まず、`Photon.Pun`と`Photon.Realtime`をusingし、継承するクラスを`MonoBehaviourPun`に変更しています。

ここからはそれぞれの関数の説明です。
##### `SetReceiveText(string message)`
引数の文字列をrecceiveTextに設定する関数です。設定と同時に`HideMessageAfterSecond()`というコルーチンを呼び出しています。
##### `HideMessageAfterSecond(float time)`
指定した秒数後にreceiveTextに設定されている文字列を空にするコルーチンです。

##### `SendText(string message)`
相手にメッセージを送信する関数です。
`photonView.RPC()`という関数を用いて相手にメッセージを送信します。
この関数の引数ですが、第一引数に受信時に実行する関数、第二引数に実行対象、第三引数に第一引数で指定した関数の引数と同じ型の値を指定します。
RPCの実行対象には以下の種類があるようです。
|RPCの実行対象|実行者自身|受信者|途中参加者|
|---|---|---|---|
|RpcTarget.All|即座に実行される|通信を介して実行される|実行されない|
|RpcTarget.Others|実行されない|通信を介して実行される|実行されない|
|RpcTarget.AllBuffered|即座に実行される|通信を介して実行される|実行される|
|RpcTarget.OthersBuffered|実行されない|通信を介して実行される|実行される|
|RpcTarget.AllViaServer|通信を介して実行される|通信を介して実行される|実行されない|
|RpcTarget.AllBufferedViaServer|通信を介して実行される|通信を介して実行される|実行される|

これを使い分けることで特定のプレイヤーのみにメッセージを送信するといったこともできるのではないかと思います。

##### `ReceiveText(string message)`
受信時に実行される関数です。
この関数の中で`SetReceiveText()`を実行しています。
受信時に実行する関数には`[PunRPC]`という属性をつける必要があります。

あとはシーン上にInputField、Textオブジェクトを配置してこのスクリプトを空のオブジェクトにアタッチ、それぞれの参照を設定、InputFieldの`OnEndEdit`の項目に`SendText()`を登録します。最後に今回作成した`MessageManagerSample.cs`ですが、`photonView`に実装されている関数を使用しているので空のオブジェクトにはPhotonViewコンポーネントもアタッチしないといけません。
ここまでできたらメッセージを送信・受信する処理の実装は完了です。

## 最後に
今回このサンプルを実装するのに使用したPUN2の機能はほんの一部分だけではないかと思います。
この機能だけでも時間をかければネットワークの知識がなくても割と本格的なマルチプレイゲームが作れると思いますので一度触ってみてはいかがでしょうか。


ここまでご覧いただきありがとうございました。次回は[bluenova1221](https://qiita.com/bluenova1221)さんの「UnityエンジニアがGodot Engineでゲームを作ってみた」です。