【Unity2d】半日でオンライン格闘ゲーム（超簡易版）を作ってみた

この記事はレコチョク [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)の2日目の記事となります。
# はじめに
株式会社レコチョクの新米エンジニアリングマネージャー曽利田です。
最近LOVEBITESという日本のヘヴィメタルバンドが気になっています。
普段は音楽に関するサービス開発を行なっていますが、今回はオンライン格闘ゲームをサクッと作る方法をまとめてみました。

# Unityインストール、プロジェクト作成
1. https://unity3d.com/jp/get-unity/download
からインストールしてください。
※Unity Hubをインストールしておくと、複数バージョンのUnityを管理できたりするので幸せになれます。

2. 2Dのテンプレートを選択してプロジェクトを作成

# Photonインストール
https://assetstore.unity.com/packages/tools/network/pun-2-free-119922
こちら無料版です。
最大20人の同時接続が可能です。（今回は1vs1なので最大10部屋まで作成可能ってことですね）

同時接続数を増やしたい場合は有償版もあります。
https://assetstore.unity.com/packages/tools/network/photon-pun-2-120838

「マイアセットに追加する」を押して、Unityに戻ってDownload/Importしましょう。

# Photon設定
PhotonServerSettingsのDashboardを押すとPhotonのWebページに飛んでいくので、Dashboardからアプリ登録しましょう。
特につまずくことなく登録できるはずなので、登録後に発行されるApp IdをコピってUnityに戻って入力します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124782/7d6460e6-48d5-ba67-c304-898719f31100.png" width="540">

# キャラクターの作成
画像を作るのは大変なのでAssetを探しましょう。
https://assetstore.unity.com/packages/2d/characters/free-maya-indian-hero-46725
とりあえず無料版のこれを使います。

「マイアセットに追加する」押して、Unityに戻ってDownload/Import

# フィールドの作成
キャラクターが落ちないようにColliderを配置しましょう。
※このあたりはUnityの基本事項なので割愛します。

# 接続してみる
以下のクラスを使います。
```
using Photon.Pun;
using Photon.Realtime;
```
続いて、Photonサーバへの接続です。
```C#
    void Start()
    {
        Connect("1.0");
    }

    private void Connect(string gameVersion)
    {
        if (PhotonNetwork.IsConnected == false)
        {
            PhotonNetwork.GameVersion = gameVersion;
            PhotonNetwork.ConnectUsingSettings();
        }
    }
```
gameVersionを引数にしてますが、今回は固定値です。
大型アップデート等で複数バージョンのオンライン対戦機能を実装する場合には、この値を変えて制御できます。
旧Verのアプリ：1.0
新Verのアプリ：2.0
のように変えることで各バージョンごとのマッチングが可能になります。

# 各種コールバックを受け取る
Photonには多数のコールバックを用意されており、各種コールバックを受け取るためにはMonoBehaviourPunCallbacksを継承する方法が最も楽です。
```C#
public class OnlineBattleManager : MonoBehaviourPunCallbacks
```
### 必要最低限のコールバックを受け取る
```C#:OnlineBattleManager.cs
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class OnlineBattleManager : MonoBehaviourPunCallbacks
{
    [SerializeField] private GameController gameController;

    // Start is called before the first frame update
    void Start()
    {
        Connect("1.0");
    }

    private void Connect(string gameVersion)
    {
        if (PhotonNetwork.IsConnected == false)
        {
            PhotonNetwork.GameVersion = gameVersion;
            PhotonNetwork.ConnectUsingSettings();
        }
    }

    /*
     * Callbacks
     */
    // Photonに接続
    public override void OnConnected()
    {
        Debug.Log("OnConnected");
    }

    // Photonから切断された時
    public override void OnDisconnected(DisconnectCause cause)
    {
        Debug.Log("OnDisconnected");
    }

    // マスターサーバーに接続した時
    public override void OnConnectedToMaster()
    {
        Debug.Log("OnConnectedToMaster");
    }
```
ということでPhotonサーバーの接続までできました。

# マッチング処理
続いてマッチングするための処理を書いていきます。

