Notifications
Article
PUN2で始めるオンラインゲーム開発入門【その1】
Updated 10 days ago
234
0
とりあえず他プレイヤーの画面にオブジェクトを表示させてみよう
【その1】|【その2】|【その3】|【その4】|【その5】

開発環境

  • Unity 2018.2.14f1
  • PUN2 - FREE v2.4
  • WebGL(unityroomに公開する前提)

初期設定をしよう

1. 公式サイトへサインインする

通常、オンラインゲーム開発ではサーバーを用意する必要がありますが、PUN2ではPhoton Cloudという開発者側では管理が不要なサーバーが(同時接続20人まで)無料で利用できます。そのためにはアプリケーションを登録して、アプリケーションIDを発行してもらう必要があります。公式サイトの右上に「サインイン」のリンクがありますので、まずそこから(アカウントの登録を済ませて)サインインしてください。

2. 新しくアプリを作成してアプリケーションIDを取得する

サインイン後のダッシュボード画面で「新しくアプリを作成する」ボタンを押すと、新しいアプリケーションの作成画面になります。Photonの種別を「Photon PUN」にして、好きなアプリケーション名を入力したら「作成する」ボタンを押してください。(アプリケーション名、説明、URLは後から編集できるので仮で入れても大丈夫です) 正しく作成されると、以下の画像のようにアプリケーションIDが確認できるようになります。

3. アセットストアから「PUN2 - FREE」をインポートする

Unityのプロジェクトを開いて、アセットストアから以下のアセットをインポートします。 https://assetstore.unity.com/packages/tools/network/pun-2-free-119922

4. アプリケーションIDを入力してセットアップ完了

インポート後(または [ Window ] > [ Photon Unity Networking ] > [ PUN Wizzard ] を選択すると)「PUN Wizzard」が開きますので、先ほど取得したアプリケーションIDを入力後「Setup Project」を押してください。
セットアップが完了すると、Photonの設定ファイルが生成されます。設定はデフォルトのままでも問題ありませんが、以下の画像にオススメの追加設定を記載していますので参考にしてください。

とりあえず覚えておきたい重要クラス

PUN2には様々なクラスが含まれていますが、開発中に頻繁に使用するクラスは実はそれほど多くありません。ここで開発を始める前に、とりあえず覚えておいて欲しい2つのクラスを紹介します。

PhotonNetwork

PhotonNetworkクラスは、ネットワーク関連機能の窓口となっている静的なクラスです。
  • サーバーへの接続・切断
  • ルームの作成・参加・退出
  • プレイヤーリストの取得
  • ネットワーク上で同期させるオブジェクトの生成・破棄
  • ネットワーク関連の各種設定
など、オンラインゲームを開発するための主要な操作のほとんどは、このクラスから使用できます。例えば、Photonの設定ファイルの内容を使ってサーバーへ接続するには、以下のようなコード一行のみで実行できます。
PhotonNetwork.ConnectUsingSettings();
PUN2にはどんな機能が揃っているのか?あんな機能やこんな機能はないのか?と思った時は、このクラスから調べ始めてみると色々と捗るでしょう。

PhotonView

ネットワーク上で同期させるオブジェクトはネットワークオブジェクトと呼ばれます。PhotonViewはネットワークオブジェクトを作成するための必須コンポーネントです。通常のオブジェクトはプレハブからインスタンスを生成しても、他プレイヤー側では一切反映されることはありません。ネットワークオブジェクトは、自身側でインスタンスを生成すると他プレイヤー側でも自動的にインスタンスが生成され、また逆に他プレイヤー側で生成したインスタンスは自身側でも自動的にインスタンスが生成されます。通常のオブジェクトをネットワークオブジェクトにする条件は以下の3つです。
  • プレハブにPhotonViewコンポーネントを追加する
  • プレハブをResourcesフォルダーに入れる
  • PhotonNetwork.Instantiate()でインスタンスを生成する

他プレイヤーの画面にオブジェクトを表示させよう

サーバー接続からネットワークオブジェクトを表示するまでの流れ

Photonのサーバーにはマスターサーバーゲームサーバーの2種類があります。マスターサーバーはプレイヤーのマッチングを行う場所で、ゲームサーバーはマッチングしたプレイヤー同士が実際にゲームを行うルームが作成される場所です。
上記をふまえて、他プレイヤーと通信するまでの流れは以下のようになります。
  1. マスターサーバーへ接続する
  2. マスターサーバーへの接続が成功した後、ルームの作成・参加を行う
  3. マッチング(ルームへの参加)が成功した後、ネットワークオブジェクトを生成する

ネットワークオブジェクトを作成しよう

まず以下の画像を参考にネットワークオブジェクトを作成してください。 (SpriteRendererで表示するスプライトは好きな画像でかまいません)

ネットワークオブジェクトを表示するための最小スクリプト

