chronote

ゲーム創作活動の備忘録、日々の雑記など。

制作進捗11 ~通信同期検証~


前回は、ルームの作成とマッチングを実装しましたので
今回は、PUN による通信同期とデータ送受信について、検証しました。
最初に言っておくと、全然おもしろい話ではありません。

PhotonView リアルタイム通信

エディタでの操作

リアルタイムに同期させたい GameObject には、PhotonView コンポーネントをアタッチします。
ObservedComponents に同期させたい要素を追加。
種類は Animator / Rigidbody / Rigidbody2D / Transform の4種類。

今回はこちらの方法は使わないので割愛。

スクリプトでの操作

Photon.MonoBehaviour 継承クラスなら photonView に直接アクセスできますが、
標準の MonoBehaviour や GameObject であれば、以下のように参照できます。

PhotonView photonView = PhotonView.Get(this);

※PhotonView コンポーネントは、最低1つアタッチしなければならない


スクリプトから任意の値を指定することで、柔軟な同期が可能です。

// 頻繁に任意の値を同期したい場合、ここで指定する
// ストリームに書き込まなければ送信はされない
void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
   if (stream.isWriting)
   {
       //We own this player: send the others our data
       stream.SendNext((int)controllerScript._characterState);
       stream.SendNext(transform.position);
       stream.SendNext(transform.rotation);
   }
   else
   {
       //Network player, receive data
       controllerScript._characterState = (CharacterState)(int)stream.ReceiveNext();
       correctPlayerPos = (Vector3)stream.ReceiveNext();
       correctPlayerRot = (Quaternion)stream.ReceiveNext();
   }
}

Photonでサポートされている型

どんな型の値を送受信できるのか、という情報はこちらのページにまとまっています。
Photonでシリアル化 | Exit Games

自分で定義した型をやりとりしたい場合、
Photon に対してメソッドを登録してあげる必要があります。
上記のページでは、Vector2 のオブジェクト(8バイト)を
シリアライズ、デシリアライズするメソッドを定義し、
PhotonPeer.RegisterType() で登録する方法が紹介されています。

PhotonPeer はフレームワークの中身をいじることになるため、情報は一般公開されていません。
\Photon Unity Networking\Plugins\PhotonNetwork\CustomTypes.cs
このソースに追記していくことになります。

通信内容の特性上、やりとりするカスタムオブジェクトが多くなる場合は、
オブジェクトの構造を JSONYAML などに変換して登録する、というのも手です。
これなら、速度は犠牲になっても、フレームワークの汎用性は損なわれません。

最近だと JSON はやめて、MessagePack を使うことを推奨されているようですが、
De/Serialize にかかる負荷が許容できるようなら、これらの選択肢も無くはない?
ただし、リアルタイム通信用途の話ではないと思われます。

構造体をbyte配列にして一括送信

変数を個別に同期するのは手間、かつ応答回数も増えるので、
構造体単位でまとめて送信できないか、検証してみました。

参考文献
BinaryReader・BinaryWriterでの構造体の読み書き (構造体⇔バイト配列の変換) - Programming/.NET Framework/Tips - 総武ソフトウェア推進所

上記の方法では、BitConverter が対応する基本型しか変換できないため、
例えば構造体フィールドに対して配列や構造体の定義には未対応でした。


後から知ったんですが、 BinaryFormatter を使えば、
どんなフィールドでもまるっとシリアライズできるそうな。
【Unity】みなさん、データの保存ってどうやってます?俺はこうやっちゃってます。 - ハルシオンシステムの気ままBlog

以前はこれを使うと iOS では動かない、と言われていましたが、
なんだか回避方法が見つかっている?また時間作って検証してみます。

リモートプロシージャコール(RPC)

リアルタイム通信とは違って、
任意のタイミングで通信を発生させたい場合(ターン制、ステート同期など)や、
ルーム内のプレイヤーにブロードキャストしたい場合なんかには、この RPC を使います。

受信コールバックには [RPC] Attribute の定義が必要となります。

// RPC送信側
photonView.RPC("OnSyncMyDataRPC", PhotonTargets.All, (byte[])data);

// RPC受信側
[RPC]
public void OnSyncMyDataRPC(byte[] data, PhotonMessageInfo info)
{
     // dataを受け取る処理を記述
     Debug.Log(String.Format("Info: from {0} {1} {2}", info.sender, info.PhotonView, info.timestamp));
}

注意点として、RPC は各クライアントの一致する PhotonView に対して行われるため、
シーンロード中に呼び出す時など、状況が揃わない場合は実行されない。
対策としては、シーンロード前にメッセージキューを停止させる。

// これ以降のネットワークメッセージの処理を一時的に停止する
PhotonNetwork.isMessageQueueRunning = false;
Application.LoadLevel(levelName);

