tanaka's Programming Memo

プログラミングについてのメモ。

Photon BoltでPhysicsを利用する(ver1.2.9)

f:id:am1tanaka:20181129195310g:plain
作例

こちら( Photon BoltでPhysicsのボールは動くのか - tanaka's Programming Memo )の記事の内容が古くなっていたので、2019/7/28現在の最新場である1.2.9に対応させて書き直しました。

目次

Photon Boltとは

マルチプレイヤーゲームのネットワーク部分を担当してくれるUnity用のネットワークエンジンです(ビジュアルスクリプティングのBoltの話ではございません!!Exit Gamesが提供しているネットワークエンジンの一つで、最近流行っている人が集まって対戦するようなリアルタイムネットワークゲームを開発するのに向いています。

詳しくは以下の公式サイトや、アセットストアをどうぞ。

www.photonengine.com

注意点!

Photon Boltの最大の注意点の一つは、現時点でWebGLに対応していない!!ということだと思います。1週間ゲームジャムとかでは使えませんのでご注意を!PUNの方は使えます。

目的

学園祭のような人が集まる場所で、同じ教室内のLAN環境で、エアホッケーのネットワーク対戦ゲームを動かしたいというのが今回の出発点です。ビルドターゲットはWindowsmacなどのPCです。最初のマッチング時にPhotonクラウドを通す必要があるのでインターネット接続は必要です。

エアホッケーならRigidbodyvelocityを同期できれば簡単に作れそうです。しかし、Photon Boltにはvelocityを同期するためのState設定がありません。そこで、Commandを使うことで対応させました。その構築手順です。

前提

はじめようを読んで、Photon Bolt Freeのインストールと概要を確認しておくのが望ましいです。

ブログではUnity2018.4.4f12019.1.5で動作確認しました。ブログ執筆時のPhoton BoltのUnityのサポートバージョンは2017.4.26以降です。

考え方

Photon BoltでPC間で同期したいデータは、Stateを作って、それにデータを割り当てます。そこにRigidbodyvelocityとかを同期する設定があればいいのですがありません。UNetにはあったので油断していました。

Photon Boltでは、Commandsを使って、クライアントからホストに操作情報を送らせて、サーバーでまとめて制御した結果をクライアントに戻して反映させる、という仕組みがあります。ざっくりこんな感じ。

f:id:am1tanaka:20181129142906p:plain
(public) RollerBallBoltChart | Sketchboard

1カ所でまとめて物理シミュレーションを行うので操作に対する反応は少し遅れますが、全てのプレイヤーの動きを一致させることができます。

今回は、CommandsでマウスのX-Yの移動量を送信して、プレイヤーの座標を結果(Result)として返すようにしました。

プロジェクトを作成

まずはProjectを作成します。作業中、指示通りにやっているのに動かない場合は、Unityを閉じてプロジェクトを開き直すとか、Unityが2017.4以降かを確認してください。

アカウントの入手とApp IDの取得

Sign In | Photon Engineを開いて、Photonアカウントの作成と、Bolt用のアプリを作成して、App IDを生成してください。

フィールドを読み込む

作例用のプレイヤーやフィールドのオブジェクトを用意しました。まずはそれらを組み込みます。

  • 新規にPhotonBoltPhysicsなどの名前でUnityのプロジェクトを作成してください。Unityのバージョンは2017.4以降の安定版がよいでしょう。このブログではUnity2018.4.4を使っています
  • こちらを開いてください
  • 最新版のPhotonBoltPhysics@v?.?.?.unitypackageをクリックしてダウンロードします
  • ダウンロードが完了したら、UnityのProjectウィンドウにドラッグ&ドロップして、インポートします

以上で、ゲーム用のオブジェクトが読み込まれます。Scenesフォルダー内のGameシーンをダブルクリックすると、フィールドとプレイヤー2つ、ボールが確認できます。

f:id:am1tanaka:20190728141043p:plain
Gameシーン

Photon Boltの初期設定

  • Asset StoreからPhoton Bolt Freeをダウンロードしてインポートします

Photon Boltは、同時接続20接続(マッチング後にサーバーを利用しなくなったアカウントも含みます)などの制約以内であれば、無料で利用できます。開発中や、今回のような試用の場合はFree版でいけます。

エラーが出るので、直すために設定をします。

  • Boltメニューがあるか確認します。見当たらなかったら、Unityを閉じてプロジェクトを開きなおします
  • BoltメニューからWizardを選んで、ウィンドウが開いたらNextをクリックします
  • PhotonのWebページで作成したApp IDをコピーして、Photon Bolt App ID or Email欄に入力します
  • 変なリージョンに行かれると接続ができなかったりしたので、テスト時はRegion[jp] Japan :: Tokyoにしておきます
  • NAT Punchthrough Enabledはチェックを入れたままにしておきます
  • 以上選択したらNext

f:id:am1tanaka:20181129125930p:plain

  • Sampleを有効にすると、公式のサンプルを見ることができます。今回は不要なので、そのままNextをクリックします
  • Doneをクリックします
  • Bolt Setup Completeダイアログが表示されます。コンパイルが必要なので、Yesをクリックします

以上でエラーが解消されます。エラーがまだ表示されていたら、ConsoleウィンドウのClearボタンをクリックすれば消えます。

Menuシーンの作成

サーバーとクライアントのどちらでログインするかを決めるためのボタンとサーバー接続のコードを実装したMenuシーンを作成します。

  • デフォルトで作成されるSampleSceneがあったら、名前をMenuに変更します
    • Unity2017だとシーンはないので、New Sceneで新しいシーンを作成して、名前をMenuにします
  • 作成したMenuシーンをダブルクリックして切り替えます
  • HierarchyウィンドウのMain Cameraをクリックして選択します
  • InspectorウィンドウのAdd Componentをクリックして、New scriptで新しいスクリプトを作成して、名前をMenuにします
  • 作成したMenuスクリプトをダブルクリックして開きます
  • 以下のスクリプトに置き換えます。Photon BoltアセットのGettingStartedサンプルに含まれているMenu.csを元にして、シーン名をGameに変更したものです
using System;
using System.Collections;
using System.Collections.Generic;
using Bolt.Matchmaking;
using UdpKit;
using UnityEngine;

namespace Bolt.Samples.GettingStarted
{
    public class Menu : Bolt.GlobalEventListener
    {
        bool ShowGui = true;

        private void Awake()
        {
            Application.targetFrameRate = 60;
            BoltLauncher.SetUdpPlatform(new PhotonPlatform());
        }

        void OnGUI()
        {
            if (!ShowGui) { return; }

            GUILayout.BeginArea(new Rect(10, 10, Screen.width - 20, Screen.height - 20));

            if (GUILayout.Button("Start Server", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)))
            {
                // START SERVER
                BoltLauncher.StartServer();
            }

            if (GUILayout.Button("Start Client", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)))
            {
                // START CLIENT
                BoltLauncher.StartClient();
            }

            GUILayout.EndArea();
        }

        public override void BoltStartBegin()
        {
            ShowGui = false;
        }

        public override void BoltStartDone()
        {
            if (BoltNetwork.IsServer)
            {
                string matchName = Guid.NewGuid().ToString();

                BoltMatchmaking.CreateSession(
                 sessionID: matchName,
                 sceneToLoad: "Game"  // 元のファイルからここを変更
                );
            }
        }

        public override void SessionListUpdated(Map<Guid, UdpSession> sessionList)
        {
            Debug.LogFormat("Session list updated: {0} total sessions", sessionList.Count);

            foreach (var session in sessionList)
            {
                UdpSession photonSession = session.Value as UdpSession;

                if (photonSession.Source == UdpSessionSource.Photon)
                {
                    BoltNetwork.Connect(photonSession);
                }
            }
        }
    }
}

