🔥

[Unity6]Photon Fusionのサーバーモードでエアホッケー的な物を作ってみたい(移動〜Unity判定編2/3)

に公開

はじめに

前回では、導入から生成を実装してみたので、今回はエアホッケー部分の移動や判定を実装してみます。

前回からの変更点(スクリプト以外)

  • サーバークライアントでプロジェクトを分けるのではなく同じプロジェクトにしました。
     - サーバー用のシーンとクライアント用シーンで切り分けるようにしています。
    ServerScene

    GameScene
  • NetworkRunnerのスクリプトを1つにまとめました。
     - インスペクターで変更できるisServerフラグを用意して処理分けをしています。(詳細は後ほど後述します)
  • 前回作成した壁にBoxColliderをアタッチしたものをServerSceneGameSceneに作りました。
     - 左右の壁はトリガーにしています。
  • Canvasで表示等をしていた所をSpriteで表示するように変更しました。
  • UniRxをインポートしました。

前回からの変更点(スクリプト)

Scrips配下

今回実装するものも混ざっているので、前回分のNetworkRunnerLauncherServerRunnerLogic部分の変更点です。
Player.csに関してはCanvasからSpriteにしたので全て変更になってます。

NetworkRunnerLauncher.cs

  • NetworkRunnerの部分を統一して、サーバーかクライアントを判定してNetworkRunnerを作成するようにしました。
  • 外側からNetworkRunnerを参照できるようにシングルトン実装にしました。
  • コールバックの中身の処理を別の関数として分けました。
  • ballPrefabplayerPrefabは後の解説で作成します。
using UnityEngine;
using Fusion;
using System.Collections.Generic;
using System;
using Fusion.Sockets;

/// <summary>
/// ネットワークランナーランチャー
/// </summary>
public class NetworkRunnerLauncher : MonoBehaviour, INetworkRunnerCallbacks
{
    [SerializeField]
    [Header("プレイヤーのプレハブ")]
    NetworkObject playerPrefab;

    [SerializeField]
    [Header("ボールのプレハブ")]
    NetworkObject ballPrefab;

    [SerializeField]
    [Header("サーバーかクライアントか")]
    bool IsServer;

    [Header("ネットワークランナー")]
    public NetworkRunner runner;

    // インスタンス
    private static NetworkRunnerLauncher instance;

    // インスタンス
    public static NetworkRunnerLauncher Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindFirstObjectByType<NetworkRunnerLauncher>();
            }
            return instance;
        }
    }

    /// <summary>
    /// 初期化
    /// </summary>
    async void Start()
    {
        // コールバックを受け取る
        runner.AddCallbacks(this);

        // サーバーなら
        if (IsServer)
        {
            // デフォルトのシーン
            NetworkSceneInfo sceneInfo = default(NetworkSceneInfo);

            // サーバーモードで起動
            StartGameArgs startGameArgs = new StartGameArgs()
            {
                GameMode = GameMode.Server, // サーバーモードで動作
                SessionName = "GameServer",    // セッション名(任意)
                Scene = sceneInfo,  // default の NetworkSceneInfo を設定
            };

            // サーバーdゲームを開始
            StartGameResult result = await runner.StartGame(startGameArgs);

            // リザルトがOKじゃなかったら
            if (!result.Ok)
            {
                Debug.LogError($"サーバー起動に失敗: {result.ShutdownReason}");
            }
            // リザルトがOKなら
            else
            {
                Debug.Log("サーバーが正常に起動しました。");
            }
        }
        // クライアントなら
        else
        {
            // クライアント接続する場合
            StartGameArgs startGameArgs = new StartGameArgs()
            {
                GameMode = GameMode.Client, // クライアントモード
                SessionName = "GameServer" // サーバーのセッション名と一致させる
            };

            // クライアントを開始
            StartGameResult result = await runner.StartGame(startGameArgs);

            // リザルトがOKじゃなかったら
            if (!result.Ok)
            {
                Debug.LogError($"サーバー接続失敗: {result.ShutdownReason}");
            }
            // リザルトがOKなら
            else
            {
                Debug.Log("サーバーに接続できました。");
            }
        }
    }
    /// <summary>
    /// プレイヤーが参加した時
    /// </summary>
    /// <param name="networkRunner">ネットワークランナー</param>
    /// <param name="playerRef">接続したプレイヤー</param>
    public void OnPlayerJoined(NetworkRunner networkRunner, PlayerRef playerRef)
    {
        Debug.Log($"Connect Server IsServerFlag:{runner.IsServer}");

        // サーバーなら
        if (runner.IsServer)
        {
            // サーバーの接続ロジック
            ServerRunnerLogic.JoinplayerLogic(networkRunner, playerPrefab, ballPrefab, playerRef);
        }
    }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~他のコールバック関数~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

