Unity 1週間ゲームジャム、お題「ギリギリ」に投稿しました。
先に言い訳を言っておきますが、今回のゲームはゲームとしての面白さが相当低いです。
前に作った「FIND SPACE」の方がゲームとしては面白かったですね(´;ω;`)
今回のコンセプトは、少し前に勉強したネットワーク同期エンジン「Photon」を使って何かネットワーク対応のゲームを作れないか、というものでした。Photonの基本概念はわかったものの、実際にゲームを完成させれば学ぶことも多いだろう、という軽い気持ちでした。
1週間ゲームジャムとはいうものの、平日はいろいろ忙しく、本格的にコーディングを始めたのは金曜日の昼休みと夜から。土曜日はVR Zone新宿でドラゴンクエストVRを予約していた(めっちゃ面白かった)のであまり進捗がなく、実質2.5days程度GameJamになりました。
まずはPhotonを使ったネットワーク対戦の「ルーム」部分から作り始めたのですが、いきなりここで躓きました。(結果的に、ネットワーク周りの実装だけで時間切れになりました)
Photonの考え方としては、「ロビー」に入ってから「ルーム」を作ってゲームを開始するという流れになります。このあたりは、ゆめみさんのVRStudiesを参考にして作りました。
・ゲーム開始まで参加者を待機する(一定時間で自動開始)
・ゲーム開始後は参加(乱入)できなくする
⇒途中参加者は、ゲームの残り時間を見つつ待機する
⇒ゲームが終了したら自動的に次のゲームに参加する
・ゲーム終了後は、続けるか終了するか選択できる
⇒放置すると終了し、ネットワークを切断する(ブラウザ放置すると延々と続けることが可能なため)
⇒続けると、次のゲームが終わるまで待機して、自動的に参加する
今回は行っていませんが、プレイヤーの強さによってオートマッチングなどを考えると、まだまだルーム周りはやることが多そうです。
ということで、作った際に気が付いたところをメモに残しておきます。
PhotonのRoomには、CustomPropertiesという値を設定して共有する機能があります。
Roomに紐づく情報(ステージの状態や、残り時間など)は、これを経由して全クライアントで共有するのが便利です。
ところが、この値が同期されるのはRoomに入っているクライアントだけになります。
なので、「途中参加者はゲームが終わるまで、ゲームの残り時間(Roomの値)を見ながら待機する」ということができません。調べた結果、下記のようにすればRoomに入っていなくても、LobbyからRoomの値を見ることができるようになりました。
RoomOptions roomOptions = new RoomOptions() { MaxPlayers = 20, IsOpen = true, IsVisible = true, CustomRoomPropertiesForLobby = new string[] { "RestTime", "WaitTime" } }; PhotonNetwork.JoinOrCreateRoom(ROOM_NAME, roomOptions, null);
RoomOptionsのCustomRoomPropertiesForLobbyで指定した値は、Roomの外からでも下記のように参照可能になります。
foreach (var r in PhotonNetwork.GetRoomList()) { if (r.Name == "VR-Room") { int time = (int)r.CustomProperties["RestTime"]; GameManager.Instance.SetMessageText(string.Format("次のゲーム開始まで{0}秒ほどお待ちください", time)); } }
今回、ステージは開始時にランダムに生成するようにしました。ネットワーク対戦ですので、ステージの情報を全クライアントで共有しなければなりません。
PhotonNetwork.Instantiate関数はなかなか協力で、呼ぶだけで全クライアント上にInstantiateしてくれますし、あとからRoomに入ったクライアントに対してもキャッシュしておいて呼んでくれるという便利なものです。
ただ、ステージの外壁やランダムでない部分の構造物などは同期する必要がないので、それぞれのクライアントで純粋にInstantiateし、動的な部分だけをPhotonで同期しました。
この図で言うと、碁盤の目になっている白い建物がローカルでInstantiateしていて、オイル缶と灰色の建物がランダムに作成してネットワーク同期しているものになります(建物は同じはずなのですが、シーンの再読み込み時などになぜか微妙に色が変わる現象が起きています)
ランダムに生成する際には、いったん、前に作ったものを破棄しなければいけません。Photonでは、PhotonNetwork.Destroy()というネットワーク越しに削除する関数を使用しますが、注意点としては、オブジェクトの所有者(Instantiateしたクライアント)でないと削除できません。所有者だけに対して命令を送る方法がわからなかったので、RPCを使用して下記のようにDestroyを行いました。
(少し前にUnityテストのセミナーで聞いた、依存性注入のためのinterfaceも導入しています)
// 呼び出し側 void DestroyWithTag(string tag) { var gos = GameObject.FindGameObjectsWithTag(tag); for (int i = 0; i < gos.Length; i++) { gos[i].GetComponent<inetworkDestroy>().DestroyMe(); } }
// 呼び出され側 public interface INetworkDestroy { void DestroyMe(); } public class Oil : MonoBehaviour,INetworkDestroy { public void DestroyMe() { GetComponent<photonView>().RPC("DestroyWrapper", PhotonTargets.All); } [PunRPC] void DestroyWrapper() { if (GetComponent<photonView>().isMine) { PhotonNetwork.Destroy(gameObject); } } }
今回、Photonのマスタークライアント(一番最初にRoomに入室したクライアント)が、ステージ生成を行っています。なので、マスタークライアントが切断してしまうと、動的に生成したステージのオブジェクトが全て消えてしまい、ゲームが続行できなくなることに気が付きました。
ここは作りを変えて、ステージをRoomのカスタムプロパティで表すなどすれば回避できたかもしれませんが、時間の都合上、マスタークライアントが切断したらゲームも強制リロードとしました。
void OnMasterClientSwitched(PhotonPlayer newMasterClient) { Debug.Log("MasterClient switched"); SetMessageText("ゲームマスターが退出したため、5秒後にゲームを再起動します"); StartCoroutine(Reboot()); } IEnumerator Reboot() { yield return new WaitForSeconds(5); PhotonNetwork.Disconnect(); UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Main"); }
今回は上記のようにネットワーク周りの実装ばかりで、肝心のゲーム内容が相当薄くなってしまっています。
本当は、相手の邪魔をしつつオイル缶を多くとった方が勝ち、など、ゲーム性を高めたかったのですが時間切れに終わりました。今後、スキを見て更新出来たらと思っています。
最後に、いつも企画やサーバの準備をしてくださっている@naichilabさんありがとうございます!Uniteでお話できてよかったです!