Playすると、以下のようにメニューが表示されます。

f:id:am1tanaka:20190728162702p:plain
Menu画面

まだ設定などが足りないので、ボタンを押してもエラーになります。Playを停止して設定を続けます。

ビルド設定

チャプター1 | Photon Engineを参考にビルド設定をします。

  • FileメニューからBuild Settingsを選択します
  • ProjectメニューからMenuシーンをドラッグして、Scenes In Build欄にドロップします
  • 同様に、Gameシーンもドラッグ&ドラッグでScenes In Build欄に追加します

f:id:am1tanaka:20190728161910p:plain
ビルドシーン

  • Player Settings...ボタンをクリックします
  • Resolution and Presentation欄をクリックして開いて、以下を設定します
    • Fullscreen ModeWindowsに設定
    • Default Screen Width640に設定
    • Default Screen Height360に設定
    • Run In Backgroundにチェックを入れます
    • Display Resolution DialogDISABLEDにします

以上は開発用の設定です。開発が完了したら適宜変更してください。

  • Build Settingsウィンドウを閉じます
  • Boltメニューから、Compile Assemblyを選択します。これをやらないと、シーンがBoltに認識されないので動きません

以上設定したらPlayして、Start Serverボタンをクリックしてください。Gameシーンに切り替わります。