ServerRunnerLogic.cs

using UnityEngine;
using Fusion;
using System.Linq;

/// <summary>
/// サーバーのランナーロジック
/// </summary>
public static class ServerRunnerLogic
{
    /// <summary>
    /// プレイヤー接続ロジック
    /// </summary>
    /// <param name="networkRunner">ネットワークランナー</param>
    /// <param name="playerPrefab">プレイヤープレハブ</param>
    /// <param name="playerRef">プレイヤー情報</param>
    public static void JoinplayerLogic(NetworkRunner networkRunner, NetworkObject playerPrefab, NetworkObject ballPrefab, PlayerRef playerRef)
    {
        // 現在のプレイヤー数を取得
        int playerCount = networkRunner.ActivePlayers.Count();

        Debug.Log("playerCount" + playerCount.ToString());

        // プレイヤーのネットワークオブジェクト
        NetworkObject player = null;

        // プレイヤー生成
        player = networkRunner.Spawn(playerPrefab, new Vector3(0.0f, 0.0f, 0.0f), Quaternion.identity, playerRef);

        // プレイヤーが生成されていたら
        if (player != null)
        {
            player.GetComponent<Player>().RpcSetPlayerIndex(playerCount);
            player.GetComponent<Player>().RpcInit();
        }

        // 2人目のプレイヤー
        if (playerCount == 2)
        {
            // ボール生成
            networkRunner.Spawn(ballPrefab, new Vector3(0.0f, 0.0f, 0.0f), Quaternion.identity);
        }
    }
}

衝突処理のNetworkRunner事前準備

衝突処理をさせるのには通常通りRigidbodyColliderを使いますが、Photon FusionではNetworkRunnerをアタッチしているゲームオブジェクトに対してRunnerSimulatePhysics2Dのアタッチが必要になります。

クライアント側の設定

今回、クライアント側では衝突の計算をさせない処理にしたいのでClientPhysicsSimulationSync Transformにする必要があります、項目が色々あると思いますがSync Transformは衝突の計算をせずサーバーからの位置を同期させるだけのモードです。

サーバー側の設定

サーバー側では衝突の計算をしたいのでClientPhysicsSimulationSimulation AlwaysにしますSimulation Alwaysは衝突の計算をし続けるモードです。

プレイヤーPrefab

Spriteに下記をアタッチします。

  • BoxCollider2D
     - 画像に合わせて判定作成
  • Rigidbody2D
     - Dynamicで位置のy以外を固定
  • NetworkRigidbody2D(そのまま)
  • プレイヤースクリプト

プレイヤースクリプト全体

using UnityEngine;
using Fusion;

/// <summary>
/// プレイヤー
/// </summary>
public class Player : NetworkBehaviour
{
    [SerializeField]
    [Header("リジットボディ")]
    Rigidbody2D rigidbody2D;

    [Networked]
    [SerializeField]
    [Header("プレイヤーのインデックス")]
    int playerIndex { get; set; } = 0;