次に空のゲームオブジェクトを作成して以下のスクリプトを追加してください。
using Photon.Pun; using Photon.Realtime; using UnityEngine; // MonoBehaviourではなくMonoBehaviourPunCallbacksを継承して、Photonのコールバックを受け取れるようにする public class SampleScene : MonoBehaviourPunCallbacks { private void Start() { // PhotonServerSettingsに設定した内容を使ってマスターサーバーへ接続する PhotonNetwork.ConnectUsingSettings(); } // マスターサーバーへの接続が成功した時に呼ばれるコールバック public override void OnConnectedToMaster() { // "room"という名前のルームに参加する(ルームが無ければ作成してから参加する) PhotonNetwork.JoinOrCreateRoom("room", new RoomOptions(), TypedLobby.Default); } // マッチングが成功した時に呼ばれるコールバック public override void OnJoinedRoom() { // マッチング後、ランダムな位置に自分自身のネットワークオブジェクトを生成する var v = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f)); PhotonNetwork.Instantiate("GamePlayer", v, Quaternion.identity); } }
実行してみてネットワークオブジェクトが画面に表示されたら成功です。本当に他プレイヤー側でも自動的にネットワークオブジェクトが生成されているのか確認したい場合は、ビルドして複数起動してみると良いでしょう。

以下の★パートは中級者向けの補足です。わからない部分は読み飛ばして、慣れた頃に読み返すのがオススメです。

★マッチング後にシーン遷移する場合の注意点

先にルームに参加している他プレイヤーのネットワークオブジェクトは、マッチング直後のアクティブなシーンに生成されます。もしマッチング後にシーン遷移を行い、ネットワークオブジェクトを保持するシーンが破棄されると、ネットワークオブジェクトも同時に破棄されてしまいます。これはそのまま他プレイヤーが表示されないバグに繋がります。 問題を回避するためには、他プレイヤーのネットワークオブジェクトが生成される(厳密にいうと、サーバーから受信したオブジェクト生成メッセージを処理する)タイミングを、シーン遷移後にする必要があります。
ここで使用するのがPhotonNetwork.IsMessageQueueRunningプロパティです。このプロパティを設定することで、受信メッセージ処理の実行・一時停止を切り替えることができます。遷移前のシーンでfalseに設定し、
public override void OnJoinedRoom() { PhotonNetwork.IsMessageQueueRunning = false; SceneManager.LoadSceneAsync("Game", LoadSceneMode.Single); }
遷移後のシーンでPhotonNetwork.IsMessageQueueRunningをtrueに戻すことで、他プレイヤーのネットワークオブジェクトが遷移後のシーンに正しく生成されるようになります。
private void Start() { PhotonNetwork.IsMessageQueueRunning = true; var v = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f)); PhotonNetwork.Instantiate("GamePlayer", v, Quaternion.identity); }
PhotonNetwork.IsMessageQueueRunningは、メッセージの受信自体を止めるわけではないことに注意してください。処理を止めている間に受信したメッセージは内部のキューにためられ、再開した後に全て受信した順に処理が行われます。処理を止めている時間が長すぎると、その分だけ大量の受信メッセージがため込まれ、再開時に大量の処理が行われてしまうので、十分に気を付けましょう。

★MonoBehaviourPunCallbacksを継承しない方法

Photonのコールバックを受け取るにはMonoBehaviourPunCallbacksを継承する方法が最も簡単ですが、MonoBehaviourを継承しないクラスや、既に他のクラスを継承しているクラスでこの方法は使えません。 コールバックを受け取るための正確な条件はMonoBehaviourPunCallbacksを継承することではなく、コールバックメソッドが定義されているインターフェースを実装したクラスをコールバック対象として登録することです。MonoBehaviourPunCallbacksは単に、MonoBehaviourに(PhotonView型のプロパティ追加と)コールバックメソッドが定義されたインターフェースの空実装、自動でコールバック対象への登録・解除を行う処理の追加をしただけのクラスになっています。それらを自前のクラス側で補うことで、どんなクラスでもコールバックを受け取ることが可能になります。
この記事の前半のコードをMonoBehaviourPunCallbacksを継承しない形に書き換えると、以下のようになります。
// MonoBehaviourPunCallbacksを継承せずに、必要なインターフェースを実装する public class SampleScene : MonoBehaviour, IConnectionCallbacks, IMatchmakingCallbacks { private void OnEnable() { // コールバック対象としてこのクラスを登録する PhotonNetwork.AddCallbackTarget(this); } private void OnDisable() { // コールバック対象の登録を解除する PhotonNetwork.RemoveCallbackTarget(this); } private void Start() { PhotonNetwork.ConnectUsingSettings(); } void IConnectionCallbacks.OnConnectedToMaster() { PhotonNetwork.JoinOrCreateRoom("room", new RoomOptions(), TypedLobby.Default); } void IMatchmakingCallbacks.OnJoinedRoom() { var v = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f)); PhotonNetwork.Instantiate("GamePlayer", v, Quaternion.identity); } void IConnectionCallbacks.OnConnected() {} void IConnectionCallbacks.OnCustomAuthenticationFailed(string debugMessage) {} void IConnectionCallbacks.OnCustomAuthenticationResponse(Dictionary<string, object> data) {} void IConnectionCallbacks.OnDisconnected(DisconnectCause cause) {} void IConnectionCallbacks.OnRegionListReceived(RegionHandler regionHandler) {} void IMatchmakingCallbacks.OnFriendListUpdate(List<FriendInfo> friendList) {} void IMatchmakingCallbacks.OnCreatedRoom() {} void IMatchmakingCallbacks.OnCreateRoomFailed(short returnCode, string message) {} void IMatchmakingCallbacks.OnJoinRoomFailed(short returnCode, string message) {} void IMatchmakingCallbacks.OnJoinRandomFailed(short returnCode, string message) {} void IMatchmakingCallbacks.OnLeftRoom() {} }