f:id:am1tanaka:20190728164339p:plain
Gameシーン

Gameシーンの設定を全くしていないので操作はできません。停止して、Gameシーンの実装をします。

Gameシーンを作る

ネットワークに接続して、Gameシーンに切り替えることができたので、Gameシーン本体の実装を始めます。

Stateの作成

Boltでネットワークに登場させるゲームオブジェクトは、同期するデータを定義したStateを持ちます。今回はTransformだけ同期します。そのためのTransformStateを作成します。

  • BoltメニューからAssetsを選択します
  • Bolt Assetsウィンドウの何もない部分を右クリックして、New Stateを選択します
  • 以下の通り設定します

f:id:am1tanaka:20190728165116p:plain
TransformStateの作成

  • 設定したら、Bolt Editorウィンドウは閉じます

利用するプロパティはTransform型のTransformだけです。

SpaceはデフォルトがLocalだったのでそのままにしました。今回は使っていませんが、StateCommandなどの設定にCompression(圧縮)の項目があって、そこで値の範囲を制限して送信するデータ量を減らすことができます。SpaceLocalにしておくことで、設定した範囲を越える移動は親のオブジェクト側で行うような工夫ができそうです。

捕捉: クライアントの動きを滑らかにする設定(補間とNetwork Rate)

Smoothing AlgorithmNoneのままにしました。Interpolation(内挿)とExterpolation(外挿)の違いは以下にあります。

Interpolation vs. Extrapolation | Photon Engine

ざっくりと、Interpolationは過去のデータを利用して動きを滑らかに見せます。過去の場所の間に補間するので、突飛な場所に移動することはありません。ただし、少し古い座標に表示されることになります。

Exterpolationは過去の動きの傾向から、次の座標を予想して動かします。レスポンスは速いですが、予想が外れた場合はいたことがない座標に移動してしまいます。

今回のようにマウス操作&Physicsベースだと、動きの予想がしずらいのでExterpolationでの予測精度があまりよくありませんでした。また、Interpolationは遅延が気になります。

ということで、Noneのままにして補間はしないことにしました。そのままではガタついて見えてしまいますが、これはStateの送受信の頻度が原因です。最後の方の設定の調整のところで調整します。

Commandの作成

今回の肝であるクライアントからマウスの移動量を送り、座標を返してもらうためのCommandを作成します。

  • Bolt Assetsウィンドウの余白を右クリックして、New Commandを選択します
  • 以下のように設定します

f:id:am1tanaka:20190728165856p:plain
RollerBallBoltCommandの作成

  • 以上設定したら、Bolt Editorウィンドウと、Bolt Assetsウィンドウを閉じます