    /// <summary>
    /// Photonのスポーン時
    /// </summary>
    public override void Spawned()
    {
        // サーバーじゃないなら
        if (!NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
        }
        // サーバーなら
        else
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);
        }
    }

    /// <summary>
    /// Unityの初期化
    /// </summary>
    private void Start()
    {
        // プレイヤーのインデックスが設定されていたら
        if (playerIndex != 0)
        {
            // Playerを見つける
            GameObject playerParent = GameObject.Find("Player");

            // Playerがあったら
            if (playerParent != null)
            {
                // Playerが見つかった場合、親を設定
                transform.SetParent(playerParent.transform);
            }
            // なかったら
            else
            {
                Debug.LogError("Playerの親が見つかりませんでした。");
            }

            // プレイヤー1なら
            if (playerIndex == 1)
            {
                // 親の変更で位置とスケールがおかしくなるので修正
                transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
                transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
            }
            // プレイヤー2なら
            else if (playerIndex == 2)
            {
                // 親の変更で位置とスケールがおかしくなるので修正
                transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
                transform.position = new Vector3(8.0f, 0.0f, 0.0f);
            }
        }
    }

    /// <summary>
    /// RPCの初期化
    /// </summary>
    [Rpc]
    public void RpcInit()
    {
        // Playerを見つける
        GameObject playerParent = GameObject.Find("Player");

        // Playerがあったら
        if (playerParent != null)
        {
            // Playerが見つかった場合、親を設定
            transform.SetParent(playerParent.transform);
        }
        // なかったら
        else
        {
            Debug.LogError("Playerの親が見つかりませんでした。");
        }

        // プレイヤー1なら
        if (playerIndex == 1)
        {
            // 親の変更で位置とスケールがおかしくなるので修正
            transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
            transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
        }
        // プレイヤー2なら
        else if (playerIndex == 2)
        {
            // 親の変更で位置とスケールがおかしくなるので修正
            transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
            transform.position = new Vector3(8.0f, 0.0f, 0.0f);
        }
    }

    /// <summary>
    /// 更新
    /// </summary>
    public override void FixedUpdateNetwork()
    {
        MovePlayer();
    }

    /// <summary>
    /// プレイヤーの移動
    /// </summary>
    void MovePlayer()
    {
        // サーバーなら
        if (NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // プレイヤーの入力データを取得、できなかったら処理しない
            if (NetworkRunnerLauncher.Instance.runner.TryGetInputForPlayer(Object.InputAuthority, out PlayerInputData inputData))
            {
                // プレイヤーの入力を取得
                float moveSpeed = 5f;

                // 入力に応じて物理移動
                Vector2 moveDir = new Vector2(inputData.horizontal, inputData.vertical);
                Vector2 newPos = rigidbody2D.position + moveDir * moveSpeed * NetworkRunnerLauncher.Instance.runner.DeltaTime;

                // リジットボディで移動
                rigidbody2D.MovePosition(newPos);
            }
        }
    }

    /// <summary>
    /// プレイヤーのインデックス設定
    /// </summary>
    /// <param name="index">プレイヤーのインデックス</param>
    [Rpc]
    public void RpcSetPlayerIndex(int index)
    {
        // プレイヤーのインデックスが設定されていなかったら
        if (playerIndex == 0)
        {
            playerIndex = index;
        }
    }
}

プレイヤースクリプト解説

変数宣言

    [SerializeField]
    [Header("リジットボディ")]
    Rigidbody2D rigidbody2D;

    [Networked]
    [SerializeField]
    [Header("プレイヤーのインデックス")]
    int playerIndex { get; set; } = 0;
  • rigidbody2Dで移動を行うのでPlayerのrigidbody2Dをアタッチしてください。
  • プレイヤーが1なのか2なのかを判定するためにintの変数を用意します。
     - [Networked]を使用するとそのプロパティがネットワーク上で同期されます。

Photonのスポーン時

    /// <summary>
    /// Photonのスポーン時
    /// </summary>
    public override void Spawned()
    {
        // サーバーじゃないなら
        if (!NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
        }
        // サーバーなら
        else
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);
        }
    }
  • public override void Spawned()関数を作成するとNetworkRunnerSpawnをした時の処理を作成できます。
  • NetworkRunner.IsServerを使用して自身がサーバーなのかクライアントなのかを判定できます。
  • NetworkRunner.SetIsSimulated(NetworkObject, bool)を使用するとそのオブジェクトの物理挙動をシミュレートするかしないかを設定できます。

UnityのStart

    /// <summary>
    /// Unityの初期化
    /// </summary>
    private void Start()
    {
        // プレイヤーのインデックスが設定されていたら
        if (playerIndex != 0)
        {
            // Playerを見つける
            GameObject playerParent = GameObject.Find("Player");

            // Playerがあったら
            if (playerParent != null)
            {
                // Playerが見つかった場合、親を設定
                transform.SetParent(playerParent.transform);
            }
            // なかったら
            else
            {
                Debug.LogError("Playerの親が見つかりませんでした。");
            }

            // プレイヤー1なら
            if (playerIndex == 1)
            {
                // 親の変更で位置とスケールがおかしくなるので修正
                transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
                transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
            }
            // プレイヤー2なら
            else if (playerIndex == 2)
            {
                // 親の変更で位置とスケールがおかしくなるので修正
                transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
                transform.position = new Vector3(8.0f, 0.0f, 0.0f);
            }
        }
    }
  • UnityStartではplayerIndexが設定されていた場合に位置を初期化する処理を実装しています。

