2019/7/28現在、この記事の内容が古くなっていたので以下にPhoton Bolt 1.2.9対応に書き直しました。以下のリンク先の記事をご覧ください。
アドベントカレンダー1日目(12/1)の記事です。
明日はRaspberlyさんの3Dモデルの影を別の影に差し替える - Raspberlyのブログです!
- (2018/12/12 Network Rateの設定で動きを滑らかにすることと、Boltアイコンの消し方について追記しました)
- (2018/12/7 PUNのvelocityの同期について追記しました)
目次
- 2019/7/28現在、この記事の内容が古くなっていたので以下にPhoton Bolt 1.2.9対応に書き直しました。以下のリンク先の記事をご覧ください。
- 目次
- Photon Boltとは
- 目的
- 前提
- 動作確認
- プロジェクトの準備手順
- 実装方法の解説
- 制作過程の概要
- オブジェクトのBoltアイコンを消すには(2018/12/12追記)
- まとめ
- Physicsの同期について(2018/12/7追記)
- 参考URL
Photon Boltとは
マルチプレイヤーゲームのネットワーク部分を担当してくれるUnity用のネットワークエンジンです(ビジュアルスクリプティングのBoltの話ではございません!!)Exit Gamesが提供しているネットワークエンジンの一つで、最近流行っている人が集まって対戦するようなリアルタイムネットワークゲームを開発するのに向いています。
詳しくは以下の公式サイトや、アセットストアをどうぞ。
注意点!
Photon Boltの最大の注意点の一つは、現時点でWebGLに対応していない!!ということだと思います。1週間ゲームジャムとかでは使えませんのでご注意を!PUNの方は使えます。
目的
学園祭のような人が集まる場所で、同じ教室内のLAN環境で、エアホッケーのネットワーク対戦ゲームを動かしたいというのが今回の出発点です。ビルドターゲットはWindowsやmacなどのPCです。
エアホッケーならRigidbody
のvelocity
を同期できれば簡単に作れそうです。しかし、Photon Boltにはvelocity
を同期するためのState設定がありません。そこで、ブログタイトルということになります。結論から言えば、
動きます!!
このブログでは、実際に動かした習作プロジェクトを動かす手順と、どのように設定をしたのかをご紹介します。
前提
UnityへのPhoton Bolt Freeのインストールと、はじめようを試しておくのが望ましいです。
ブログのUnityは2017.4.15f1です。2018以降だと一部メニューの場所が異なるのでご注意ください。ブログ執筆時のPhoton BoltのUnityのサポートバージョンは2017.4.13以降です。
動作確認
とりあえず動きが見てみたい!という方は、Windowsとmacそれぞれの実行ファイルを用意しましたので、以下の手順でお試しください。
- こちらを開きます
- Windows用はRollerBallBolt.zip、mac用はRollerBallBolt-mac.zipです。最新版のものをダウンロードして、適当な場所に展開します
RollerBallBolt
の実行ファイルを2つ起動します。あるいは、2台のPCでそれぞれ1つずつ起動します- 好きな画面サイズ、あるいはフルスクリーンを選択して、Play!をクリックして起動します
- 以下のようなメニューが起動するので、先に起動する方でStart Serverを押します
- 接続が完了して、赤いプレイヤーとボールが表示されたら、もう一方でStart Clientをクリックします
- 接続が完了したら、それぞれのPCでマウス操作して、ボールを打ち合えます
ログが邪魔な場合(というか邪魔なので)、[Tab]キーを押すと非表示にできます。
プロジェクトの準備手順
プロジェクトをUnityで動かすための手順です。Photon Boltは、途中から組み込むのがちょっと面倒だったりします。手順にお気をつけください。指示通りにやっているのに動かない場合、Unityを閉じてプロジェクトを開き直すとか、Unityが2017.4以降かを確認してみてください。
アカウントの入手とApp IDの取得
Sign In | Photon Engineを開いて、Photonアカウントの作成と、Bolt用のアプリを作成して、App IDを生成してください。
リポジトリーのダウンロード
- こちらを開いてください
- Cloneするか、Download ZIPでリポジトリーをダウンロードして、作業をしたいフォルダーに展開してください
- Unity2017.4.13以降で、プロジェクトを開きます
この時点ではエラーが大量に出ます。引き続き、以下を設定します。
初期設定
- Asset StoreからPhoton Bolt Freeをダウンロードしてインポートします。
bolt/project.bytes
はこちらで用意したものを利用したいのでチェックを外してください
Photon Boltは、同時接続20接続(マッチング後にサーバーを利用しなくなったアカウントも含みます)などの制約以内であれば、無料で利用できます。開発中や、今回のような試用の場合はFree版でいけます。
まだエラーが出ます。設定を続けます。
- Assetsメニューを開いて、Boltがメニューにあるか確認します。見当たらなかったら、Unityを閉じてプロジェクトを開きなおします
- Assetsメニューから、Bolt > Compile Assemblyを選んで、パッケージをコンパイルします。ビルドが完了するのを待ってください
- Windowメニューから、Bolt > Wizardを選んで、ウィンドウが開いたらNextをクリックします
- PhotonのWebページで作成したApp IDを入力します
- 変なリージョンに行かれると接続ができなかったりしたので、テスト時はRegionを[jp] Japan :: Tokyoにしておきます
- 以上選択したらNext
- Core Packageをクリックしてデータをインポートしたら、Next
- Doneをクリック
- Assetsメニューから、Bolt > Compile Assemblyを実行
以上で、エラーがなくなり、構築完了です。最後に、ビルドの際にエラーの原因になるのでコメントアウトしておいたコードを復活させます。
- Projectウィンドウから、RollerBallBolt > Scriptsフォルダーを開いて、Menu.csをダブルクリックして開きます
10
行目付近になる/*
と、54
行目付近にある*/
を削除します- 上書きしてUnityに戻ります
以上でプロジェクトの設定は完了です。動作を確認しましょう。
動作確認
Photon Boltでは、開発を楽にするために実行形式のファイルを自動的にビルドして、ホストとクライアントで起動してくれる機能があります。それを使ってGameシーンをネットワークで実行してみます。
- Windowメニューから、Bolt > Scenesを選択します
- Bolt Scenesウィンドウで、Clients欄を
1
にします。これで、クライアントアプリが1つ起動するようになります - Gameと書いてある欄の右のDebug Startをクリックします
ビルドが始まって、しばらく待つとアプリが1つ起動します。起動したアプリのPlay!ボタンをクリックすると、Unityでホスト、クライアントアプリでクライアントが動きます。マウスを動かすと、Player1(赤いやつ)とPlayer2(青いやつ)が同時に動きます。1台のPCで動かしているので、どちらも同じマウスの入力で動くからです。
設定の調整(2018/12/12追記)
Photon Boltの設定がデフォルトのままなので、クライアント側の動きがガタガタで、オブジェクトに雷マークが表示されています。これらを直すための設定は以下の通りです。
- Windowメニューから、Bolt > Settingsを選択します
- Network Rateを
1
にします- これで、状態の同期を1フレームごとに行うようになるので、クライアントの動きがスムーズになります
- ネット環境によっては、動きがおかしくなることがあるので、その時は
2
にしたり3
に戻したり調整してみてください
- 必要に応じて、Miscellanceous欄の以下のチェックを外します
- Show Debug Info
- オブジェクトに表示されるBoltアイコンが消えます
- Show Help Buttons
- ヘルプボタンを非表示にします
- Visible By Default
- 情報ウィンドウをデフォルトで非表示にします
- Show Debug Info
サンプルプロジェクトの動かし方については以上です。ここからは、このプロジェクトの解説です。
実装方法の解説
Photon BoltでPC間で同期したいデータは、Stateを作って、それにデータを割り当てます。そこにRigidbodyのvelocityとかを同期する設定があればいいのですがありません。UNetにはあったので油断していました。
Photon Boltでは、Commandsを使って、クライアントからホストに操作情報を送らせて、サーバーでまとめて制御した結果をクライアントに戻して反映させる、という仕組みがあります。ざっくりこんな感じ。
こちらの方が正確な物理シミュレーションが可能で、誤差も少なくなることが期待できます。ということで、CommandsでマウスのX-Yの移動量を送信して、プレイヤーの座標を結果として返すようにしました。
ほとんどの処理はサーバー側で行って、クライアントは端末扱いです。これをどのように実装したかを大まかにまとめます。詳しいところは公式ドキュメントなどをどうぞ。
制作過程の概要
プロジェクトの作成
新規プロジェクトを作成して、以下をやりました。
- Asset StoreからPhoton Boltのインポート
- AssetsメニューからBolt > Compile Assemblyを実行
- Windowメニューから、Wizardを起動して、Boltのコアパッケージのインストール、App ID、リージョンの設定
- AssetsメニューからBolt > Compile Assemblyを実行
シーンの作成
サーバーとクライアントのどちらでログインするかを決めるためのボタンと、サーバー接続のコードを実装したMenuシーンと、Gameシーンの2つを用意しました。
Menuシーン
空のシーンを作成して、Main CameraにBolt 102 - はじめに | Photon EngineにあるMenu.cs
をアタッチしました。シーン名がサンプルのものと違う場合は、そこを変更するのを忘れずに。
Gameシーン
ゲームに必要なオブジェクトを作って配置します。
ビルド設定
シーンができたらビルド設定をします。作業する前に、Assets > Bolt > Compile Assemblyを実行します。Photon Boltは、シーンやオブジェクト、スクリプトなどを事前に把握する仕組みがあり、Compile Assemblyをしないと新しく作成したものがプロジェクトに反映されず、エラーの原因になったりします。
ビルド設定は、1-プロジェクト設定 | Photon Engineの「シーンを立ち上げる前に、全てが動作していることを確認するため、・・・」以降の手順が参考になります。
Stateの作成
PC間で同期するデータを決めるStateを作ります。今回のプロジェクトは物凄くシンプルなので、プレイヤーもボールもTransformのみを共有すればOKです。ということで、以下のようなTransformState
を作りました。
利用するプロパティはTransform
型のTransform
だけです。
SpaceはデフォルトがLocalだったのでそのままにしました。親子階層がないのでWorldでもよさそうではあります。この辺は調査不足で、明確な理由があってこうしている訳ではありません。
クライアントの動きを滑らかにする設定(補間とNetwork Rate)
Smoothing AlgorithmはNoneのままにしました。Interpolation(内挿)とExterpolation(外挿)の違いは以下にあります。
ざっくりと、Interpolationは過去のデータを利用して動きを滑らかに見せます。過去の場所の間に補間するので、突飛な場所に移動することはありません。ただし、少し古い座標に表示されることになります。
Exterpolationは過去の動きの傾向から、次の座標を予想して動かします。レスポンスは速いですが、予想が外れた場合はいたことがない座標に移動してしまいます。
今回のようにマウス操作&Physicsベースだと、動きの予想がしずらいのでExterpolationでの予測精度があまりよくありませんでした。また、Interpolationは遅延が気になります。
ということで、Noneのままにして補間はしないことにしました。そのままではガタついて見えてしまいますが、これはStateの送受信の頻度が原因です。デフォルトの設定では3フレームに1回同期する設定になっています。今回の場合、送信するデータが非常に少ないので、1フレームに1回にするという力業で補間せずに済ませました。この辺は設定のコツがあるかも知れません。今後の調査項目です。
設定方法は以下の通りです。
- WindowメニューからBolt > Settingsを選択します
- Network Rateを
1
に変更します
以上です。ネット環境によっては、動きがおかしくなる場合があります。その際は値を戻したり2
にしたりして調整してみてください。(2018/12/12追記)
Commandの作成
クライアントのデータをやりとりするために、以下のようなRollerBallBoltCommandを作成しました。
入力のMouseはVector型で、動かす時に楽をするためにXとZで送ることにします。
結果はXYZ全て利用するVector型で、そのままローカル座標に放り込みます。
効果がよくわかりませんでしたが、なんとなく奇麗に動いている気がするのでSmooth Corrections(スムーズに補正する)にチェックを入れました。
ボールプレハブの作成
ゲームオブジェクトをネット対応させます。ネットワーク上で共有するオブジェクトにはBoltEntity
スクリプトをアタッチして、ネットワークに接続した時にBoltNetwork.Instantiate()
で生成します。
ボールは、Rigidbodyをくっつけて、位置を同期させるだけです。Sphereを作成して、以下の通りRigidbodyとBoltEntityをアタッチします。
また、新規にスクリプトを作成してBall
などの名前にしておきます。TagにBall
を追加して、割り当てておきます。
BoltEntity
は、アタッチ直後はエラーが発生します。例のごとく、AssetsメニューからBolt > Compile Assemblyでビルドしてから、State欄をITransformStateに設定します。
Ball.cs
は以下の通りです。
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RollerBallBolt { public class Ball : Bolt.EntityBehaviour<ITransformState> { /// <summary> /// transformをIPlayerStateのTranformに割り当てます。 /// </summary> public override void Attached() { state.SetTransforms(state.Transform, transform); } } }
アタッチされた時にtransform
をStateのTransformに割り当てているだけです。
以上できたらプレハブ化して、ボールの出現位置に配置します。
プレイヤーの作成
プレイヤーを作成します。形はCylinderにして、メッシュと当たり判定の形状を変えたかったので、親子階層にして親にSphere Collider、子にCylinderを設定しました。
設定は以下の通りです。
- TagはPlayer
- Sphere Colliderをアタッチして、良い場所と大きさに調整
- Rigidbodyをアタッチして、Use Gravityを外し、Collision DetectionをContinuous Dynamic、Freeze PositionのYにチェック、Freeze Rotationを全てチェック
- Bolt Entityをアタッチして、Compile Assemblyを実行後、StateをITransformStateに設定
- 新規スクリプトを作成して、
Player
の名前に変更します
Player.cs
スクリプトは以下の通りです。
using System.Collections; using System.Collections.Generic; using Bolt; using UnityEngine; namespace RollerBallBolt { 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()
でマウスの移動量を記録しておきます- アタッチされた時に、
transform
をStateのTransformに割り当てるコードをAttached()
メソッドに実装します SimulateController()
は、操作をシミュレートする際にBoltから呼び出されるメソッドです。ここで、Update()
で記録しておいたマウスの移動量をRollerBallBoltCommandのInputのMouseに設定して、QueueInput()
で登録しますExecuteCommand(command, resetState)
に、コマンドを実行するための処理を実装します。command
は送受信するためのRollerBallBoltCommand
のインスタンス、resetState
はサーバーかクライアントのどちらで動いているかを表していて、true
の時はコントローラー(クライアント)側の処理、つまり、送られてきた結果を自分のローカル座標に反映させます。false
の時は、オーナー(サーバー)側の処理で、QueueInput()
で蓄えられた入力データを速度に反映させて、オブジェクトを動かし、結果をcommandに渡します
あとは、Player1用とPlayer2用で別のマテリアルを設定してプレハブ化して、出現させたい場所に配置しておきます。
フィールド
枠や地面などのフィールドは、ネットワークで共有する必要はないので、特に何もせずにそのまま配置しておきます。
オブジェクトを配置する機能の作成
チュートリアルでは、ランダムに座標を設定したりしてスクリプト側で座標を作っていましたが、最初から登場させたい場所にゲームオブジェクトを配置して、そこにInstantiateした方が楽だろうということで作った機能です。
- 新規に空のGame Objectを作成して、
NetworkSceneManager
などの名前にします - 新しいスクリプトを作成して、
NetworkSceneManager
などの名前にします
スクリプトは以下の通りです。
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RollerBallBolt { 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()
で、PlayerとBallのタグが付いたオブジェクトを検索して、プレハブIDと座標を記録したらDestroyします。サーバーでオブジェクトを生成する際に、プレハブIDで記録しておいた座標を検索して、その座標にゲームオブジェクトを配置しています。
ネットワーク管理スクリプト
最後に、シーンが始まった時に、プレイヤーやボールを生成するためのオブジェクトとスクリプトです。これは、ゲームオブジェクトにはアタッチしません。新規にスクリプトを作成したら、RollerBallBoltServerCallbacks
などの名前にします。コードは以下の通りです。
using UnityEngine; using RollerBallBolt; namespace RollerBallBolt { [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")] public class RollerBallBoltServerCallbacks : Bolt.GlobalEventListener { /// <summary> /// このプログラムがシーンを読み込んだ時の処理 /// </summary> /// <param name="map"></param> public override void SceneLoadLocalDone(string map) { // プレイヤー1を生成して、操作を担当します BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player); 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
シーンが準備できた時にサーバーで実行するコードです。Player1とBallを生成して、Player1はサーバー自身が制御するのでTakeControl()
を呼び出します。
SceneLoadRemoteDone()
は、クライアントのGame
シーンが準備できた時にサーバーで実行するコードです。Player2を生成して、操作はクライアントが担当するので、AssignControl()
で接続先(connection
)に割り当てます。
制作過程は、おおよそ以上の通りです。
オブジェクトのBoltアイコンを消すには(2018/12/12追記)
デフォルトのままですとオブジェクトに雷のBoltアイコンが表示されっぱなしになります。これを消すには、WindowメニューからBolt > Settingsを選んで起動して、Miscellanceous欄のShow Debug Infoのチェックを外します。
他にも、Show Help ButtonsやVisible By Defaultのチェックを外すと、デバッグ用のボタンや情報ウィンドウを消すことができるので、適宜、設定するとよいでしょう。
まとめ
Physicsで制御するゲームオブジェクトを、Photon Boltを使ってネットワーク上で共有することができました。
UNetに比べると手順はやや多い印象ですが、動いたあとの安心感があります。サービス停止の心配ないし...。
エアホッケーやブロック崩し、ピンボール、コインプッシャーなど、Physicsを使えば簡単に作れるタイプのゲームがあります。今回ご紹介した方法を利用すれば、そのようなゲームをネットワーク対応させることができそうです。
WebGLが使えたらなぁ...と願いつつ、明日のRaspberlyさんの3Dモデルの影を別の影に差し替える - Raspberlyのブログにバトンタッチします。
Physicsの同期について(2018/12/7追記)
Photon Unity Networking(PUN)では、PhysicsのRigidbodyのvelocityを同期するためのクラスPhotonRigidbodyView
があります。
Photon Unity Networking: PhotonRigidbodyView Class Reference
Photon Boltでも、State
にvelocity
用のVector3
を追加して速度の同期を試みることはできます。ただ、このAPIリファレンスの解説にある通り、Rigidbodyのvelocityを同期するだけではネットワーク間のオブジェクトの動きが変わってしまう可能性があります。
今回のように、物体の衝突による動きが直接ゲーム性に関わる場合、PC間の挙動の違いでボールの飛ぶ方向が変わるのは困ります。座標を送って補正したとしても、動きが不自然になることが予想されます。そのため、Commandsで操作を送ってサーバーがまとめて処理する方式にしました。
Rigidbodyのvelocityを使えば、各PC上で座標計算ができるので動きを綺麗にすることができます。弾などの単純な軌道で、衝突時に跳ね返る必要がないようなオブジェクトの場合は、Rigidbodyのvelocityを同期する手法は使えると思います。どのような動きをさせたいかで採用する技術が変化してくるので、研究のし甲斐があると思います。今回と同じ内容のものを、PUNのvelocity同期で実装したらどうなるかも見てみたいところです。