入力のMouseVector型で、動かす時に楽をするためにXZで送ることにしました。

結果はXYZ全て利用するVector型で、そのままローカル座標に放り込みます。効果がよくわかりませんでしたが、なんとなく奇麗に動いている気がするのでSmooth Corrections(スムーズに補正する)にチェックを入れました。あとは以前の設定にもあったのでTeleport Threshold10を設定しました。

ボールプレハブの作成

ゲームオブジェクトをネット対応させます。ネットワーク上で共有するオブジェクトにはBoltEntityスクリプトをアタッチして、ネットワークに接続した時にBoltNetwork.Instantiate()で生成します。そして、Stateのデータをオブジェクトのプロパティーと関連付けます。

  • Projectウィンドウから、Gameシーンをダブルクリックしてシーンを切り替えます
  • ProjectウィンドウのRollerBallBolt > Prefabsフォルダーを開いて、Ballプレハブをクリックして選択します
  • InspectorウィンドウでOpen Prefabボタンをクリックします(Open Prefabボタンがない場合はそのまま編集します)
  • InspectorウィンドウのAdd Componentボタンをクリックして、Bolt Entinyを検索などして見つけてアタッチします

アタッチした直後はエラーになります。以下を設定します。

  • Ballプレハブを再び開いて、State欄をITransformStateにします

f:id:am1tanaka:20190728170730p:plain
Stateを設定

  • BoltメニューからCompile Assemblyを選択してコンパイルします

以上でエラーがなくなります。

  • その他の設定も確認して、以下のようにします。TagBallを予め割り当てています

f:id:am1tanaka:20181129195533p:plain
Ball Inspector

  • InspectorウィンドウからAdd Component > New scriptを選択して、Ballという名前のスクリプトを作成します
  • Ball.csは以下の通りです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace PhotonBoltPhysics
{
    public class Ball : Bolt.EntityBehaviour<ITransformState>
    {
        /// <summary>
        /// transformをIPlayerStateのTranformに割り当てます。
        /// </summary>
        public override void Attached()
        {
            state.SetTransforms(state.Transform, transform);
        }
    }
}

アタッチされた時にtransformStateTransformに割り当てているだけです。

プレイヤーの作成

プレイヤーも同様にネットワーク対応させていきます。

以下は設定済みです。

  • Player1Player2とも、TagPlayer
  • Sphere Colliderをアタッチして、良い場所と大きさに調整
  • Rigidbodyをアタッチして、Use Gravityを外し、Collision DetectionContinuous DynamicFreeze PositionYにチェック、Freeze Rotationを全てチェック

ネットワーク対応させるために、以下を設定します。

  • ProjectウィンドウからPlayer1プレハブをクリックして選択します
  • InspectorウィンドウにOpen Prefabボタンがあったらクリックします
  • InspectorウィンドウのAdd Componentボタンをクリックして、Bolt Entityを選択してアタッチします
  • InspectorウィンドウのStateITransformStateに設定します
  • BoltメニューからCompile Assemblyを選択して実行したら、Player1プレハブを開き直します
  • 新規スクリプトを作成して、Playerの名前にします
  • Player.csスクリプトの中身は以下の通りです。
using System.Collections;
using System.Collections.Generic;
using Bolt;
using UnityEngine;

namespace PhotonBoltPhysics
{

    public class Player : Bolt.EntityBehaviour<ITransformState>
    {

        [Tooltip("送られてきた操作を速度に変換するレート。Mouse XとMouse Yは-1~1に正規化されているので、動作環境の解像度は考えなくて構いません。"), SerializeField]
        float input2Speed = 20f;

        Rigidbody rb;

        float _x;
        float _y;

        #region System Loop

        private void Awake()
        {
            rb = GetComponent<Rigidbody>();
        }

        private void Update()
        {
            PollMouse();
        }