RPC属性のInit

    /// <summary>
    /// RPCの初期化
    /// </summary>
    [Rpc]
    public void RpcInit()
    {
        // Playerを見つける
        GameObject playerParent = GameObject.Find("Player");

        // Playerがあったら
        if (playerParent != null)
        {
            // Playerが見つかった場合、親を設定
            transform.SetParent(playerParent.transform);
        }
        // なかったら
        else
        {
            Debug.LogError("Playerの親が見つかりませんでした。");
        }

        // プレイヤー1なら
        if (playerIndex == 1)
        {
            // 親の変更で位置とスケールがおかしくなるので修正
            transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
            transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
        }
        // プレイヤー2なら
        else if (playerIndex == 2)
        {
            // 親の変更で位置とスケールがおかしくなるので修正
            transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
            transform.position = new Vector3(8.0f, 0.0f, 0.0f);
        }
    }
  • [RPC]をつける事によってRPCの実装ができ、プレイヤー1が実行するとサーバーとプレイヤー2に対しても関数の実行ができます。
     - RPCの実装は関数名にRPCを使用しないとエラーになります。
  • 他の処理内容に関してはUnityStartと同じです。

PhotonのFixedUpdate

   /// <summary>
    /// PhotonFixedUpdate
    /// </summary>
    public override void FixedUpdateNetwork()
    {
        MovePlayer();
    }

    /// <summary>
    /// プレイヤーの移動
    /// </summary>
    void MovePlayer()
    {
        // サーバーなら
        if (NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // プレイヤーの入力データを取得、できなかったら処理しない
            if (NetworkRunnerLauncher.Instance.runner.TryGetInputForPlayer(Object.InputAuthority, out PlayerInputData inputData))
            {
                // プレイヤーの入力を取得
                float moveSpeed = 5f;

                // 入力に応じて物理移動
                Vector2 moveDir = new Vector2(inputData.horizontal, inputData.vertical);
                Vector2 newPos = rigidbody2D.position + moveDir * moveSpeed * NetworkRunnerLauncher.Instance.runner.DeltaTime;

                // リジットボディで移動
                rigidbody2D.MovePosition(newPos);
            }
        }
    }
  • FixedUpdateNetworkを使用して更新をします。
     - このUpdateSimulation AlwaysSimulation Forwardの設定時のみ実行されます。
  • サーバーで動作をさせてクライアントに反映する形なのでNetworkRunnerLauncher.Instance.runner.IsServerでサーバーなら処理をする処理になっています。
  • NetworkRunner.TryGetInputForPlayer(Object.InputAuthority, out PlayerInputData inputData)を使用してプレイヤーの入力を取得しMovePositionで移動させます。

プレイヤーインデックスの設定

    /// <summary>
    /// プレイヤーのインデックス設定
    /// </summary>
    /// <param name="index">プレイヤーのインデックス</param>
    [Rpc]
    public void RpcSetPlayerIndex(int index)
    {
        // プレイヤーのインデックスが設定されていなかったら
        if (playerIndex == 0)
        {
            playerIndex = index;
        }
    }
  • プレイヤーのインデックス設定をサーバーの振り分けで行っているのでRPC属性にして引数のインデックスを受け取り設定します。

ここまでの実装

ここまでの実装でNetworkRunnerLauncherをアタッチしてplayerPrefabに作成したプレハブをアタッチしIsServerフラグでサーバーとクライアントを分けビルドしてサーバー1クライアント2で実行をすると、サーバーに対してプレイヤー1がとプレイヤー2が接続され左右に表示されるようになります。

プレイヤーの入力をサーバーで取得する

入力情報を保存する構造体を作成

using Fusion;

/// <summary>
/// プレイヤー入力のデータ構造体
/// </summary>
public struct PlayerInputData : INetworkInput
{
    // 縦方向の入力
    public float vertical;
    // 横方向の入力
    public float horizontal;
}
  • INetworkInputを継承したクラスを作成、中には入力データを保存できるようにします。

NetworkRunnerLauncherで入力を受け取る

~~~~~~~~~~~~~~~~~~~~~~~~NetworkRunnerLauncherに追加~~~~~~~~~~~~~~~~~~~~~~~~
    /// <summary>
    ///  キー入力したら
    /// </summary>
    /// <param name="networkRunner"></param>
    /// <param name="networkInput"></param>
    public void OnInput(NetworkRunner networkRunner, NetworkInput networkInput)
    {
        // クライアントなら
        if (!runner.IsServer)
        {
            // サーバーの接続ロジック
            ClientRunnerLogic.OnInputLogic(networkInput);
        }
    }
  • NetworkRunnerLauncherクラスのコールバックの1つであるOnInputを使用します。
    中身の処理に関してはサーバーと同じく切り分けています。
  • キー入力等がされると実行される関数でクライアントのみ処理したいので!runner.IsServerでクライアントのみ処理しています。

