ここまでで、常に情報のやり取りが必要なキャラクターの位置の同期が実装できました。
ネットワークゲームでは、アイテムの取得や攻撃の成功など、たまに発生するようなイベントがあります。変化の少ない情報を常にやりとりするのはネットワーク資源の無駄遣いです。そのような通信にはRPCという機能が用意されています。アイテムの取得をRPCを使って実装してみます。
RPC(Remote Procedure Call)リモートプロシージャーコールとは
RPCとは、ネットワークごしに関数を呼び出す仕組みで、Unityのネットワークもこの機能を持っています。UnityのRPCは、自分と同じviewIDを持ったゲームオブジェクトが持つ関数のみ呼び出せるという制約があります。
RPCは2通りの方法で送信先を指定することができます。まずは、RPCModeで相手と自分の関係性から送信先を決める方法です。(Unity - スクリプトリファレンス:)。
- Server
- ゲームサーバー
- Others
- 自分以外の全員に送信
- OthersBuffered
- 自分以外の全員に送信して、バッファーに記録する
- All
- 自分を含めた全員に送信
- AllBuffered
- 自分を含めた全員に送信して、バッファーに記録する
もう一方は、NetworkViewが持つownerプロパティなどから読み取れるNetworkPlayerを使って送信先を指定する方法です。
いずれも指定した送信先にいる自分と同じNetworkIDを持つゲームオブジェクトの関数を呼び出します。
使い方は以下の通りです(Unity - スクリプトリファレンス:)。
networkView.RPC("関数名", 送信先, 引数の配列);
- 呼び出す関数名を文字列で指定します。RPC呼び出しできる関数の宣言の前には[RPC]キーワードを書きます(C#の場合)。
- 送信先は、RPCModeのいずれかか、特定の相手のNetworkPlayerを指定します。
- 引数のオブジェクト配列を渡します。使える型は以下の通りです。
- int
- float
- string
- NetworkPlayer
- NetworkViewID
- Vector3
- Quaternion
ネットワーク先の他のゲームオブジェクトの関数を呼び出すには
RPCの引数に、呼び出したいゲームオブジェクトのviewIDを渡します。
viewIDとは、Network.Instantiate()でネットワークにゲームオブジェクトを生成した時に割り振られる値です。あるゲームオブジェクトが持つviewIDは、ネットワーク先でも同じ値になりますので、呼び出したい相手のviewIDが分かればネットワーク先で目的のゲームオブジェクトを見つけることができます。viewIDは、NetworkView.viewIDで確認できます。
NetworkView.Find(viewID)で、自分の環境にいる目的のゲームオブジェクトのNetworkViewを得られます。このNetworkViewでRPCを呼び出せば、他のゲームオブジェクトの関数を呼び出すことができます。
アイテムの実装
アイテムの出現や削除はNetwork.Instantiate()やNetwork.Destroy()でできますので、以下のような手順で処理すればよさそうなものです。
- プレイヤーAがアイテムと接触
- アイテムを拾った処理をする
- アイテムの削除をネットワークに要請
- 他のプレイヤーのゲームから該当アイテムが消える
しかし、マルチプレイヤーの場合はこの方法では不具合が発生します。上記の3と4の手順の間にネットワークによる時間差が発生します。アイテムが消える前にプレイヤーBが同じアイテムを拾えてしまうのです。
以上を解決するために、以下の方法に改めます。
- プレイヤーAがアイテムと接触
- アイテムがすでに取られていたら処理を終了
- まだ取られていなければ、アイテムを取ったことにする
- プレイヤーAのアイテムを取った処理を呼び出す
- アイテムは自分自身を削除する
上記の流れであれば、プレイヤーAとプレイヤーBが同時にアイテムを取ろうとしても、先に処理を開始したプレイヤーしかアイテムは取れなくなります。
上記の処理は、誰か一人に担当させる必要があります。ネットワークのあちこちで処理をしてしまうと、ネットワークの数だけアイテムを取ってしまうかもしれないからです。今回はアイテムが一つであることを保証したいので、アイテムの持ち主が処理のきっかけを作ることにします。
当たり判定は、プレイヤーが操作しているPC上で発生したものを利用すると操作性が良くなります。接触したかどうかは、プレイヤーを制御しているところでやります。
以上を考慮して、RPCに当てはめた流れを以下に示します。
アイテムのOnTriggerEnter
- プレイヤーが接触したことを確認
- 接触したプレイヤーが自分が制御していない場合は何もしない
- アイテムが自分が管理しているものかをNetworkView.isMineで確認
- 自分が管理するアイテムだった場合は、アイテムの取得処理を直接呼び出す
- 別のネットワークで管理しているアイテムだった場合は、RPCを使ってネット先のアイテムの取得処理を呼び出す
- 上記、接触したプレイヤーのviewIDを引数で渡す
アイテムの取得呼び出し(RPC)
この処理は、アイテムを管理しているPC上で行います。
- 取得処理を開始していたら何もしない
- 取得処理の開始フラグを設定。このアイテムを多重に取得できないようにする
- 引数で渡されたviewIDで接触相手のプレイヤーを検索して取得
- プレイヤーが自分と同じネットの場合は、直接アイテム取得関数を呼び出す
- 違うネットの場合は、検索したNetworkViewのownerで相手先を指定して、NetworkViewのRPCでアイテム取得関数を呼び出す
- 自分を削除
ここまでで、アイテムの処理は終わりです。あとはプレイヤーでアイテムの効果を反映させます。
プレイヤーのアイテム取得(RPC)
- アイテムを取得した効果をデータに反映する
以上です。この流れに従って実装していきます。
アイテムの出現
アイテムを表すCubeを作成します。
アイテムプレハブの作成
- [Unity]に切り替える
- [Hierarchy]ビューの[Create]から[3D Object]>[Cube]を選択
- 作成したCubeの名前を「Item」に変更する
- [Item]をドラッグして、[Project]ビューにドロップして、プレハブにする
- プレハブ化したら、[Hierarchy]ビューの[Item]を削除する
アイテムを出現させる
アイテムは、画面に常に1つ出現させることにします。場所はランダムにします。それらを管理するためのスクリプトを作成しましょう。
アイテムが登場しているかをアイテム自身が返せるようにします。
- [Project]ビューの[Create]から[C# Script]を選択
- 新しく作成したスクリプトの名前を「CItem」にする
- [CItem]スクリプトをドラッグして、[Project]ビューの[Item]プレハブにドロップ
- [CItem]をダブルクリックして、エディタで開く
- スクリプトは以下の通り
using UnityEngine; using System.Collections; public class CItem : MonoBehaviour { /** アイテムが出現しているかフラグ*/ public static bool exist {get; set;} // Use this for initialization void Start () { } // Update is called once per frame void Update () { } }
- 上書き保存をして、Unityに戻る
次に、アイテムが出現しているかを監視して、なくなったら出現させる処理を作ります。
- [Project]ビューの[Create]から[C# Script]を選択
- 新しく作成したスクリプトの名前を「CItemController」にする
- [CItemController]をドラッグして、[Hierarchy]ビューの[SimpleNet]にドロップ
- [CItemController]をダブルクリックして、エディタで開く
- スクリプトは以下の通り
using UnityEngine; using System.Collections; public class CItemController : MonoBehaviour { /** アイテムのプレハブを設定する*/ public GameObject prefItem = null; /** アイテムを出現させる横範囲*/ public float RAND_WIDTH = 6f; /** アイテムを出現させる縦範囲*/ public float RAND_HEIGHT = 5f; void Start() { // アイテムの出現フラグをクリアしておく CItem.exist = false; } void Update() { // サーバー時のみ処理 if (Network.isServer) { if (!CItem.exist) { Vector3 pos = new Vector3(Random.Range(-RAND_WIDTH, RAND_WIDTH), Random.Range(-RAND_HEIGHT, RAND_HEIGHT), 0f); Network.Instantiate(prefItem, pos, Quaternion.identity, 0); CItem.exist = true; } } } }
- 上書き保存
- Unityに移動
- [Hierarchy]ビューの[SimpleNet]を選択
- [Project]ビューの[Item]プレハブをドラッグして、[Inspector]ビューの[Pref Item]欄にドロップする
以上ができたら実行して[ゲームサーバーになる]を選択します。アイテム代わりのCubeが出現すればOKです。
アイテムの取得処理を実装
RPCを使って、アイテムの取得を実装します。アイテムを取得したら、アイテムの取得数を管理するようにもしてみます。
下準備
今回は、アイテムに接触したらすぐに消えるのでトリガーを設定します。RPCを利用するために、NetworkViewコンポーネントを追加します。また、プレイヤーを見分けられるようにタグも設定します。
- [Project]ビューから[Item]プレハブを選択
- [Inspector]ビューの[Box Collider]コンポーネントの[Is Trigger]欄にチェックを入れる
- [Inspector]ビューの[Add Component]をクリックして、[Miscellaneous]>[Network View]を選択
- [Project]ビューの[Player]プレハブを選択
- [Inspector]ビューの[Tag]のドロップボックスをクリックして、[Player]を選択
アイテムに接触した時の処理
アイテムに接触の検出プログラムを作りましょう。
- [Project]ビューから[CItem]スクリプトをダブルクリックしてエディタで開く
- 以下の変数をクラス定義の直後当たりに追加
/** ネットビュー*/ private NetworkView netView = null;
- Start()関数に以下を追加
// Use this for initialization void Start () { netView = GetComponent<NetworkView>(); }
- 以下の関数を追加
void OnTriggerEnter(Collider col) { // 接続相手がプレイヤーの時に処理 if (col.gameObject.CompareTag("Player")) { // プレイヤーのNetworkViewを取得 NetworkView netplayer = col.gameObject.GetComponent<NetworkView>(); // アイテムを自分が制御している時は、そのままアイテム取得処理を呼び出す if (netView.isMine) { PickupItem(netplayer.viewID); } else { // アイテムがネットワーク先の場合 // アイテムの管理者をownerで指定して、RPC越しに呼び出す netView.RPC("PickupItem", netView.owner, netplayer.viewID); } } }
以上で接触時の処理は完了です。やっていることは以下のとおりです。
- 接触した相手が自分が制御するプレイヤーであることを確認
- 自分がアイテムの管理者であれば、自分のアイテムの取得処理を呼び出し
- アイテムの管理者が別のネットワークの時は、そのネットワークを指定してアイテムの取得処理を呼び出し
まだ関数が足りないのでエラーが発生します。引き続き関数を定義します。
アイテムの取得処理
実際のアイテムの取得処理はRPCの関数としてPickupItemという名前で作成します。ここでアイテムの多重取得を解決して、プレイヤーにアイテムを取らせます。
- [CItem]スクリプトをエディタで開く
- 以下の変数をnetViewの定義の下あたりに追加
/** アイテムの取得処理の開始*/ private bool isPicked = false;
- 以下の関数を追加
[RPC] void PickupItem(NetworkViewID viewID) { // 処理を開始していたら処理しない if (isPicked) return; // 処理開始フラグを設定 isPicked = true; // 取得したプレイヤーを検索 NetworkView netplayer = NetworkView.Find(viewID); if (netplayer == null) return; // 自分が制御している場合は、自分のネットにいるプレイヤーのアイテム取得関数を呼び出す if (netplayer.isMine) { netplayer.SendMessage("GetItem", 1); } else { // ネット先にいるプレイヤーのアイテム取得関数を呼び出す netplayer.RPC("GetItem", netplayer.owner, 1); } // 自分自身を破棄する Network.Destroy(gameObject); // RPCから自分を生成する命令を削除 Network.RemoveRPCs(netView.viewID); // アイテムをいなくする exist = false; }
- 上書き保存
以上でアイテムの実装は完了です。[Build & Run]をしてリビルドを行い、ビルドしたプログラムとUnityでで実行してみましょう。プレイヤー側の処理を実装していませんが、アイテムを取得すると画面から消えて、新しいアイテムが出現します。
プレイヤーの処理
プレイヤーにアイテムを拾った時の処理と、これまでに拾ったアイテムの数を表示する処理を実装して仕上げます。
アイテムの取得数を表示
プレイヤーがいくつアイテムを取ったか管理できるようにして、画面に表示してみます。
- [Unity]に切り替える
- [Project]ビューの[Player]プレハブをドラッグして、[Hierarchy]ビューでドロップする
- [Hierarchy]ビューの[Player]を右クリックして、[3D Object]>[3D Text]を選択
- 追加された[New Text]を選択して、[Inspector]ビューで以下を設定
- [Text]欄を0に変更
- [Anchor]欄を[Upper Center]に変更
- [Font Size]を8に変更
- [Hierarchy]ビューの[Player]を選択
- [Inspector]ビューの[Apply]を押して、以上の変更をプレハブに反映させる
- 以上でPlayerプレハブの更新が完了したので、[Hierarchy]ビューの[Player]を削除する
アイテム数の管理と、それを表示するコードを追加します。
- [CPlayer]スクリプトをエディターで開く
- アイテム数をカウントするための変数と文字描画のための変数をnetViewの下あたりに追加
public int iItemCount = 0; private TextMesh textItem = null;
- Start関数内に3D Textのインスタンスを取得するためのコードを追加
// Use this for initialization void Start () { myRigidbody = GetComponent<Rigidbody>(); netView = GetComponent<NetworkView>(); textItem = GetComponentInChildren<TextMesh>(); }
- Update関数に、アイテム数を3D Textに反映させるコードを追加。Update関数の最初に以下を追加
textItem.text = ""+iItemCount;
- 上書き保存して、Unityに切り替える
動作確認してみます。Unityで実行してください。[ゲームサーバーになる]でスタートすると、プレイヤーの真ん中に0が表示されます。
[Hierarchy]ビューで[Player]を選択して、[Inspector]ビューの[I Item Count]欄の数字を変更してみましょう。ゲーム画面のプレイヤーの数値も同じ数字に変更されることが確認できます。
アイテムの取得処理の実装
途中になっていた、アイテムの取得関数をプレイヤーに追加します。
- [CPlayer]スクリプトをエディターで開く
- 以下の関数を追加
[RPC] void GetItem(int add) { iItemCount += add; }
- 上書き保存をしてUnityに切り替える
以上で、アイテムの取得がひとまずできました。[File]>[Build & Run]でリビルドして、実行ファイルとUnityで通信してみましょう。
アイテムを取ると、取った画面のプレイヤーの数字が増えていきます。しかし、通信先のプレイヤーの数字は変化しません。変数iItemCountの値を同期させていないので、プレイヤーの座標の最初の頃と同じ状態になっているからです。同期データに入れる方法もありますが、変化しない値を毎回やりとりするのは無駄なので、これもRPCで解決しましょう。
アイテム数をRPCで同期させる
すべてのネットワーク先のRPC関数を呼び出して、最新のアイテム数を設定すればうまくいきます。
- [CPlayer]スクリプトをエディターで開く
- 以下の関数を追加
[RPC] void SetItemCount(int ic) { iItemCount = ic; }
- アイテム取得関数に、SetItemCountの呼び出しを追加
[RPC] void GetItem(int add) { iItemCount += add; netView.RPC("SetItemCount", RPCMode.OthersBuffered, iItemCount); }
- 上書き保存をして、Unityに切り替える
RPCMode.OthersBufferedでRPCを呼び出すことで、自分以外のすべてのネットワーク先のSetItemCountを呼び出します。Bufferedなので、サーバーに関数が残ります。これにより、後から参加したメンバーにも数が通知されます。
[File]>[Build & Run]でリビルドして、実行ファイルとUnityでテストしてみてください。アイテムの数がどちらの画面でも同じになります。また、クライアントを閉じて、もう一度起動して接続してもちゃんと数が同じになるはずです。
まとめ
RPCの使い方は以上です。常時同期が不要なデータはRPCを使うことで通信量を減らすことができます。ゲームの開始やクリアといった処理もRPCを使うとよいでしょう。ネットワーク先とゲームオブジェクトの指定で混乱しやすいので、じっくりと考えてみてください。
最低限の内容は紹介しました。これまでの内容を応用して、ネットを切断したプレイヤーが消えるような処理を入れたり、不要になったRPCを整理する処理などを入れていけば簡単なネットゲームが作れるようになるでしょう。応用がまだ難しい場合は、紹介した書籍や、Unityの公式ページにあるサンプルなどを調べるとよいでしょう。