        #endregion System Loop

        #region Bolt Events

        /// <summary>
        /// transformをIPlayerStateのTranformに割り当てます。
        /// </summary>
        public override void Attached()
        {
            state.SetTransforms(state.Transform, transform);
        }

        /// <summary>
        /// 操作権を持つプレイヤー(Player1はホスト、
        /// Player2はクライアント)で呼び出されます。
        /// 操作をBoltEntityに渡します。
        /// </summary>
        public override void SimulateController()
        {
            IRollerBallBoltCommandInput input = RollerBallBoltCommand.Create();

            Vector3 data = new Vector3(_x, 0, _y);
            input.Mouse = data;

            entity.QueueInput(input);
        }

        /// <summary>
        /// オブジェクトのオーナーで呼び出されます。
        /// これはPlayer1, 2ともにホストで呼び出されます。
        /// 入力を受け取って動かします。
        /// Player2からは、クライアントに結果を送信します。
        /// </summary>
        /// <param name="command">送られてきた操作コマンド</param>
        /// <param name="resetState">操作権を持っていたらtrue</param>
        public override void ExecuteCommand(Command command, bool resetState)
        {
            RollerBallBoltCommand cmd = (RollerBallBoltCommand)command;

            if (resetState)
            {
                // Player2。送られてきたコマンドのデータを反映させます
                transform.localPosition = cmd.Result.Position;
            }
            else
            {
                // 入力を使ってオブジェクトを動かします
                rb.velocity = cmd.Input.Mouse * input2Speed;

                // ホストとクライアントの双方で呼び出されます
                // 現在の座標を送信します
                cmd.Result.Position = transform.localPosition;

            }
        }

        #endregion Bolt Events

        #region My Methods

        void PollMouse()
        {
            _x = Input.GetAxis("Mouse X");
            _y = Input.GetAxis("Mouse Y");
        }

        #endregion My Methods

    }
}

Advencedチュートリアルをベースに改造したものです。ポイントは以下の通りです。

  • Bolt.EntityBehaviour<ITransformState>を継承します
  • Update()でマウスの移動量を記録しておきます
  • アタッチされた時に、transformStateTransformに割り当てるコードをAttached()メソッドに実装します
  • SimulateController()は、操作をシミュレートする際にBoltから呼び出されるメソッドです。ここで、Update()で記録しておいたマウスの移動量をRollerBallBoltCommandInputMouseに設定して、QueueInput()で登録します
  • ExecuteCommand(command, resetState)に、コマンドを実行するための処理を実装します。commandは送受信するためのRollerBallBoltCommandインスタンスresetStateはサーバーかクライアントのどちらで動いているかを表していて、trueの時はコントローラー(クライアント)側の処理、つまり、送られてきた結果を自分のローカル座標に反映させます。falseの時は、オーナー(サーバー)側の処理で、QueueInput()で蓄えられた入力データを速度に反映させて、オブジェクトを動かし、結果をcommandに渡します

Player2の作成

同様にPlayer2も設定します。以下、ざっとした手順です。

  • Player2プレハブを開きます
  • Bolt Entityをアタッチ
  • 作成済みのPlayerスクリプトをアタッチ
  • StateITransformStateに設定
  • BoltメニューからCompile Assemblyを選択

以上でプレイヤーの設定完了です。

フィールド

枠や地面などのフィールドは、ネットワークで共有する必要はないのでそのまま配置しておけばOKです。

f:id:am1tanaka:20181129203141p:plain

オブジェクトを配置する機能の作成

公式のチュートリアルでは、ランダムに座標を設定したりしてスクリプト側で座標を作っていましたが、最初から登場させたい場所にゲームオブジェクトを配置して、そこにInstantiateした方が楽だろうということで作った機能です。

  • 新規に空のGame Objectを作成して、NetworkSceneManagerなどの名前にします
  • 新しいスクリプトを作成して、NetworkSceneManagerなどの名前にします