ロードが終わったらメッセージキュー停止を戻しておく。


RPC を使えば、簡易テキストチャットが作れます。

[RPC]
void ChatMessage(string name, string msg)
{
     Debug.Log(name + ": " + msg);
}
photonView.RPC("ChatMessage", PhotonTargets.All, name, msg);

ハマりポイント

素直に考えれば間違えないけど、頭が固い自分が個人的にハマった点をメモ。

PhotonView の同期は、ルーム内共通の同一オブジェクト(ViewID)に対してやりとりされる。
つまり、それぞれの世界ではお互いのプレイヤーが唯一無二の存在となり、
完全に別オブジェクトに分かれないといけない。
(1つのゲーム機で1画面で格闘ゲームをプレイしている、と考えると腑に落ちる)

例えば2人対戦の時、マスターがプレイヤー1、クライアントがプレイヤー2であれば、
クライアントのゲームから見ても自身はプレイヤー2である必要がある。

これを勘違いして、自身はプレイヤー(1P)、相手はライバル(2P)、という考え方にすると、
お互いがプレイヤー(1P)のデータを同期しあって衝突を起こすことになる。
(2人が別々のゲーム機でオンライン対戦ゲームをプレイしていて、
 自分は必ず1P側になる、と考えるとみごとにバグる

そのため、ゲーム内に複数存在するプレイヤーのうち、
操作プレイヤーをどのオブジェクトに振り分けるのか、といったルールが必要になる。

あとがき

ただの PUN の解説日記になってしまった感。
ゴールデンウィーク中に対戦実装まで完了したかったけど
検証だけで過ぎ去ってしまいました・・・
しばらく平日は体力的に作業ができず、進みが遅いです。あと、花粉。
引き続き、同期通信によるターン制御を進めます。

制作進捗10 ~光子に導かれ~


Photon のドキュメントを翻訳してると、ちょくちょく「光子」がでてくるのでつい・・・

今回は、PhotonUnityNetwork(以下PUN)の全体像を把握しつつ、
実際にマッチングしてバトルを開始するまで、を作ってみました。


ドキュメント

ざっくりと全体像を把握するのであれば、Photon Realtime の Reference 項目を
順に眺めるとよさげです。doc.exitgames.com


PUN 用のまともなリファレンスがぱっと見当たらなかったのですが、
検索すると PUN 日本語版のリファレンスが置いてあったので、
細かい使い方はこちらでなんとか把握できるかと思います。

photoncloud.jpphotoncloud.jp

http://photoncloud.jp/pdf/PhotonNetwork-Documentation_ja.pdf

例えば、特定のメソッドを使った結果のコールバックはこれ、
といった関連性を調べるには、日本語の正確な情報はここから得られます。
各種プロパティの説明もあるので、ここを検索するのが早いですね。


ランダムマッチングしてみる

Photon サーバに接続する方法の1つとして、簡単なやり方。

PhotonNetwork.ConnectUsingSettings("v0.1");

これを呼ぶだけ。接続完了/失敗のコールバックは次のとおり。

// [Callback] Photonサーバに接続完了
void OnConnectedToMaster();
// [Callback] Photonサーバに接続失敗
void OnFailedToConnectToPhoton(DisconnectCause cause);

コールバックは Photon.MonoBehaviour を継承した GameObject にて実装しました。

続いて、接続が完了している状態で何らかのボタンを押したら
マッチングを開始する、という流れを作りました。

//-------------------------------------------------------------------------
// ランダムマッチルームへの参加を試みる
PhotonNetwork.JoinRandomRoom();    // 何らかのトリガで呼ぶ

//-------------------------------------------------------------------------
// [Callback] ルームへの参加に失敗
public virtual void OnPhotonRandomJoinFailed()
{
     // 新たなルーム作成を試みる
     PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers = 2 }, null);
}

//-------------------------------------------------------------------------
// [Callback] ルーム作成に失敗
public virtual void OnPhotonCreateRoomFailed();

//-------------------------------------------------------------------------
// [Callback] ルームへの参加に成功
public virtual void OnJoinedRoom();

まずは、すでにルームがあることを想定して JoinRandomRoom() を行います。
そのまま参加できれば、OnJoinedRoom() が呼ばれて準備完了です。
あとはどこかの Update() などで対戦相手が揃っているか人数確認を行い、バトルへ移ります。

ルームへ参加できなかった場合、自らがマスターとなるルームを作成するために、
CreateRoom() を呼びます。
成功すれば同じく OnJoinedRoom() が呼ばれ、失敗すれば
OnPhotonCreateRoomFailed() が呼ばれるようです。(失敗動作は未検証)

例外処理など省いていますが、これだけでランダムマッチングができてしまいました。


条件マッチングもあるよ

