こんにちは。
大虎工房です。
今回は、プログラマ or Unity+Photon経験者向けの記事です。
たまには、技術的な話でブログの知識的価値を上げる努力をしてみようかと思いまして。
Photonを使って、裏マッチを実装
裏マッチとは
COM戦を実行しながら、マッチングを待機する手法
当然、COM戦も対人戦も同じコードで動かしたい。メンテが楽になるし。
Photonシステムだけでマッチングを待機するためには、Roomに入る必要がある
なので、Photon.roomに入ってGameObjectをInstantiateしつつゲームをCOMモードで動作させる。(このプレイヤをAとする)
このとき、一人であるならば問題ないなく動作する。
しかし、マッチング待機中に別のプレイヤ(ここではBとする)がRoomへ入ってきた場合、Bも同様にInstantiateしてCOMモードを実行しようとする。
(白の少女は4人マッチのゲームなので、4人揃うまではAもBもCOMで待機するため)
そうなると、Photonの仕様でGameObjectのInstantiateは同期されるというルールがあるため、Bが生成したオブジェクトもAの端末で出現してしまうという問題が発生する。
これを回避するためには、Roomに依存しないGameObject生成が必要になってくる。
そこで、
Photon管理ではなくUnity管理でGameObjectを生成することで、Roomに依存しないGameObjectを生成してみようと思う。
その1
PhotonNetwork.InstantiateをGameObject.Instantiateに置き換えるることで対応してみることにする。
これで、Unity管理されるGameObjectになる。
namespace Utility { public class Instantiate { public static GameObject Create(string path) { return Create(path, null); } //注意、Nativeはdataパラメータを渡せない public static GameObject Create(string path, object[] data) { var native = GlobalSpace.Instance.NativeInstantiate; if(native) { //GameObject生成 var prefab = Resources.Load<GameObject>(path); var go = GameObject.Instantiate(prefab); var script = go.GetComponent<Utility.Behaviour>(); if (data != null) { script.photonView.instantiationData = data; } script.Initialize(); return go; } else { //Photon生成 GameObject go = null; if(data != null) { go = PhotonNetwork.Instantiate(path, Vector3.zero, Quaternion.identity, 0, data); } else { go = PhotonNetwork.Instantiate(path, Vector3.zero, Quaternion.identity, 0); } return go; } } }
その2
Instantiateを置き換えるだけだと正常に動作しないプロパティやメソッドは、Photon.Behaviourを継承しつつオーバーライドしてしまうことで対処する
・photonView.isMineをオーバーライド
このパラメータ。roomのMasterだと強制trueっぽいが、2番目にRoomに入ってきてたりすると、falseになってしまう。
COM戦の場合、GameObject.Instantiateを実行するため必ず自分管理なので、オーバーライドして強制trueにする。
public bool IsMine { get { if (GlobalSpace.Instance.NativeInstantiate) return true; else return photonView.isMine; } }
・photonView.RPCをラップ
他にもRPC通信をラップする処理を継承クラスに実装する必要がある。
RPC通信部分も、ローカル処理しか必要がないので、呼び出し文字列の関数をローカルでCallするようにラップする
public virtual void CallRPC(string methodName, PhotonTargets target, params object[] parameters) { if(GlobalSpace.Instance.NativeInstantiate) { var t = GetType(); var mi = t.GetMethod(methodName); mi.Invoke(this, parameters); } else { photonView.RPC(methodName, target, parameters); } }
・photonView.insitationDataをオーバーライド
PhotonだとInstantiateに初期化パラメータを渡せる。
ここは、PhotonとNativeで、個別処理を記載しないとダメだった。
NativeはInstantiate後にInitializeをCall
PhotonはAwake時にInitializeをCallするように変更
public virtual void Initialize() { }
Instantiateクラスから呼び出す際に、このInitializeメソッドも呼び出してあげることで、GameObject.InstantiateでもphotonView.insitationDataを処理しようとしている
{ //GameObject生成 var prefab = Resources.Load<GameObject>(path); var go = GameObject.Instantiate(prefab); var script = go.GetComponent<Utility.Behaviour>(); if (data != null) { script.photonView.instantiationData = data; } script.Initialize(); return go; }
Photon.Instantiateの場合は処理できないので、Awakeのタイミングで呼び出すようにしている
void Awake() { if (!GlobalSpace.Instance.NativeInstantiate) { Initialize(); } }
その3
PhotonNetwork.DestroyをGameObject.Destroyに置き換える
ちゃんと生成したモジュールで解放してあげるようにラップしてやる
public static void Destroy(GameObject go) { var native = GlobalSpace.Instance.NativeInstantiate; if(native) { GameObject.Destroy(go); } else { PhotonNetwork.Destroy(go); } }
これでRoomに依存しないゲームオブジェクトを作れるようになった。
こうすると、Roomに入って、マッチングを待ちながら、完全ローカルのゲームが実行できる形になる。(Photonから独立したPhoton.Behaviourを作れる)
対戦ゲームはマッチング待機が一番ツライので、こういう方法でCOM戦に逃がしてあげることでそこそこ緩和できる。
また、マッチング待機時間もかなり長く設定できるので、自由度も高いと思われる。
Photonの仕様が、基本的に繋いで対戦っていう機能しかないので、Photonのリソースを使いながら独立駆動させる仕組みを模索してみた結果、上のようになった。
もしかしたら、これより良い方法がすでにPhotonさんで用意してるかもしれない。
ただ、一通り調べてみたけど無さそうなので作ってみた感じ。
まぁ、マッチングサーバを自前で用意できるのであれば、上のような処理は不要だけど、作るの大変すぎるんで…
そんなんで、技術的だけど、ちょっとニッチなトンチの話でした。
それでは~
おまけ。ソースコード全文
Instantiate.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Utility { public class Instantiate { public static GameObject Create(string path) { return Create(path, null); } //注意、Nativeはdataパラメータを渡せない public static GameObject Create(string path, object[] data) { var native = GlobalSpace.Instance.NativeInstantiate; if(native) { //GameObject生成 var prefab = Resources.Load<GameObject>(path); var go = GameObject.Instantiate(prefab); var script = go.GetComponent<Utility.Behaviour>(); if (data != null) { script.photonView.instantiationData = data; } script.Initialize(); return go; } else { //Photon生成 GameObject go = null; if(data != null) { go = PhotonNetwork.Instantiate(path, Vector3.zero, Quaternion.identity, 0, data); } else { go = PhotonNetwork.Instantiate(path, Vector3.zero, Quaternion.identity, 0); } return go; } } public static GameObject CreateSceneObject(string path) { var native = GlobalSpace.Instance.NativeInstantiate; if (native) { //GameObject生成 var prefab = Resources.Load<GameObject>(path); var go = GameObject.Instantiate(prefab); var script = go.GetComponent<Utility.Behaviour>(); script.Initialize(); return go; } else { //Photon生成 GameObject go = null; go = PhotonNetwork.InstantiateSceneObject(path, Vector3.zero, Quaternion.identity, 0, null); return go; } } public static void Destroy(GameObject go) { var native = GlobalSpace.Instance.NativeInstantiate; if(native) { GameObject.Destroy(go); } else { PhotonNetwork.Destroy(go); } } } }
Behaviour.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Utility { public class Behaviour : Photon.MonoBehaviour { public bool IsMine { get { if (GlobalSpace.Instance.NativeInstantiate) return true; else return photonView.isMine; } } public virtual void Initialize() { } public virtual void CallRPC(string methodName, PhotonTargets target, params object[] parameters) { if(GlobalSpace.Instance.NativeInstantiate) { var t = GetType(); var mi = t.GetMethod(methodName); mi.Invoke(this, parameters); } else { photonView.RPC(methodName, target, parameters); } } } }