スクリプトは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace PhotonBoltPhysics
{
    public struct OBJECT_RECORD
    {
        public int id;
        public Vector3 position;
    }

    public class NetworkSceneManager : MonoBehaviour
    {
        public static NetworkSceneManager Instance
        {
            get;
            private set;
        }

        public List<OBJECT_RECORD> playerPositions {
            get;
            private set;
        }
        public OBJECT_RECORD ballPosition
        {
            get;
            private set;
        }

        private void Awake()
        {
            Instance = this;

            playerPositions = new List<OBJECT_RECORD>();

            GameObject[] gos = GameObject.FindGameObjectsWithTag("Player");
            OBJECT_RECORD or;
            foreach(GameObject go in gos)
            {
                or.id = go.GetComponent<BoltEntity>().PrefabId.Value;
                or.position = go.transform.position;
                playerPositions.Add(or);
                Destroy(go);
            }

            GameObject ball = GameObject.FindGameObjectWithTag("Ball");
            or.id = ball.GetComponent<BoltEntity>().PrefabId.Value;
            or.position = ball.transform.position;
            ballPosition = or;
            Destroy(ball);
        }

        /// <summary>
        /// BoltEntity.prefabId.Valueを受け取って、該当する
        /// プレハブIDで記録された座標を返します。
        /// </summary>
        /// <param name="id">プレハブID</param>
        /// <returns>座標を返します</returns>
        public Vector3 GetPlayerPosition(int id)
        {
            foreach(OBJECT_RECORD or in playerPositions)
            {
                if (or.id == id)
                {
                    return or.position;
                }
            }
            return Vector3.zero;
        }
    }
}

Awake()で、PlayerBallのタグが付いたオブジェクトを検索して、プレハブIDと座標を記録したらDestroyします。サーバーでオブジェクトを生成する際に、プレハブIDで記録しておいた座標を検索して、その座標にゲームオブジェクトを配置しています。

ネットワーク管理スクリプト

最後に、シーンが始まった時に、プレイヤーやボールを生成するためのオブジェクトとスクリプトです。これは、ゲームオブジェクトにはアタッチしません。新規にスクリプトを作成したら、PhotonBoltPhysicsServerCallbacksなどの名前にします。コードは以下の通りです。

using UnityEngine;

namespace PhotonBoltPhysics
{
    [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")]
    public class PhotonBoltPhysicsServerCallbacks : Bolt.GlobalEventListener
    {
        /// <summary>
        /// このプログラムがシーンを読み込んだ時の処理
        /// </summary>
        /// <param name="map"></param>
        public override void SceneLoadLocalDone(string map)
        {
            // プレイヤー1を生成して、操作を担当します
            BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player1);
            be.transform.position = NetworkSceneManager.Instance.GetPlayerPosition(be.PrefabId.Value);
            be.TakeControl();

            // ボールを作ります
            be = BoltNetwork.Instantiate(BoltPrefabs.Ball);
            be.transform.position = NetworkSceneManager.Instance.ballPosition.position;
        }

        /// <summary>
        /// クライアントがシーンを読み込んだ時に報告されるコールバック
        /// リモート用のプレイヤーを生成
        /// </summary>
        /// <param name="connection"></param>
        public override void SceneLoadRemoteDone(BoltConnection connection)
        {
            // プレイヤー2を生成して、操作を接続先に任せます
            BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player2);
            be.transform.position = NetworkSceneManager.Instance.GetPlayerPosition(be.PrefabId.Value);
            be.AssignControl(connection);
        }
    }
}