1.Photonサーバに接続後、入室可能な対戦部屋があれば入室します。
```C#
    // マスターサーバーに接続した時
    public override void OnConnectedToMaster()
    {
        Debug.Log("OnConnectedToMaster");

        PhotonNetwork.JoinRandomRoom();
    }
```

2.入室に失敗したら部屋を作成します。
```C#
    // ランダムな部屋への入室に失敗した時
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        Debug.Log("OnJoinRandomFailed");

        var roomOptions = new RoomOptions();
        roomOptions.MaxPlayers = 2;

        PhotonNetwork.CreateRoom(null, roomOptions);
    }
```

3.部屋の作成と入室に成功したらキャラクターを生成します。
最初に部屋に入った人を左側、あとから部屋に入った人を右側に配置します。
※最初に部屋に入った人（最初に部屋を作った人）を マスタークライアント（ホストプレイヤー）と呼びます。
```C#
    // 部屋に入室した時
    public override void OnJoinedRoom()
    {
        Debug.Log("OnJoinedRoom");

        if (PhotonNetwork.IsMasterClient)
        {
            position = new Vector3(-5f, 0f, 0f);
            player = PhotonNetwork.Instantiate("MasterPlayer", position, Quaternion.identity);
            Vector3 scale = player.transform.localScale;
            scale = new Vector3(0f - scale.x, scale.y, scale.z);
            player.transform.localScale = scale;
        }
        else
        {
            position = new Vector3(5f, 0f, 0f);
            player = PhotonNetwork.Instantiate("ClientPlayer", position, Quaternion.identity);
        }
    }
```

これだけでマッチング処理は終わりです。
ただし、これは一番シンプルな方法で、
・入室可能な部屋一覧を表示して選択する
・レベルに応じてマッチング相手を変更する
・パスワード付きの部屋を作成する
などなどカスタマイズは可能です。

# キャラクターにオンライン同期用のコンポーネントを付与する
ここまでで、オンライン対戦用の準備ができました。
あとはキャラクターにオンライン同期用のコンポーネントを付与します。

必要なコンポーネントは、
１．PhotonView
このコンポーネントをキャラクターにアタッチすることで、位置情報などをネットワーク経由での同期対象に含めることが可能になります。

２．PhotonTransformView
このコンポーネントをキャラクターにアタッチして、PhotonViewのObservedComponentに割り当てることで、transform（位置情報や回転）の同期が可能になります。
※今回は割愛していますが、アニメーションを同期したい場合は、PhotonAnimatorViewコンポーネントをアタッチして、PhotonViewのObservedComponentに割り当てることでアニメーションの同期が可能になります。

これで自身の動きが相手側にも見えるようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124782/de2813cd-dd07-c60f-6c9c-f9c590908ba5.png" width="400">


# 動かしてみる
キャラクターの移動処理、ジャンプ、攻撃については、Unityの基本事項なので割愛します。
Unityを複数起動して実行するとこのようになります。
ちゃんと同期されました。
いやー、簡単ですね。
![online.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124782/b85f688c-40c1-d4fa-9c32-769b9ea9aadf.gif)

# 対戦してみる
今回は簡易版なので攻撃ボタンを押したときに放たれる波動砲が相手にヒットしたら勝ちとします。
当たり判定も通常通りの実装でOKです。
```C#
void OnTriggerEnter2D(Collider2D other)
```
![attack.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124782/ed2908d0-404a-0809-9576-9165feea22c4.gif)

# まとめ
最後まで読んでいただきありがとうございました。
こんな簡単にオンラインゲームが作れるんだということを知ってもらえたらなと思っています。
今回は簡易版なので一番シンプルな形で作りましたが、Photonにはもっと色々な機能が用意されているので、興味をもった方は調べてみてください。

明日のレコチョク [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)は3日目 ```Jamf Pro のグループに期限付きでユーザを入れたい``` となります。お楽しみに！

<br>
<br>

この記事は[レコチョクのエンジニアブログ](https://techblog.recochoku.jp/8628)の記事を転載したものとなります。