【初心者向け】PUN2 を Unity で使ってオブジェクトの同期をとる方法

Unity で Photon（PUN2 の Asset） を利用して同期方法する方法についてです。
初めて使う人、すっかり手順を忘れてしまった人向けのファーストステップです。
以降では、下の Gif のように二つの Unity Editor 間で同期する例を記載しています。

![Moving.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/2eeedf37-4c3d-1dde-6b02-9b80c10f74d1.gif)

# はじめに
## 前提
Name  | Version
------|-----------
Unity | 2019.4.9f1
PUN 2 | 2.29

## 構成
大きく以下二つに分けて作業しています。同期するための制約的な重要項目です。
1. Photon サイトでの作業
2. Unity での作業
3. 同期するための重要項目

# 1. Photon サイトでの作業
Unity で利用するために、Photon サイトの Dashboard から AppID を取得します。

1. アカウントを作って Dashboard にログインしましょう。（Webサイトのポチポチ作業なので割愛）
2. Dashboard にログインすると `Create a New Application` ができるようになるので、ぽちっとします。
3. 以下の矢印の箇所に値を設定して（Name は適当な名前を入れてください）`CREATE` ボタンを押します。 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/4e654ecf-4339-249c-7e3b-73c73ad737fe.png" width="512" />
4. 作成した Application の下記矢印のところをクリックすると、AppID が全て表示されます。  
![2021-03-29_23h55_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/f6ef0a24-73a1-3f56-3baf-dfdb05dcc82c.png)


# 2. Unity での作業