クラスは、Bolt.GlobalEventListenerを継承します。

    // :
    [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")]

上記の属性により、このクラスはサーバー、かつ、Gameシーンのみで実行されます。

SceneLoadLocalDone()は、サーバーのGameシーンが準備できた時にサーバーで実行するコードです。Player1Ballを生成して、Player1はサーバー自身が制御するのでTakeControl()を呼び出します。

SceneLoadRemoteDone()は、クライアントのGameシーンが準備できた時にサーバーで実行するコードです。Player2を生成して、操作はクライアントが担当するので、AssignControl()で接続先(connection)に割り当てます。

制作過程は、おおよそ以上の通りです。

動作確認

Photon Boltでは、開発を楽にするために実行形式のファイルを自動的にビルドして、ホストとクライアントで起動してくれる機能があります。それを使ってGameシーンをネットワークで実行してみます。

  • BoltメニューからScenesを選択します
  • Bolt Scenesウィンドウで、Clients欄を1にします。これで、クライアントアプリが1つ起動するようになります
  • Gameと書いてある欄の右のDebug Startをクリックします
  • Saveするかの確認ダイアログが表示されたら、Saveをクリックします

f:id:am1tanaka:20181128160121p:plain

ビルドが始まって、しばらく待つとアプリが1つ起動します。Unityでサーバー、クライアントアプリでクライアントが動きます。マウスを動かすと、アクティブなウィンドウのプレイヤーが操作できます。

f:id:am1tanaka:20190728185734p:plain
完成!!

設定の調整

Photon Boltの設定がデフォルトのままなので、クライアント側の動きがガタガタで、オブジェクトに雷マークが表示されています。これらを直すための設定は以下の通りです。

  • Windowメニューから、Bolt > Settingsを選択します
  • Network Rate1にします
    • これで、状態の同期を1フレームごとに行うようになるので、クライアントの動きがスムーズになります
    • ネット環境によっては、動きがおかしくなることがあるので、その時は2にしたり3に戻したり調整してみてください
  • 必要に応じて、Miscellanceous欄の以下のチェックを外します
    • Show Debug Info
      • オブジェクトに表示されるBoltアイコンが消えます
    • Show Help Buttons
      • ヘルプボタンを非表示にします
    • Visible By Default
      • 情報ウィンドウをデフォルトで非表示にします

f:id:am1tanaka:20190728185925p:plain
本当に完成!!

まとめ

Physicsで制御するゲームオブジェクトを、Photon Boltを使ってネットワーク上で共有することができました。

UNetに比べると手順はやや多い印象ですが、動いたあとの安心感があります。サービス停止の心配ないし...。

エアホッケーブロック崩しピンボール、コインプッシャーなど、Physicsを使って簡単に作れるタイプのゲームをネットワーク対応させるのに、今回の手法は有用と思います。

WebGLが使えたらなぁ...と願いつつ。

最後の最後に: Physicsの同期について

Photon Unity Networking(PUN)では、PhysicsのRigidbodyのvelocityを同期するためのクラスPhotonRigidbodyViewがあります。

Photon Unity Networking: PhotonRigidbodyView Class Reference

Photon Boltでも、Statevelocity用のVector3を追加して速度の同期を試みることはできます。ただ、このAPIリファレンスの解説にある通り、Rigidbodyのvelocityを同期するだけではネットワーク間のオブジェクトの動きが変わってしまう可能性があります。

今回のように、物体の衝突による動きが直接ゲーム性に関わる場合、PC間の挙動の違いでボールの飛ぶ方向が変わるのは困ります。座標を送って補正したとしても、動きが不自然になることが予想されます。そのため、Commandsで操作を送ってサーバーがまとめて処理する方式にしました。

Rigidbodyのvelocityを使えば、各PC上で座標計算ができるので動きを綺麗にすることができます。弾などの単純な軌道で、衝突時に跳ね返る必要がないようなオブジェクトの場合は、Rigidbodyのvelocityを同期する手法は使えると思います。どのような動きをさせたいかで採用する技術が変化してくるので、研究のし甲斐があると思います。今回と同じ内容のものを、PUNのvelocity同期で実装したらどうなるかも見てみたいところです。

参考URL

www.photonengine.com

assetstore.unity.com