完全ランダムではなく、ある程度の条件マッチングがしたい場合もありますが、
ちゃんと方法が用意されています。

CreateRoom()JoinRandomRoom() にプロパティを渡すことで、条件指定が可能です。
例えば、"map" : "3" で遊ぶ人を募集、といった"キー" : "値"が一致する相手、といった雰囲気です。

他にも、自分で条件演算子をカスタムできる SQLロビー という指定方法があります。
まだ必要ではないので触っていませんが、これを使えば、レベルがX以上Y以下の人を募集、
といった階級マッチングができますね。

さらに、フレンド検索をして、オンラインで見つかった相手だけが見えるルームを作り、
フレンドマッチングを行う、といったこともできるようですが、詳細は必要になってから調べます。


あとがき

あぁ、もうゴールデンウィークも残り2日。せめてこの期間中に対戦実装は終わらせたい。。
というわけで、次はオブジェクトの同期周りを対応していきます。

制作進捗9 ~通信対戦の検討~

 

今回は、UNET について追いかけているうちに、それほど情報が出てこなくて、

あらためて他の通信手段を検討したらそちらの方が都合がよかった、

という備忘録です。。

 

 

UnityNetwork - MasterServer

まずは Unity オリジナルの通信機能をおさらい。


ここでいう MasterServer というのは、

Client/Server 通信における、マッチング用のロビーのことです。

Server 役となるホスト端末はこのロビーに接続し、部屋を作ります。

クライアント端末は、ロビー内の空き部屋を検索し、

その部屋のホストである Server に接続します。


Unity がデフォルトで用意しているロビー接続先はテスト用途に使えます。

(製品版で使ってはいけないのかは不明)

(テストでどの程度の負荷に耐えられるのか、なんてスペックも不明)

アドレス指定により自分で用意したサーバが使えるので、

最終的にはこちらの方法を利用することになりますかね。

・・・つまり、Unity の Network 機能をそのまま使うとなると、

自前でサーバを立てる必要があるため、当然ですが費用と手間がかかるわけですね。

参考

 

 

UNET

日本語の文献が無くて正確な意図がわかりませんが、

旧 UnityNetwork をラッピングして使いやすくしたもの?


登場経緯も覚えていませんが、ホスティング需要がたくさんあるし、

サービスとして内包したら会社の柱になるんじゃ?という空気なのかなと。

実際、オフラインゲームでできることが限られていたり、

チープなゲームでも、対戦できれば意外と熱かったり。

オンライン要素導入の壁が低くなることは良いことです。


6月の Unity5.1 リリースで公開予定(現在β中)ということで、期待は膨らむ一方、

使い方の詳細がわからず、時間が過ぎていく・・・

一応、ドキュメントは Pro βフォーラムにて公開されています。

forum.unity3d.com

 

ドキュメント内にサンプルの項目がいくつかあるので、

それが公開されれば勢いよく実装例が出てきそうな気がします。

しかし今回は時間の都合で UNET 対応を見送り、

PhotonUnityNetwork(以下PUN)を採用することにしました。

(6月にはゲームが完成していないといけない・・・!)


といっても、Unity5.x で旧Network 機能を削除する予定、と書いてあるので、

いずれは 通信系アセットは、仕様変更をせざるを得ないことになるでしょう。

(例えば、新しい iOS に対応するには Unity5.x ビルドが必須で、

 そのバージョンでは UNET しか使えない、といった状況が今後あり得るのでは?)

 

 

Photon Unity Network(PUN)

今となっては採用事例が多く、有名なこちら。

 

自前サーバーを用意しなくて済み、自前実装すべき仕組みも内包されているので、

(技術研究が目的でなければ)使わない手はないサービスです。

個人用途なら雰囲気的に

  1. 導入テストはフリー版で始める
  2. サービス開始前に「15500円で買い切り」購入(PUN+)
  3. 運用中、サーバアクセスが一定数を超えたら月額15300円」プランにのりかえ

となります。


とりあえず買い切りで気楽に初められるのはいいですね。

3段階目になった時点で、運用経費を捻出する仕組みが必須となります。

このアプリはアクセス数これくらいで、こんな風にスケールアップした、

といった具体例があると、さらにいいですね~。

スライドシェアなんかに紹介事例が多々ありますので、

そちらもチェックすると参考になります。

 

参考

doc.exitgames.com

 

 

まとめ

 

  1. 今すぐオンラインゲーム作りたいなら、迷わず PUN オススメ。
  2. 6月の Unity5.1、9月の Unity5.2(予定)まで待てるなら、UNET も視野に。
  3. 全部自作しないと気がすまない!という方は、UnityNetwork+自前サーバ。

 

自分の場合、手が空いてる期限が6月中旬までなので、ちょっと待てませんでした。

ようやく方針が決まったので、今後は PUN を使った通信対戦を、実装していく予定です。