入力伝達処理

using UnityEngine;
using Fusion;

/// <summary>
/// クライアントのランナーロジック
/// </summary>
public static class ClientRunnerLogic
{
    /// <summary>
    /// 入力ロジック
    /// </summary>
    /// <param name="networkInput"></param>
    public static void OnInputLogic(NetworkInput networkInput)
    {
        // 入力構造体作成
        PlayerInputData data = new PlayerInputData();

        // 入力を代入
        data.vertical   = Input.GetAxis("Vertical");
        data.horizontal = Input.GetAxis("Horizontal");

        // 入力を送信
        networkInput.Set(data);
    }
}
  • ClientRunnerLogic.OnInputLogicでは入力情報を保存する構造体を作成で作成した構造体にUnityのInput.GetAxisから値を取得して保存しています。
  • NetworkInput.Set()を使用してサーバーに入力データを送信しています。
     - これでサーバーの更新処理でここで入力したデータをうけとれるようになります。

壁との当たり判定

壁にColliderをアタッチすればOKです、左右はトリガーにしています。

玉との当たり判定

玉のプレハブ設定

玉のプレハブもプレイヤーと同じく、NetWorkRigidbody2DCollider等を付けて終わりですが、反射等をさせたいのでPhysicsMaterial2Dを作成して画像のように設定します。
作成し終えたら

玉のスクリプト(全体)

using UnityEngine;
using Fusion;
using UniRx;

/// <summary>
/// ボール
/// </summary>
public class Ball : NetworkBehaviour
{
    [SerializeField]
    [Header("リジットボディ")]
    Rigidbody2D rigidbody2D;

    /// <summary>
    /// スポーン時
    /// </summary>
    public override void Spawned()
    {
        // サーバーじゃないなら
        if (!NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
        }
        // サーバーなら
        else
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);

            // 右側に押し出す
            rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
        }
    }

    /// <summary>
    /// トリガーイベント
    /// </summary>
    /// <param name="collision"></param>
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // サーバーなら
        if (NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // 3秒後に処理
            Observable.Timer(System.TimeSpan.FromSeconds(3)).Subscribe(_ =>
            {
                // 初期位置にする
                rigidbody2D.position = Vector2.zero;

                // 速度をリセット
                rigidbody2D.linearVelocity = Vector2.zero;

                // 右側に押し出す
                rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
            }).AddTo(this);
        }
    }
}

スポーン時

/// <summary>
    /// スポーン時
    /// </summary>
    public override void Spawned()
    {
        // サーバーじゃないなら
        if (!NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
        }
        // サーバーなら
        else
        {
            // 物理挙動をシミュレートする
            NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);

            // 右側に押し出す
            rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
        }
    }
  • スポーン時にはクライアント側ではプレイヤーと同じく物理挙動のシュミュレーションをoffにしサーバー側ではonにし、右側に押し出す処理をします。

ゴールの当たり判定

   /// <summary>
    /// トリガーイベント
    /// </summary>
    /// <param name="collision"></param>
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // サーバーなら
        if (NetworkRunnerLauncher.Instance.runner.IsServer)
        {
            // 3秒後に処理
            Observable.Timer(System.TimeSpan.FromSeconds(3)).Subscribe(_ =>
            {
                // 初期位置にする
                rigidbody2D.position = Vector2.zero;

                // 速度をリセット
                rigidbody2D.linearVelocity = Vector2.zero;

                // 右側に押し出す
                rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
            }).AddTo(this);
        }
    }
  • 左右の壁に当たるとゴールと見なし、初期位置に戻す処理にを実装します。
     - 移動をするのでサーバーのみの処理にし、UniRxで3秒後にリセットしています。

動作確認

ServerSceneGameSceneをそれぞれビルドします。

ServerScene

GameScene

実行結果

サーバーを起動しクライアントを起動してUnityから実行をすると、矢印キーで上下に移動ができ壁や玉と当たり判定が発生します。
(サーバー+クライアントx2で録画していたので少し重たいです)

ここまででわかった事

  • サーバーとクライアントを同じプロジェクトにするとかなりやりやすくなった
  • この方法で実行すると入力->移動までラグが体感でわかるぐらいある
  • RigidbodyColliderを使った当たり判定をするのにPhoton Fusion単体ではできなくて、公式からアドオンをインポートしないと当たり判定周りはできない。

次回はスコアとかマッチング、部屋分け等々をやってみたい。

ファースト・スクラッチTech Blog

Discussion