★開発用GUIコンポーネントを活用しよう

PUN2には、現在のネットワーク状態を可視化するPhotonStatsGuiコンポーネント、回線が不安定なプレイヤーの動作をテストしたりするPhotonLagSimulationGuiコンポーネントがあらかじめ用意されています。Stats(レンダリング統計)やプロファイラーでは確認できないような、ネットワークに起因する問題のデバッグや最適化に役立ちます。

★Photon Cloudで作れるゲーム・作れないゲーム

Photon Cloudの無料プランは最大20人まで同時接続できるので「1ルームに10~20人が集まって対戦するゲームも作れるんじゃないか?」と考えた人もいるかと思います。不可能ではありませんが、作れるゲームの種類は大きく制限されます。

1ルームごとの秒間メッセージ数の制約

Photon Cloudには、1ルームごとに毎秒500メッセージという制約が設けられています。ここでカウントされるメッセージ数とは、サーバーが送信したメッセージ数と受信したメッセージ数の合計です。例えば参加プレイヤー数が4人のゲームで、あるプレイヤーが他3人のプレイヤーにメッセージを送る場合、サーバー側では1メッセージ受信と3メッセージ送信で4メッセージ分がカウントされます。1ルームの秒間メッセージ数はおよそ、
1人のプレイヤーが1秒間に送信するメッセージ数 * ルームへの参加プレイヤー数の2乗
で求めることができ、この数を500以下に抑えなければなりません。この制約は有料プランでも変わることはなく、毎秒500メッセージを超えるゲームを作りたい場合は、自前でPhoton Serverを立てて運用することになります。

参加プレイヤー数で急激に増加する通信量の影響

1人のプレイヤーが1秒間に送信できる最大メッセージ数は、参加プレイヤー数が4人なら約30回と高速な対戦アクションを作れるだけの十分な余裕がありますが、10人なら5回以内で上手くやりくりする仕組みを考えなければなりません。20人にもなると2回でも大幅に制約をオーバーしてしまうため、ゲームの種類によっては様々な通信量削減のテクニックやトリックを駆使しなければ、まともに同期させることすら難しい、あるいはほぼ実現不可能に近いでしょう。

それでも大人数が参加するゲームを作りたいなら

しかし逆に「参加プレイヤー数が20人の時は1秒間に1回程度しかメッセージを送信できない」というのは「1秒間に1回以下程度のメッセージ送信で同期できれば参加プレイヤー数が20人以上のゲームも作れる」ということでもあります。進行がリアルタイムではなく、ターンベースや(行動ゲージがたまるとコマンドが選択できるような)セミリアルタイムのゲームなど、比較的簡単に制約を回避することができるようなものでチャレンジしてみるのも良いかもしれません。

Photon Cloudで動作する20人対戦オンラインゲームの実例

手前味噌ですが、Photon Cloudを使った大人数対戦ゲームとして「パクリマンオンライン」を紹介します。これは三すくみ共食いバトルでスコアを稼ぐだけの非常にシンプルなゲームですが、通信量を限りなく抑えながらもラグを感じさせないリアルタイム(に見せかけた?)対戦オンラインゲームの実験作でもあります。
  • 各プレイヤーは常に等速運動
  • 自動的に前進し、止まることはない
  • 突き当たりは(入力がなければ)時計回りに方向転換する
  • 進行方向と逆に方向転換はできない
上記の仕様は、ゲームの駆け引き要素になっていると同時に、ある時点でのプレイヤーの位置・サーバー時間・進行方向がわかっていれば、ネットワーク上で位置を同期しなくても、限りなく正しい現在位置を計算のみで求めることができるようになっています。メッセージ送信が必要になるのは、
  • 道の分岐直前の段階で、プレイヤーの入力がある
  • 入力が進行方向と逆の方向ではない
  • 突き当たりの場合は、入力が時計回りの方向ではない
の条件を全て満たした場合のみで、実際のプレイでは平均して数秒に1回程度の通信で済むようになっています。

次の記事

作成中です。 PUN2で始めるオンラインゲーム開発入門【その2】

o8que
Ghost - Programmer
1
Comments