## 1. PUN2 をインポートします
1. 新規で Unity プロジェクトを作ってください。
2. [PUN 2 Free](https://assetstore.unity.com/packages/tools/network/pun-2-free-119922) からアセットをダウンロード、インポートします。
3. Window -> Photon Unity Networking -> PUN Wizard を開きます。  
![2021-03-30_10h10_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/bcdb876e-1430-a7de-15de-4032d9cb7507.png)
4. `App Id PUN` に先ほど取得した AppID を、`Fixed Region` に jp を設定します。  
![2021-03-30_10h10_36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/554c0e63-e7c0-ec53-f350-845986879591.png)

### 補足
* 有料版もありますが、今回の範囲であれば無料版で問題ありません。
* `Photon Unity Networking Classic - FREE` もありますが、こちらは古いバージョンのものなので選ぶ必要ありません。
* Unity Asset をまとめている人は、インポートしたフォルダから移動させても大丈夫です。

## 2. C# のコードを書きます
キーボードを A, B, C の順で押していくと、動作するように作っています。
A. Photon に接続
B. Room に参加
C. 同期するオブジェクトを生成

その上で矢印キーを押すと、オブジェクトが同期した状態で動きます。
詳細は以下の通りです。

### 1. PhotonConnecter.cs
Photon と接続する、Room に参加するプログラムです。簡易にするためエラーや切断などを省いてあります。

```C#
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class PhotonConnecter : MonoBehaviourPunCallbacks
{
    [SerializeField] private string gameVersion = "0.1";
    [SerializeField] private string nickName = "TestName";
    [SerializeField] private string roomName = "TestRoom";

    void Awake()
    {
        PhotonNetwork.AutomaticallySyncScene = true;
        PhotonNetwork.GameVersion = gameVersion;
        PhotonNetwork.NickName = nickName;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            Connect();
        }
        if (Input.GetKeyDown(KeyCode.B))
        {
            JoinRoom();
        }
    }

    private void Connect()
    {
        Debug.Log("Photon Cloud に接続します。");
        PhotonNetwork.ConnectUsingSettings();
    }

    private void JoinRoom()
    {
        Debug.Log($"{roomName}に参加します。");
        PhotonNetwork.JoinOrCreateRoom(roomName, new RoomOptions(), TypedLobby.Default);
    }

    public override void OnConnectedToMaster()
    {
        Debug.Log("Photon Cloud に接続しました。");
    }

    public override void OnJoinedRoom()
    {
        Debug.Log($"{roomName} に参加しました。");
    }

    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        Debug.Log($"{newPlayer.NickName} が入室しました。");
    }
}
```

### 2. NetworkCubeMaker.cs
Photon を経由して NetworkCube を生成します。

```C#
using Photon.Pun;
using UnityEngine;

public class NetworkCubeMaker : MonoBehaviourPunCallbacks
{
    [SerializeField] private GameObject networkCube;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.C))
        {
            PhotonNetwork.Instantiate(networkCube.name, Vector3.zero, Quaternion.identity, 0);
        }
    }
}
```

### 3. NetworkCube.cs
Photon で Position/Rotation が同期されることを確認するためのプログラムです。

* PhotonViewer.cs
* PhotonTransformViewer.cs

```C# 
using Photon.Pun;
using UnityEngine;

public class NetworkCube : MonoBehaviour
{
    [SerializeField] private PhotonView photonView;

    private void Update()
    {
        var add = Vector3.zero;
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            add = GetAdditionalMovement(-0.1f, 0, 0);
        }
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            add = GetAdditionalMovement(0.1f, 0, 0);
        }
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            add = GetAdditionalMovement(0, 0, 0.1f);
        }
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            add =GetAdditionalMovement(0, 0, -0.1f);
        }
        transform.position += add;
    }

    private Vector3 GetAdditionalMovement(float x, float y, float z)
    {
        if (!photonView.IsMine)
        {
            Debug.Log("同期されるオブジェクトなので操作できません。");
            return Vector3.zero;
        }
        return new Vector3(x, 0, z);
    }
}

```

## NetworkCube を Prefab として作る

1. Cube を Hierarchy 上に作る
2. Add Component から PhotonView.cs を追加
3. Add Component から PhotonTransformView.cs を追加
4. Add Component から NetworkCube.cs を追加
5. 追加した NetworkCube.cs の PhotonView に自分自身をドラッグ＆ドロップで設定
6. Project の Assets 配下に Resources フォルダを作る
7. Hierarchy 上で作った NetworkCube を Resources フォルダにドラッグ＆ドロップ
8. こんな感じになっているか確認  
![2021-03-24_11h27_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/f0b5a9c4-1ff4-ccf4-a470-1b2d140bfa9a.png)
9. Hierarchy 上から NetworkCube は削除

### 補足
* 7 の Resources フォルダ配下に配置することが重要です。サブフォルダとかを作ると正常に動作しません。
* 8 のところで緑色になっているところ見て察する人は多いと思いますが、今回は Position と Rotation を同期するようにしています。

## Hierarchy に NetworkCubeMaker、PhotonConnecter を作る
### 準備
1. NetworkCube が生成されることになるので Plane を作る
2. こんな感じの見た目  
![2021-03-24_11h37_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/bac2c989-cf5d-84f7-eaf0-6f4a188142ae.png)

### NetworkCubeMaker を作る
1. Create Empty して空のオブジェクトを配置
2. Add Component から NetworkCubeMaker.cs を追加
3. NetworkCubeMaker.cs にさっき作った Prefab の NetworkCube を設定
4. こんな感じになっているか確認  
![2021-03-24_11h34_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/82702d80-13e3-46d5-88bf-8dbcd84a74c5.png)

### PhotonConnecter を作る
1. Create Empty して空のオブジェクトを配置
2. Add Component から PhotonConnecter .cs を追加
3. こんな感じになっているか確認  
![2021-03-24_11h35_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/185e1111-f7f5-0859-5647-da4c1bdb033f.png)
4. Hierarchy はこんな感じ  
![2021-03-24_11h36_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/904eeeb0-109b-1b0e-49dc-909bb0c9f441.png)

# Photon に接続して同期するテスト
## 二つの Unity Project から Photon に接続する
1. 一度 Unity を閉じる
2. 作成したフォルダ毎コピー  
![2021-03-24_11h39_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/c2e04baf-7980-70d6-7901-4509a0c5760f.png)
3. 両方 Unity で開く  
![2021-03-24_11h40_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/562bb08c-241b-ab18-e6be-53eb287afbdf.png)
4. それぞれ実行する
5. PhotonTry の Game 画面を選択した状態で、キーボードの A を押す。Photon に接続しにいく。
6. Console に `Photon Cloud に接続しました。` と表示されたら、キーボードの B を押す。Room に参加しにいく。
8. `TestRoom に参加しました。` と表示されることを確認する。こんな感じ。  
![2021-03-24_11h43_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/827847c3-1f47-f6ec-bae7-fc897f3003ba.png)
9. PhotonTry - コピー のGame 画面を選択した状態で、キーボードの A を押す。Photon に接続しにいく。
10. Console に `Photon Cloud に接続しました。` と表示されたら、キーボードの B を押す。Room に参加しにいく。
11. `TestRoom に参加しました。` と表示されることを確認する。
12. ここまで来たら二つの Unity Project が、それぞれ Photon に接続して同期がとれる状態になっている。

## オブジェクトを生成して同期されるか確認する
1. PhotonTry の Game 画面を選択した状態で、キーボードの C を押す。NetworkCube が生成される。
2. PhotonTry - コピー の Game 画面でも NetworkCube が生成されていることを確認する。
3. PhotonTry の Game 画面を選択した状態で、キーボードの矢印キー（↑↓←→）を押して NetworkCube を動かす。
4. PhotonTry - コピー の Game 画面でも NetworkCube が動いていることを確認する。
5. 逆もできるし、Scene 画面から回転させたら回転も同期されます。良かったら試してみてください。

# 3. 同期するための重要項目
* 同期するシーン名が異なると同期できません。
    * 今回は両方とも Game なので問題ありません。
* 同期するオブジェクトの Prefab は Resources フォルダ配下に配置することが重要です。
    * サブフォルダとかを作ると正常に動作しません。
* 同じアプリケーションネーム、同じルームに接続していても、Region が異なると同期されません。
    * 今回はそのため jp で固定しています。