【Unity】Unity1weekでPhotonを使ってリアルタイム対戦やろうとして躓いたことを告白【unity1week】
unity1weekでリアルタイムオンライン対戦を実現しようとしたのですが、挫折しました。
そこまでに勉強したことをまとめてみようと思います。
まずはドンと公式を貼っちゃいます。
問題なく開始するために、「PUNベーシックチュートリアル」を 十分に確認したうえでコーディングしてください。
と書いてありますが…まあちょっとわかりにくいというか悪い面を伏せているというか。
概念とか仕組みいろんな機能とか色々とあるようですが、
一旦全て置いちゃいます。
「細かいことは置いておいて、とりあえずホストとクライアントで メッセージのやりとりするにはどうすればいいんだよ?」
という観点だけ解説させてください。
それでもめちゃくちゃ長いです。
文章はななめ読みして、コードと画像だけ見る、でも充分です。
使用するAssetはこちらになります。
Unityは2019.2.8f1です。
そしてテストするためにWebGLで吐き出したものと二つでテストしています。
所々にある検証画像も参考にしていただけると。
まず覚えてほしいこと
いきなり説明してんじゃん!ですが、これだけは覚える必要がありますのでご容赦を。
Photonは3つの概念があります。それは
- Photonサーバ
- ロビー
- ルーム
この3つです。
これさえ忘れなければもう怖いものはありません。
相対関係は以下の図をイメージしていただければOKです。
アカウントを作成&事前準備
この辺はささっと。
Photon側の作業
Photonを利用するにはアカウントが必要です。
新しくアプリを作成する
作りたい名前を付けて、作成する
ですぐに作成できます。
そしてアプリケーションIDをコピーして覚えておきます。
Unity側の作業
AssetStoreからPUN2 Freeをインストールし、パッケージをインポートします。
Photonの設定が開くので、設定にアプリケーションIDなど必要な情報を入力し、
SetupProjectボタンを押下すると、自動で設定ファイルが作られます。
これで準備完了です。
photonを使うには
最低限であれば適当なクラスに
Photon.PUN.MonoBehaviourPunCallbacks
を継承してあげればOKです。
これでクラスがPhotonサーバのListenerとなってくれます。
public partial class PhotonController : MonoBehaviourPunCallbacks { private static PhotonController _instance; public static PhotonController Instance { get { if (_instance == null) { var gameObject = new GameObject("PhotonController"); _instance = gameObject.AddComponent<PhotonController>(); DontDestroyOnLoad(gameObject); } return _instance; } } // 略 }
Photonサーバに接続(ホスト、クライアント共通)
3つの概念を覚えているでしょうか?
その通りに実装すれば問題ありません。
まずPhotonサーバに接続します。
以下のコードでPhotonサーバに接続することが出来ます。
PhotonNetwork.ConnectUsingSettings();
public partial class PhotonController { //-------------------------------------------------------------------- // 1. Photonサーバに接続する public void ConnectToPhotonServer() { AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); PhotonNetwork.ConnectUsingSettings(); } //--------------------------------------------------------------------- // MonoBehaviourPunCallbacks //--------------------------------------------------------------------- // Photonサーバに接続2 public override void OnConnectedToMaster() { base.OnConnectedToMaster(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // Photonサーバに接続1 public override void OnConnected() { base.OnConnected(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // 切断時 public override void OnDisconnected(DisconnectCause cause) { base.OnDisconnected(cause); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name + ":" + cause); } }
コールバックは以下の2つになります。1,2の順番で呼ばれます。
(なんで二つとも呼ばれるんだろう…?)
OnConnected() OnConnectedToMaster()
接続失敗した時のリスナー…は見当たりませんでした。
Disconnectが直接来るのかな?
(AddMessage()で経由したメソッド名を画面に出力しています。)
注意点としては
通信出来ない所で接続しようとした場合は
コールバックが来ない可能性があるので、
ポーリングしてタイムアウト処理などを自作しておいたほうが安心です。
ロビーに入る(ホスト、クライアント共通でも良い)
Photonサーバに繋いだら、Lobbyに接続します。
なお、共通でも良いと言ったのは例外がありまして、
実はホストのCreateRoomはロビーに入らなくても行うことが出来ます。
その際は自動でロビーにも接続されます。
(Lobby.Defaultに接続するならば、特にLobbyに入らなくてもOKという意味のようです。)
ロビーをカスタマイズするならば、ログインしてから作るか、
指定してルームを作成することになります。
ここでは正しい手順でロビー入ってからルームを作っていきます。
public partial class PhotonController { //-------------------------------------------------------------------- // 2.ロビーに入る public void JoinLobby() { PhotonNetwork.JoinLobby(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // ロビーから抜ける public void LeaveLobby() { PhotonNetwork.LeaveLobby(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } //--------------------------------------------------------------------- // MonoBehaviourPunCallbacks //--------------------------------------------------------------------- // ロビーにログインした public override void OnJoinedLobby() { base.OnJoinedLobby(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // ロビーから離脱した public override void OnLeftLobby() { base.OnLeftLobby(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // ***** 他はわかるけど、これは何? ***** public override void OnRoomListUpdate(List<RoomInfo> roomList) { base.OnRoomListUpdate(roomList); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name + " roomList.Count:" + roomList.Count); } }
お、なんか変なのいるぞ?(OnRoomListUpdate)
これは次に解説します。
ルーム一覧を取得する(クライアント)
例えば、凄く単純なオンラインゲームを作りたいと考えて
ホストが部屋を立てて、ゲストがその部屋を探して接続する
としたいと思った場合は、ホストが立てた部屋リストを取得したいですよね。
実は簡単なコマンドは用意されていません。
じゃあどうやってリストを取るのか?
ロビーに参加した瞬間に、以下のコールバックが叩かれます。
void OnRoomListUpdate(List roomList)
ここで現在ロビーにあるルームリストを取ることが出来ます。
public override void OnRoomListUpdate(List<RoomInfo> roomList) { base.OnRoomListUpdate(roomList); _roomList = roomList; // ルームの情報を取得してみる if(_roomList != null) { for (var i = 0; i < _roomList.Count; i++) { var room = _roomList[i]; AddMessage("----------[" + i + "]-----------"); AddMessage("RoomName: " + room.Name); AddMessage("CustomProperties : " + room.CustomProperties.ToStringFull()); // なんで空っぽなの…? AddMessage("Slots: " + room.PlayerCount + " / " + room.MaxPlayers); AddMessage("---------------------"); } } AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name + " roomList.Count:" + roomList.Count); }
Q. え、任意でルーム取りたいに決まってんじゃん。ふざけてるの?
普通そーする。誰だってそうする。
が、しかしありません。このOnRoomListUpdateが呼び出されるのは
ロビーにログインした後は自動です。
誰かがルームを作成したタイミングで呼び出されます。
// 公式よりのメッセージ
PUN 2では、ルームリストはもう内部的にキャッシュされないため、別の方法でルームリストを処理する必要があります。 まず、PUN 2パッケージに含まれているAsteroidsデモをご覧になることをお勧めします。 これをインポートしたら、LobbyMainPanelクラスを確認できます。 これは、という名前の必要なコールバックを実装しますvoid OnRoomListUpdate(List roomList)。 クライアントがサーバーから更新されたルームリストを受信するたびに呼び出されます。 これは自動的に行われます。 このコールバック内で、void UpdateCachedRoomList(List roomList)呼び出され、 それに応じてキャッシュルームリストを更新します。 その後、キャッシュされた部屋リストにアクセスして、利用可能な部屋を確認できます。 デモでは、void UpdateRoomListView()関数でこれを行っています。
Q. どうにかやる方法無いん?
実は以下の方法で再度roomlistを取ることが出来ます。
- ロビーから離脱する
- 再度ロビーに参加する
あ、ええやん。Leaveしてjoinしなおせばええんやろ。
これでOK~なんて思いきや、なぜかLeaveするとPhotonServerにも再接続します。
(何故かコールバックされる)
なんでやねーんとツッコミますが、そういう仕様のようです。
(一敗)
ルームを作成する(ホスト)
ここは簡単です。
CreateRoomするだけです。
ルームを作る時に各種パラメータを付与してゲームに合わせた情報を
付与することが出来ます。
(公式はこういう所の説明が足りないのですよね。自分がちゃんと読んでないだけでしょうけど…。)
public partial class PhotonController { public readonly string TAG_PLAYER_CUSTOM = "PLAYER_CUSTOM_HOGE"; public readonly string TAG_ROOM_CUSTOM = "ROOM_CUSTOM_FUGA"; //----------------------------------------------------------- // ルームを作成する public void CreateRoom() { // プレイヤーデータをセット var playerProperties = new ExitGames.Client.Photon.Hashtable() { { TAG_PLAYER_CUSTOM , "FROM CreateRoom" }, }; PhotonNetwork.NickName = GetName(); PhotonNetwork.SetPlayerCustomProperties(playerProperties); // ルームデータをセット var roomProperties = new ExitGames.Client.Photon.Hashtable() { { TAG_ROOM_CUSTOM , "FROM CreateRoom" }, }; var roomOptions = new RoomOptions(); roomOptions.MaxPlayers = 2; roomOptions.CustomRoomProperties = roomProperties; // ルームを作成する PhotonNetwork.CreateRoom("AAAAAAA", roomOptions, TypedLobby.Default); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } //--------------------------------------------------------------------- // MonoBehaviourPunCallbacks //--------------------------------------------------------------------- // ルームを作成した. public override void OnCreatedRoom() { base.OnCreatedRoom(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // ルーム作成に失敗した public override void OnCreateRoomFailed(short returnCode, string message) { base.OnCreateRoomFailed(returnCode, message); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } }
ルーム作成に成功すると
OnCreatedRoom() OnJoinedRoom() // <= 後述します
の両方がコールバックされます。
(なんで…?)
つまり、Created来たら「作成しました!」って
出してJoinedRoomしたら「参加しました!」って
出すなんて実装すると状況とずれたメッセージが出ます。
参加ステート、部屋立てたステートとか分けてたりするとこれも異なってしまいます。
(二敗)
また、この時、すでにロビーにログインしているユーザーがいた場合、
そのユーザーのOnRoomListUpdate()が呼ばれます。
ルームに入る
長い…長いぞPhoton。
説明すると本当に長い。
ルームリストを取るとroomInfo.nameを取ることが出来ます。
これがroomのIDになっているので、joinRoomにこの値を引数で渡せばOKです。
public partial class PhotonController { //---------------------------------------- // ルームに参加する(強制的にクライアントになる) public void JoinRoom(RoomInfo roomInfo) { // CreateRoom側と同様にプレイヤーデータを付けてあげるとホスト側にも情報を渡せます var playerProperties = new ExitGames.Client.Photon.Hashtable() { { TAG_PLAYER_CUSTOM , "FROM JoinRoom" }, }; PhotonNetwork.NickName = GetName(); PhotonNetwork.SetPlayerCustomProperties(playerProperties); // ルームに参加する(roomInfoのNameを引数に渡せばOKです) PhotonNetwork.JoinRoom(roomInfo.Name); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name + " roominfo:" + roomInfo.ToStringFull()); } //--------------------------------------------------------------------- // MonoBehaviourPunCallbacks //--------------------------------------------------------------------- // ルーム参加、作成に失敗したときのコールバック public override void OnJoinRoomFailed(short returnCode, string message) { base.OnJoinRoomFailed(returnCode, message); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } // ルームの参加が成功した時に呼ばれるコールバック // 普通引数にRoomInfo付けるでしょ…どんなルームに入ったか教えてくれても良くない? public override void OnJoinedRoom() { base.OnJoinedRoom(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); if (PhotonNetwork.InRoom) { // PhotonNetwork.CurrentRoomが入ってるルームって事らしい AddMessage("RoomName: " + PhotonNetwork.CurrentRoom.Name); AddMessage("HostName: " + PhotonNetwork.MasterClient.NickName); AddMessage(TAG_ROOM_CUSTOM + " : " + PhotonNetwork.CurrentRoom.CustomProperties[TAG_ROOM_CUSTOM] as string); AddMessage("Slots: " + PhotonNetwork.CurrentRoom.PlayerCount + " / " + PhotonNetwork.CurrentRoom.MaxPlayers); } } }
OnJoinedRoom()がコールバックで呼ばれます。
逆にホスト側では
OnEnterRoom()が呼ばれます。
// 誰か部屋に入ってきた public override void OnPlayerEnteredRoom(Player newPlayer) { base.OnPlayerEnteredRoom(newPlayer); // 誰が入ってきたんだよ?はこんな感じで AddMessage("newPlayer.NickName : " + newPlayer.NickName); AddMessage("newPlayer." + TAG_PLAYER_CUSTOM + " : " + newPlayer.CustomProperties[TAG_PLAYER_CUSTOM] as string); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name + " newPlayer:" + newPlayer.NickName); }
ここで誰が入ってきたかわかります。詳しくは後述
プロパティについて
ここまで来る途中のコードにもちょいちょい書いてあるのですが、
ルームにメッセージを付けたり、
ユーザー名やゲームパラメータを出してあげたりしたいですよね。
こういったプロパティをつけるにはどうするのか?
をここにまとめておきます。
ユーザーにプロパティを付与する
こう書きます。
var playerProperties = new ExitGames.Client.Photon.Hashtable() { { TAG_PLAYER_CUSTOM , "FROM CreateRoom" }, }; PhotonNetwork.NickName = GetName(); PhotonNetwork.SetPlayerCustomProperties(playerProperties);
ルームにプロパティを付与する
こう書きます。
var roomProperties = new ExitGames.Client.Photon.Hashtable() { { TAG_ROOM_CUSTOM , "FROM CreateRoom" }, }; var roomOptions = new RoomOptions(); roomOptions.MaxPlayers = 2; roomOptions.CustomRoomProperties = roomProperties; PhotonNetwork.CreateRoom("AAAAAAA", roomOptions, TypedLobby.Default);
ユーザープロパティを受け取る
こう書きます。
// void OnPlayerEnteredRoom(Player newPlayer) など newPlayer.NickName newPlayer.CustomProperties[TAG_PLAYER_CUSTOM] as string
ルームプロパティを受け取る
こう書きます。
PhotonNetwork.CurrentRoom.Name PhotonNetwork.CurrentRoom.CustomProperties[TAG_ROOM_CUSTOM] as string PhotonNetwork.CurrentRoom.PlayerCount + " / " + PhotonNetwork.CurrentRoom.MaxPlayers
注意
OnRoomListUpdate(List
なんで…?いや、これどうやって付与するの?
教えてPhotonの人…。
(三敗)
ホストがルームから抜けたらどうなるの?
OnMasterUpdate()が呼ばれ、
ゲスト側はルームホストに昇格します。
// ゲストで入ってホストが抜けた public override void OnMasterClientSwitched(Player newMasterClient) { base.OnMasterClientSwitched(newMasterClient); // ゲストからホストに昇格するって事ですね AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); }
ルームから抜けると?
public partial class PhotonController { //-------------------------------------------------------------------- // ルームから退室 public void LeaveRoom() { PhotonNetwork.LeaveRoom(); } //--------------------------------------------------------------------- // MonoBehaviourPunCallbacks //--------------------------------------------------------------------- public override void OnLeftRoom() { base.OnLeftRoom(); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name); } }
LeaveRoomを呼ぶと
何故か以下のコールバックまで呼ばれます。
OnConnected() OnConnectedToMaster()
これなんでなんでしょうね…?
変な処理にしてたりすると困ったことになるので注意です。
(四敗)
また、残されたユーザーは
ここが呼ばれます。
// 誰かが抜けた public override void OnPlayerLeftRoom(Player otherPlayer) { // 抜けた時はここ base.OnPlayerLeftRoom(otherPlayer); AddMessage(System.Reflection.MethodBase.GetCurrentMethod().Name + " leftPlayer:" + otherPlayer.NickName); }
メッセージの送信
やっと来ました。
お互いルームに入っている状態でメッセージを送りあう方法はこちらです。
こんな感じでメッセージを送るイベントがあった時にInstantiateしています。
受け取る側もこれで大丈夫か?と思うのですが、これで問題なく、メッセージを受け取ることが出来ました。
// メッセージを送信するボタンを押下。オブジェクトが無ければ作る public void OnSendMessage() { if(_myPhotonView == null) { _myPhotonView = PhotonController.Instance.CreateMyPhotonView(); } _myPhotonView.SendMessageRPC(PhotonController.Instance.GetName(), "hello friends."); }
//----------------------------------------------------------------------------- // マルチプレイ用オブジェクトを生成する //----------------------------------------------------------------------------- public MyPhotonView CreateMyPhotonView() { // 「Resources/MyPhotonView.prefab」 をInstantiateしようとするので作っておくこと var g = PhotonNetwork.Instantiate("MyPhotonView", Vector3.zero, Quaternion.identity); return g.GetComponent<MyPhotonView>(); }
[RequireComponent(typeof(PhotonView))] // PhotonViewも一緒に持っておくこと public class MyPhotonView : MonoBehaviour { // RPC = Remote Procedure Call (ネットワーク越しに別のコンピュータ上のプログラムを呼び出す手法) public void SendMessageRPC(string nickName , string message) { PhotonView photonView = PhotonView.Get(this); PhotonController.Instance.AddMessage("[MyPhotonView] send message == " + message); // RpcTarget.Allにすると自分にも飛びます photonView.RPC("ReceiveMessageRPC", RpcTarget.Others, "send by " + nickName , message); } [PunRPC] void ReceiveMessageRPC(string title, string message) { string receiveMessage = string.Format("[MyPhotonView] Receive:({0}) {1}", title, message); Debug.Log(receiveMessage); PhotonController.Instance.AddMessage(receiveMessage); } }
呼び出し側は
リモートクライアントに対してメソッドの呼び出しを行います。
(SendMessageRPCでReceiveMessageRPCを呼び出す)
この呼び出しを有効にするためには [PUNRPC] 属性を適用してあげる必要があります。
適用さえしてあげれば他には特に何もしなくても呼び出されます。
まとめと感想
まとめると大したことに無いような気がしますが、
これが本当にわからず、手探りで進んでいくのは中々骨が折れました。
今更だけどPhotonを使ってみたい、なんて考えてる方の
アドバイスになればいいなと思います。
(RoomInfoのカスタムプロパティの設定方法分かったら教えて下さい…)
【Unity】uGUI Scroll List で Recycle したい (初心者向け)【再利用】
お疲れ様です。ぐーるです。
また一ヶ月ぐらい空いてしまいました。
「あ、そういえば一個思いついた」
と下書きにメモだけしてて、保存して
放置してた記事があったのを二重に思い出しました。
今回の単語は
unity UGUI Scroll List Recycle
です。
marginとか追記するの忘れた…時間見つけて追記しとこう。自分用のメモ。
あと最上段で引っ張ると判定に入ってIndexOutOfException吐いちゃうかもいかんいかん
- > 追記しました。(2019/07/30)
サンプルプロジェクトを用意しましたので、
手っ取り早く見たい方はこちらを!
unityでスクロールリストをまず作ってみよう
よくある縦リストをunityで作る場合はこんな手順になります。
- ScrollRectを作る
- ViewPort(Mask)をよしなに作る
- ScrollBarを作る
- VerticalLayoutGroupとContentSizeFitterを作る
- LayoutElementを持ったprefabなどを複数追加するとリスト表示出来る
(ScrollViewをベースに作るのがおすすめです)
要素を使いまわそう
まず、要素を使い回すには、以下のものを消します。
- VeritcalLayoutGroup
- ContentSizeFitter
えっ?て思うかも知れませんが、ここを自作する形になります。
大丈夫、難しくないです。
ソースコードをまるっと
以下のソースを作成します。
/// <summary> /// 要素を再利用するレイアウトグループ(Vertical) /// </summary> public class RecycleLayoutGroup : MonoBehaviour { [SerializeField] RectTransform _itemElementPrefab; // リスト内の1要素 [SerializeField] int _instantateItemCount = 9; // 何個作っておくか [SerializeField] float _margin = 10; // 隙間 private int SAMPLE_ITEM_COUNT = 100; // サンプルで作成する数 // 生成したアイテムはLinkedListで管理する public LinkedList<RectTransform> itemList = new LinkedList<RectTransform>(); // cacheして処理の高速化を図る private RectTransform _rectTransform; private float _itemSizeheight = -1; // field protected float _diffPreFramePositionY = 0; // 一つ前の位置 protected int currentItemNo = 0; // 現在の一番上の位置 void Start() { _itemElementPrefab.gameObject.SetActive(false); for (int i = 0; i < _instantateItemCount; i++) { var item = GameObject.Instantiate(_itemElementPrefab) as RectTransform; item.SetParent(transform, false); item.name = "Recycle Item " + i.ToString(); item.anchoredPosition = new Vector2(0, -ItemHeight * i); itemList.AddLast(item); item.gameObject.SetActive(true); OnUpdateItem(i, item.gameObject); } // 先に全アイテム分だけscrollRectを広げておくのだ var delta = rectTransform.sizeDelta; delta.y = ItemHeight * SAMPLE_ITEM_COUNT; rectTransform.sizeDelta = delta; } void Update() { if (itemList.First == null) { return; } // 下 while (anchoredPositionY - _diffPreFramePositionY < -ItemHeight * 2) { _diffPreFramePositionY -= ItemHeight; var item = itemList.First.Value; itemList.RemoveFirst(); itemList.AddLast(item); var pos = ItemHeight * _instantateItemCount + ItemHeight * currentItemNo; item.anchoredPosition = new Vector2(0, -pos); OnUpdateItem(currentItemNo + _instantateItemCount, item.gameObject); currentItemNo++; } // 上 while (anchoredPositionY - _diffPreFramePositionY > 0) { _diffPreFramePositionY += ItemHeight; if(currentItemNo > 0) { var item = itemList.Last.Value; itemList.RemoveLast(); itemList.AddFirst(item); currentItemNo--; var pos = ItemHeight * currentItemNo; item.anchoredPosition = new Vector2(0, -pos); OnUpdateItem(currentItemNo, item.gameObject); } } } private void OnUpdateItem(int itemIndex, GameObject gameObject) { if (itemIndex < 0 || itemIndex >= SAMPLE_ITEM_COUNT) { gameObject.SetActive(false); } else { gameObject.SetActive(true); var listElement = gameObject.GetComponentInChildren<ItemElement>(); listElement.SetMessage("message == " + itemIndex); } } //----------------------------------------------------------------------- // getter private float anchoredPositionY { get { return -rectTransform.anchoredPosition.y; } } public float ItemHeight { get { if (_itemElementPrefab != null && _itemSizeheight == -1) { _itemSizeheight = _itemElementPrefab.sizeDelta.y + _margin; } return _itemSizeheight; } } protected RectTransform rectTransform { get { if (_rectTransform == null) _rectTransform = GetComponent<RectTransform>(); return _rectTransform; } } }
これをVerticalLayoutGroupの代わりに使えばOKです。
終わり!なんて言うとブーイングの嵐なので、ちょっと解説をば。
Start
void Start() { _itemElementPrefab.gameObject.SetActive(false); for (int i = 0; i < _instantateItemCount; i++) { var item = GameObject.Instantiate(_itemElementPrefab) as RectTransform; item.SetParent(transform, false); item.name = "Recycle Item " + i.ToString(); item.anchoredPosition = new Vector2(0, -ItemHeight * i); itemList.AddLast(item); item.gameObject.SetActive(true); OnUpdateItem(i, item.gameObject); } // 先に全アイテム分だけscrollRectを広げておくのだ var delta = rectTransform.sizeDelta; delta.y = ItemHeight * SAMPLE_ITEM_COUNT; rectTransform.sizeDelta = delta; }
ここでは、使い回す前提で指定個数しかprefabをinstantiateしないでおきます。
で、その際のanchorPositionを各アイテムの高さをずらして配置します。
今までであれば、VerticalLayoutGroupがやってくれていたことを自作するイメージです。
指定個数のprefabを作り終わったら、
このリスト全体の大きさを表示したい全ての項目分広げておきます。
(ここがポイントです。)
Update
スクロール処理はScrollRectがまかないます。
ScrollRectがcontentに指定しているRectTransformを動かします。
このupdateで動いた値をチェックして、逐一情報を更新します。
下に動かすか、上に動かすかで、リストの付与先(First or Last)が違うのに注意してください。
void Update() { if (itemList.First == null) { return; } // 下 while (anchoredPositionY - _diffPreFramePositionY < -ItemHeight * 2) { _diffPreFramePositionY -= ItemHeight; var item = itemList.First.Value; itemList.RemoveFirst(); itemList.AddLast(item); var pos = ItemHeight * _instantateItemCount + ItemHeight * currentItemNo; item.anchoredPosition = new Vector2(0, -pos); OnUpdateItem(currentItemNo + _instantateItemCount, item.gameObject); currentItemNo++; } // 上 while (anchoredPositionY - _diffPreFramePositionY > 0) { _diffPreFramePositionY += ItemHeight; if(currentItemNo > 0) { var item = itemList.Last.Value; itemList.RemoveLast(); itemList.AddFirst(item); currentItemNo--; var pos = ItemHeight * currentItemNo; item.anchoredPosition = new Vector2(0, -pos); OnUpdateItem(currentItemNo, item.gameObject); } } }
OnUpdateItem
ここでカラムの更新を行います。
最初に作った指定個数より少ない場合に注意。
private void OnUpdateItem(int itemIndex, GameObject gameObject) { if (itemIndex < 0 || itemIndex >= SAMPLE_ITEM_COUNT) { gameObject.SetActive(false); } else { gameObject.SetActive(true); var listElement = gameObject.GetComponentInChildren<ItemElement>(); listElement.SetMessage("message == " + itemIndex); } }
結果
簡単に要素を使い回すスクロール処理を作ることが出来ました。
今回の例では縦でしたが、xにすればもちろん横でも出来ます。
簡単なのに効果大です。
是非使ってみて下さい。
注意
「リストに入れるprefabの種類がいくつかあるんだけど…?」
こうなってくるとリサイクルがちょっと複雑になりそうです。
あくまでここでは単一のリストの例ってことで。
お疲れ様でした。
次はまた当分先になりそう!
サークル活動、ゲーム作りの方も頑張ります。
【Unity】unity1week「あつめる」 GOLD RUSHの実装について 【unity1week】
unity1week「あつめる」が終わりました
お疲れ様です。ぐーるです。
終わりましたunity1week。
今回も難産でした。
お題は「あつめる」。
ちょろっと作ったカードダンジョンRPGを利用して
ゴールドを集めるゲームにしようと思い立ちました。
Twitter上でも非常に多くの方が注目してくれて
気恥ずかしい限りでしたが、非常に励みになりました。
自分の「あつめる」はストレートにゴールドを集めるにしました。
— ぐーるさん (@uuha_goul) July 3, 2019
ダンジョンに潜ってゴールドを集めながらどこまで潜れるかを競うイメージ。
RPGとパズルゲームを合わせた感じを出したい。#unity1week pic.twitter.com/Hx32pFZhs8
本当にありがとうございます。
今回も毎回お馴染みの実装について紹介したいと思います。
なるべくコンパクトにまとめたいのですが、毎回長文です…。
設計について
正直あんまり話すこと無い…w
前回のやつと比べるとすごい簡単。全部uGUIだし。
前回のなんてObject Pool , Preload System , Recycler
とメモリ管理の嵐でInstantiateを鬼の形相で排除してました。
これに比べたら全然言うことがない。
設計といえるほど工夫した点は特に無いんですが、
カードはもちろん継承して汎用的に処理をまとめて
どっからでも処理できるようにしてました。
UtilやExtension,NCMB,Effect,Soundなどゲームを補助するシステム
は省いてるけど基盤はこんな感じです。
(基本的にメモリに乗っかっててほしいので、1sceneです。
3Dじゃないし、リソースそんなに多くないのでめちゃくちゃ軽量だしね)
EnemyEngine?
この関連図を見ると異様なものが一個ある。そうEnemyEngineです。
これは敵キャラのカードを作った後、そのカードのステータスを決定するシステムです。
フロアによって出現する敵を制御したり、パラメータをセットしたり、画像リソースを持ってたりする。
これ、本当に製品にするなら、全てのカードに作るべきだと思った。
「GoldCard」を作る時は必ず「GoldEngine」を経由する とか
カードを生成する時、パラメータが状況によって変わってくる。
武器やゴールドは値しかないので、その値入れるだけなのだが、
その処理を外に出しておくと見通しが良くなって拡張性が高くなっていく
今回は面倒でEnemy以外はそのまま書いてしまった。反省。
ちゃんとやるなら全てのカードはEngine化する
画面について
左半分カードフィールド側
正直ちょっと失敗だったかも。
カード含めて全てuGUIでやってました。
が、カードの上に攻撃パーティクルを表示したくて
それを出すのにSortingLayerとか使って涙ぐましい努力してたりします。
SpriteRenderer + TextMeshProにしちゃって、3Dオブジェクトとして扱えばもっとラクに色々出来たかもしれない。
また、フロアが進むと3x3 , 5x5 , 7x7 ...とどんどんフィールドが広くなっていくので、
メモリ管理にどうなのかとちょっと不安ではありました。
今はいいですが20x20とかやろうとすると
同一カードのグルーピングをしてバッチング処理を考えた実装にしないと影響が出るかも知れません。
右半分ステータス側
また、値周りはUniRXのReactiveProperty使って値変えるだけで即座に反映されるようにしてました。
例えばゲーム一周してタイトルに戻った時に値を初期化するのですが、
その時にnewし直すと当たり前にsubscribeが外れるので、上手く回そう。
(newと初期値投入は別にするとかね。やっぱりInitializerクラスを作って別でやるべきだったかなぁと。
初期化処理は分散してますが、大量に処理があります。)
プログラムについて
特筆すべき所は…ありません!w
とか言うと本当に何にもなくなっちゃうので、むりやり捻出。
(コード的に難しい事は何にもしてないのでちょっと恥ずかしい)
ゲームログを表示している箇所はキューイングして複数のログを受け取ることが出来て、
一個ずつ遅延評価してます。
using UnityEngine; using System.Collections; using UnityEngine.UI; using System.Text; using System.Linq; using System.Text.RegularExpressions; using System.Collections.Generic; namespace CardDungeon { public class TextWriter : MonoBehaviour { [SerializeField] private Text _text; // 全部フィールド化して少しでもメモリ管理をラクにしてあげたい // 涙ぐましい努力 private int _length; private string _textString; private StringBuilder _stringBuilder; private bool _isAnimation = false; private const float ONE_TIME = 0.01f; private List<string> _messageQue = new List<string>(); public void Initialize() { _isAnimation = false; _text.text = ""; _length = 0; _textString = ""; _stringBuilder = null; _messageQue = new List<string>(); } public void Show(string txt) { _messageQue.Add(txt); } private void Update() { if (!_isAnimation && _messageQue != null && _messageQue.Count > 0) { StartCoroutine(textWriter(_messageQue[0])); } } IEnumerator textWriter(string text) { _isAnimation = true; _length = 0; _stringBuilder = new StringBuilder(); _text.text = _stringBuilder.ToString(); yield return null; while (_length < text.Length) { _textString = text.Substring(_length, 1); _stringBuilder.Append(_textString); _text.text = _stringBuilder.ToString(); _length++; yield return new WaitForSeconds(ONE_TIME); } _text.text = text; yield return new WaitForSeconds(0.25f); _isAnimation = false; _messageQue.RemoveAt(0); yield return true; } } }
またPopupする処理は得意のプーリング。
namespace CardDungeon { public class PopupManager : SingletonMonoBehavior<PopupManager> { [SerializeField] Dialog _dialog; [SerializeField] Dialog _yesnoDialog; [SerializeField] RectTransform _popupCanvas; [SerializeField] PopText _popTextPrefab; private List<PopText> _popList; private readonly int POP_TEXT_CNT = 10; private bool _isEnable; public bool IsEnable { get => _isEnable; set => _isEnable = value; } public void Initialize() { if (_popList != null) { foreach (var p in _popList) { Destroy(p); } _popList.Clear(); } // 予め生成しておいて非表示する _popList = new List<PopText>(); for (var i = 0; i < POP_TEXT_CNT; i++) { var p = Instantiate(_popTextPrefab, _popupCanvas); p.Initialize(); _popList.Add(p); } _dialog.Initialized(); _yesnoDialog.Initialized(); } // 以下略 public void ShowPopup(string valueString, RectTransform rect, Vector3 adjustPos) { if (!_isEnable) return; if (_popList != null) { // 使用中でないポップアップを使用する var p = _popList.FirstOrDefault(pop => !pop.IsUse); if (p != null) { p.Show(valueString, rect, adjustPos); } } } } }
簡単なコードだけどゲームには効果抜群です。
unity1weekとか出してるやつはほとんどやってそうですけど。
金土日の追い込みでやったこと
ここが一番つらく、楽しい時間です。
4時間睡眠 x 2です。(気絶するように寝る)
基本的なシステムは出来上がっていたが、
御粗末すぎるので、それを以下に製品に近づけるかの作業でした。
- オンラインランキング
- 敵エンジン
- グラフィックを仮素材から正式版に(UIまで出来ず)
- バグ取り(結局取り切れず。まれに同じ場所にカードが作られてしまう)
- タイトル、チュートリアル、ゲームオーバー画面などのUI全部
- その他細かい調整など
これは寝れない…。
寝たら死ぬぞ!と思いながらストロングゼロ呑んでました。
拾った画像なんですが、凄くお気に入りです。
グラフィック周りは本当にうつらうつらと…。
終わって
今回もすごいゲームが多いです。
ほんとみんな発想の鬼だなと。
とある漫画家の方がインタビューで言ってましたが、
「目についたものでネームを切ってみる練習をしている」
と。
普段から「これをゲームにしたらどうだろう?」と考えながら
生活しているような人は発想力が鍛えられているのではないでしょうか。
自分もそうありたいものです。
ゲーム紹介ページにも書きましたが、沢山の課題が残ってます。
それでも遊んでくださった方がいて凄く嬉しいです。
本当にありがとうございます。
次回はもう少しぶっ飛んだ感じのやつ作りたいなと思ってます。
【Unity】 文字列に含まれる絵文字を判別する
お世話になっております。ぐーるです。
また久しぶりになってしまいました。
ずっと開発はしてて、RPGとカードゲームを2本同時に作成しているのですが、
このRPG作るのが凄く楽しいのです。
シナリオ、システム、グラフィック、キャラクター
全部自分で用意するのですが、
キャラクターの絵を描く、描いたキャラクターをいきいきと会話させる、
といった事だけでとても楽しい。
「こんな会話させよう」「こうしたらこう展開出来るな」
といったシーンを考えて実装してるだけで楽しいです。
(現在進捗率70%)
妄想癖が功を奏するなんて事もあるのだな、と思います。
閑話休題、今日はunityで絵文字の判別をする方法を共有しようかなと。
じゃあ対応する必要なくない?
使いたい、という要望があったり
外部サービスと連携していたりすると
対応する必要が出てきます。
Unicodeの絵文字って
Unicodeでは絵文字は上記ルールに沿って実現されています。
Unicode一つで表示出来るものもあれば、複数にまたがって表示しているものもあります。
これを一つ一つ判別する必要があります。
判別する方法として
- 正規表現
- 完全一致
の2つがあります。
正規表現パターンの方がオススメですが、
絵文字以外の文字を含めてしまう恐れがあります。
完全一致であれば、含めてしまう恐れが少なくなりますが、
追加があった場合、逐一入力する必要があります。
正規表現例
iOSで扱われるUnicode 6.0絵文字の判定をする正規表現 · GitHub
完全一致例
Unity-UI-emoji/info.txt at master · mcraiha/Unity-UI-emoji · GitHub
実装
完全一致で実装する場合のコード例を共有します。
絵文字コードをまるっとソースに入れちゃってますが、
これはブログ用の実装で、プロジェクトにするならTextAssetsなど
の方が管理はしやすいでしょう。
また、絵文字コード表は全ては網羅出来ていないと思われますので、ご了承ください。
コード例)
// inputからEmojiを取り除いた文を返却する public static string RemoveEmojiString(string inputString) { int i = 0; string firstString = null; string secondString = null; string threeString = null; string fourString = null; StringBuilder sb = new StringBuilder(); var uint32Size = sizeof(UInt32); // unicode byte配列に文字列を変換 byte[] unicodeBytes = Encoding.Unicode.GetBytes(inputString); // unicode配列からUTF32配列に変換 var utf32Bytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF32, unicodeBytes); // 配列の長さ int length = utf32Bytes.Length / uint32Size; while (i < length) { // 1文字目をチェック firstString = BitConverter.ToUInt32(utf32Bytes, i * uint32Size).ToString("X4"); // 2文字目までつなげてチェック if (i < (length - 1)) { secondString = firstString + "-" + BitConverter.ToUInt32(utf32Bytes, (i + 1) * uint32Size).ToString("X4"); } // 4文字目までつなげてチェック if (i < (length - 3)) { threeString = BitConverter.ToUInt32(utf32Bytes, (i + 2) * uint32Size).ToString("X4"); fourString = secondString + "-" + threeString + "-" + BitConverter.ToUInt32(utf32Bytes, (i + 3) * uint32Size).ToString("X4"); } // 後ろから文字コード表と合わせてチェックして一致してたら絵文字と判断して読み飛ばす if (EMOJI_CODES.Any(e => e.Equals(fourString))) { i += 4; } else if (EMOJI_CODES.Any(e => e.Equals(secondString))) { i += 2; } else if (EMOJI_CODES.Any(e => e.Equals(firstString))) { i += 1; } else { // 正しい文字だけ投入 sb.Append(Char.ConvertFromUtf32(BitConverter.ToInt32(utf32Bytes, i * uint32Size))); i += 1; } } return sb.ToString(); } // 絵文字コード一覧表 public static readonly string[] EMOJI_CODES = { "1F004", // 略 ~~~~~~~~ };
絵文字コード一覧表をそのまま載せるとあまりにソースが長くなるので別ファイルにしました。
あとがき
以上で絵文字の判別が出来ます。
コード見ると「はいはい、まあそうだよね」って感じですよね。
軽く調べるとサロゲートペアだけ判別すればいいよ、なんて
乱暴なコードもあったりして、ちょっと気になるなと思った次第です。
次はUnity1Weekの記事になりそうです。
うーん、もうちょっと更新したい。
【Unity】Unity2019.1で初級者でも本当に簡単にECSを実現する
ECSって難しそう
- ECSってのがあるらしい。
- Unityで大量のオブジェクトを表示しても軽いらしい。
- だがまだpreview版なので、仕様変更が激しいらしい。
という噂だけ聞いていて、
「ほーんでもまだゲームに使うには早そうだぬ」
と煎餅をバリバリ食べて放置していたんですが、
重い腰を上げてやってみたら凄い簡単に出来ちゃいました。
なので、記事にして記憶を残しておこうと思います。
ECSの解説…はちょっと置いといて
ECSってなに?ComponentSystemって?といった解説記事は
沢山の先駆者が残してくれていて、自分も存分に参考にさせて頂いてます。
ちゃんと知りたい!といった方は是非読んでみてください。
などなど、本当にいつもお世話になっております。
まず動かしてみよう
自分は、
まず動かしてみてから、動作を見つつ用語を紐づけて理解していく
スタイルでやってみようと思いました。
まず、単純に一つのcubeをECSを利用して表示するコードを紹介します。
導入
Unity 2019.1.1f1 を使用。
このサンプルでは新規プロジェクトから行ってますが、
途中のプロジェクトでも必要なpackageをインストールすれば問題なくECS使えます。
unityが開いたらPackageManagerを開きます。
Advancedからpreview packageを表示させます。
以下のpackageをinstallします。
- Burst
- Entities
- Hybrid Renderer
- Mathematics
- Jobs (後々必要になると思いますが、本当に最小なら無くても大丈夫です)
これで事前準備は完了です。
実装
早速コードを書いていきます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Entities; using Unity.Transforms; using Unity.Rendering; using Unity.Collections; public class ECSBootstrap : MonoBehaviour { [SerializeField] private Mesh _mesh; [SerializeField] private Material _material; private const int ECS_CREATE_COUNT = 1; // Start is called before the first frame update void Start() { InitializeECS(); } private void InitializeECS() { var entityManager = World.Active.EntityManager; var entityArchetype = entityManager.CreateArchetype( // renderer typeof(RenderMesh) ,typeof(LocalToWorld) // Transform ,typeof(Translation) ); NativeArray<Entity> entityArray = new NativeArray<Entity>(ECS_CREATE_COUNT, Allocator.Temp); entityManager.CreateEntity(entityArchetype, entityArray); for (var i = 0; i < entityArray.Length; i++) { Entity entity = entityArray[i]; entityManager.SetSharedComponentData(entity, new RenderMesh { mesh = _mesh, material = _material, }); } entityArray.Dispose(); } }
コードこれだけでOKです。
meshは標準で入っているcubeを、materialは適当にcreateして使用します。shaderはstandardで大丈夫です。
実行してみましょう。
はい、出ました。ECS簡単ですね。
画像では、Entity Debuggerというものも一緒に載せています。
これはECSで作られたEntityを確認することが出来るwindowsです。
Window > Analysisの所にあります。
ECSを利用してオブジェクトを生成するとInspectorに表示されないため、
どんな状態になっているか把握できないことがあります。
そういった時にこれを見ると、Entityが実際に作られているか確認することが出来ます。
この例で行くと、「Entity 0」が作られているのが確認できます。
Entityって?
EntityとはGameObjectに相当する構造体の事です。
ECSでは1Entityを1つの実体として処理します。
簡単に解説
Managerを取得
EntityManagerはECSのEntityを管理するクラスです。
ECSを利用するためにまずそのManagerを取得します。
// 現在有効なEntityManagerを取得
var entityManager = World.Active.EntityManager;
どのようなEntityを生成するか指定
続いてどのようなEntityを生成するか?を指定します。
cubeを表示するだけならLocalToWorld , Translationは必要ないのでは?
と思うかもしれませんが、描画する位置情報を処理するarcheTypeも追加しないと
カメラ上には出てきませんので、追加しています。
(Entity的には無くてもちゃんと作られます)
// どのようなEntityを生成するか指定 var entityArchetype = entityManager.CreateArchetype( // renderer typeof(RenderMesh) // Meshを描画する ,typeof(LocalToWorld) // 座標をWorld座標に変換させ3D空間に描画する // Transform ,typeof(Translation) // 座標を指定してEntityに反映させる );
Entityをいくつ作るかを指定
次にそのEntityはいくつ作るか?を指定します。
// いくつEntityを生成するかを指定。ここでは1つだが、この値を変えればその個数分作られる private const int ECS_CREATE_COUNT = 1; NativeArray<Entity> entityArray = new NativeArray<Entity>(ECS_CREATE_COUNT, Allocator.Temp); entityManager.CreateEntity(entityArchetype, entityArray);
Entityの初期設定
そして作ったEntityの初期設定を行います。
// 各Entityに対する初期設定を行います。 for (var i = 0; i < entityArray.Length; i++) { Entity entity = entityArray[i]; // 各EntityのRenderMeshに使用したいmeshとmaterialを渡します。 entityManager.SetSharedComponentData(entity, new RenderMesh { mesh = _mesh, material = _material, }); } entityArray.Dispose(); // ちゃんと破棄する
こんな流れになります。
初期位置を変更してみよう
つまり、初期設定で位置を指定すれば位置を変えることが出来そうだ。
やってみましょう。
まず個数を10にします。
private const int ECS_CREATE_COUNT = 10; // 10に変更
続いて、以下のように初期化処理にTranslationを操作する処理を追加します。
using Unity.Mathematics; // float3はUnity.Mathematics.float3なので entityManager.CreateEntity(entityArchetype, entityArray); for (var i = 0; i < entityArray.Length; i++) { Entity entity = entityArray[i]; entityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f)) }); // 追加 entityManager.SetSharedComponentData(entity, new RenderMesh { mesh = _mesh, material = _material, }); } entityArray.Dispose();
実行してみましょう。
Entityの個数が10に増え、位置がランダムになっています。
このようにentityManager.SetComponentDataで初期値を操作する事が可能です。
独自の処理を追加する
こっからです。
もちろん予め用意されたものだけではゲームなんて作れません。
独自の振る舞いを追加したいに決まっています。
初期値で定めた速度で上下に移動する処理
を追加したいと思います。
オリジナルのComponentDataを作成する
移動速度を持つComponentDataを作成します。
using Unity.Entities; // classではなく、structなのに注意して下さい。 public struct MoveSpeedComponent : IComponentData { public float _moveSpeed; }
Entityを移動させる処理を作成する
Entityをリアルタイムで操作する処理はComponentSystemが行っています。
そのComponentSystemを独自に実装する事で振る舞いを追加することが出来ます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Entities; using Unity.Transforms; using Unity.Mathematics; [AlwaysUpdateSystem] public class MoveSystem : ComponentSystem { protected override void OnUpdate() { Entities.ForEach((ref Translation translation, ref MoveSpeedComponent moveSpeedComponent) => { translation.Value.y += moveSpeedComponent._moveSpeed; // 上下反転 if (translation.Value.y > 5f) { moveSpeedComponent._moveSpeed = -math.abs(moveSpeedComponent._moveSpeed); } if (translation.Value.y < -5f) { moveSpeedComponent._moveSpeed = +math.abs(moveSpeedComponent._moveSpeed); } }); } }
生成した各EntityをForEachで回して、そのEntityが持っているComponentDataの参照を取得し処理を行うように記述します。
[AlwaysUpdateSystem]
これが無いと
このようにComponentSystemが動いていない状態になります。
独自にWorldを作成している場合はこの限りでは無いと思うのですが、この例ではWorldは一旦置いています。
archeTypeにMoveSpeedComponentを追加
作成したMoveSpeedComponentをEntityに追加します。
var entityArchetype = entityManager.CreateArchetype( // renderer typeof(RenderMesh) // Meshを描画する ,typeof(LocalToWorld) // 座標をWorld座標に変換させ3D空間に描画する // Transform ,typeof(Translation) // 座標を指定してEntityに反映させる // Custom ,typeof(MoveSpeedComponent) // Entityの移動速度を指定する );
Entityの初期化処理に移動速度を設定
移動速度をランダム値で初期化して、Entityごとに移動速度をバラつかせます。
entityManager.CreateEntity(entityArchetype, entityArray); for (var i = 0; i < entityArray.Length; i++) { Entity entity = entityArray[i]; entityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f)) }); entityManager.SetComponentData(entity, new MoveSpeedComponent { _moveSpeed = UnityEngine.Random.Range(-0.5f, 0.5f) }); // 追加 entityManager.SetSharedComponentData(entity, new RenderMesh { mesh = _mesh, material = _material, }); } entityArray.Dispose();
これで独自処理追加完了です。実行してみましょう。
Entityがランダムな速度で上下移動しているのが確認できました。
プラスワン
個数を10000にしてみましょう。
余りに大量なため、画面が埋まっていますが、
FPSに影響が無く動作する事が確認できます。
GameObjectがTransformで行っている処理であれば
Unity.Transforms.Scale , Unity.Transforms.Rotation
なども使えますし、
色を変化させたければ、RenderMeshのmaterialからcolorを変化させたりもできます。
(materialはSharedなので、全部一緒に変わりますが、shaderを使えばもっと複雑に出来そうではあります。)
Entities.ForEach((Entity e) => {
var renderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
var material = renderMesh.material;
// ~~~~
renderMesh.material = material;
});
こんな感じでRenderMeshにアクセスすることはできます。
とっかかりとして
ECSをまず動かす。という観点でやってみました。
ここから
- どんな仕組みで動いているんだろう
- Worldってなんだろう
- NativeArrayって何なんだろう
- なんでComponentDataは構造体しか使えないんだろう
- もっと複雑な処理をECSで実現したい
といったさらに深く学習していくとっかかりとなってくれたら幸いです。
反省
4月記事書いてない!最低でも一月一記事ぐらい投稿したかった。
次はもう少しゲームらしい記事を上げたい所です。
別件でストーリーのあるゲームを作成していまして
- ストーリーのプロット完成
- カットシーンのラフは完成(画像などは仮だが動作や文章は出来ている)
- 立ち絵、顔画像 (いまここ作成中)
となっています。
エターなりそうですが、出来るだけ頑張りたいと思ってます…。
【Unity】第11回unity1week「つながる」オブジェクトプールについて
またunity1weekが開催されましたね。
これで11回目になります。すべて参加してまして、
「じょじょにUnityに慣れてきてるんだなぁ、ちょっとずつだけど出来る事が広がっているな」
と自分なりに感じる結果となっています。
(今回作ったアイコン。文字はあれですが、ユニティちゃんはちょっと力作です。)
サイドバーに今までunity1weekで作ったアプリを表示してますので、
良かったら…やっぱ見ないでいいです。
初期のものはコードも酷いし、unityわかってないし、絵も下手くそですごく恥ずかしいですが
恥を忍んで、公開したままにしてます。成長の証って事で…。
お題について
お題もそうなんですが、実は毎回自分に対して
「今回は○○について勉強すること」
という題目を決めて参加してます。
今回は「オブジェクトプーリング」「負荷対策」について
主に取り上げてみようかなと漠然と考えてました。
事件がおこる
Unityでビル街を自動生成するツールを作った。
— ぐーるさん (@uuha_goul) February 26, 2019
単純なランダムじゃなくて、必ず道が作られるようにした。 pic.twitter.com/dZHQf0aFvE
なんだこれ。けもみみおーこくすげーな。って事件です。
この辺りではまだ勉強段階で、
「こうやったらこんなこと出来るのかなぁ」
という推測を元にスクラップ&ビルドを繰り返してました。
で、これをゲームにしようと思いました。
つながる要素
前回の最大の反省点は「難しすぎた事」でした。
僕は格闘ゲームとかもプレイしてるのですが、
基本的に「連打をしない」ということが染み付いていまして、
どんなゲームでもリズムよく(タイミングよくポンポンポンと)
押すのが普通になってしまっていました。
ぱふもどきさんの放送を見てたのですが、
凄い連打してたり、位置取りとかあんまり気にしてなかったりと
「ああ、これが普通の人だ。自分が間違っていた…」
とちょっとショックを受けました。
その反省を活かそうと、今回はなるべく複雑な操作、
システムは止めようと漠然と考えていて
コンボが繋がるとスコアがどんどん伸びていく感じがわかりやすいだろう
とシンプルな要素に落ち着かせました。
破壊について
「ボロノイ図」とかでググってみてね。
voronoiのアルゴリズムを利用して作ってます。
自分なりにやっちゃってるので、なんちゃってボロノイかもしれませんので割愛。
最初のランダム点配置は Random.insideUnitSphere でやっちゃってます。
これオンラインで同期する場合はRandom.seedの共有も必要になるんだろうな…。
オブジェクトプールと破片について
一ブロックにつき、10~20の破片にランダムで分解されるような仕様にしています。
この各破片は一つ一つがGameObjectになってます。
Rigidbodyで物理演算の影響も受けないとならないため、別々なオブジェクトにしないとなりませんでした。
そして以前から告知している通り、随時動的に計算して作っています。
単純に毎回Instantiateするとプチフリーズの嵐になってしまうので以下のようにプールを作りました。
PartsPool.cs public void InitializePool() { if (IsPopulated && parts.Length == maxPoolSize) return; // Clear old shard pool if one exists. if (parts != null) { foreach (var part in parts) { if (part == null) continue; DestroyImmediate(part.gameObject); } } parts = new Part[maxPoolSize]; for (int i = 0; i < maxPoolSize; i++) { parts[i] = new GameObject("Part").AddComponent<Part>(); parts[i].transform.parent = transform; } }
このプールは破片プールです。
各破片はここにあるGameObjectを使いまわします。
800個から1000個ほど生成しておいていました。
Part.cs void Initialize() { if (GetComponent<Renderer>() == null) gameObject.AddComponent<MeshRenderer>(); if (GetComponent<Rigidbody>() == null) gameObject.AddComponent<Rigidbody>(); meshFilter = GetComponent<MeshFilter>() == null ? gameObject.AddComponent<MeshFilter>() : GetComponent<MeshFilter>(); meshCollider = GetComponent<MeshCollider>() == null ? gameObject.AddComponent<MeshCollider>() : GetComponent<MeshCollider>(); meshCollider.convex = true; meshFilter.sharedMesh = new Mesh(); IsUse = false; gameObject.SetActive(false); }
破片はゲーム内でInstatiateせず、Inspector上で800個用意して、Activeを切っておきます。
また各種必要なコンポーネントを変数に持っておきアクセスを容易にしときます。
Part.cs public void Use(GameObject parent, Vector3[] newVertices, Vector3[] newNormals, int[] newTriangles, Vector2[] newUVs) { part.name = "Fractured Parts"; var mesh = new Mesh(); part.Mesh = mesh; part.Mesh.vertices = newVertices; part.Mesh.normals = newNormals; part.Mesh.uv = newUVs; part.Mesh.triangles = newTriangles; part.meshCollider.sharedMesh = part.Mesh; part.gameObject.SetActive(true); part.IsUse = true; part.GetComponent<Renderer>().material = parent.GetComponent<Renderer>().material; if (parent.GetComponent<Rigidbody>()) { part.GetComponent<Rigidbody>().mass = parent.GetComponent<Rigidbody>().mass; part.GetComponent<Rigidbody>().velocity = parent.GetComponent<Rigidbody>().velocity; } }
使う時にActive化して、位置、大きさ、Meshをセットしてあげます。
また、この破片はparentとなる元オブジェクトが管理しており、
設定した時間後に削除されます。この削除は非アクティブ状態にして、Poolに戻るだけにしておきます。
PartsPool.cs public static void BackPool(GameObject obj , float waitTime) { var part = obj.GetComponent<Part>(); if (part != null) { pool.StartCoroutine(pool._BackPool(part , waitTime)); } } private System.Collections.IEnumerator _BackPool(Part part, float waitTime) { yield return new WaitForSeconds(waitTime); part.IsUse = false; part.gameObject.SetActive(false); part.transform.parent = transform; }
(一定時間後に削除する、というのをCoroutineで簡易的に実装してます。)
こうすることによって、poolingを使い回すことが可能です。
これでゲーム中はInstatiateとDestroyを使わずに進めることが可能です。
(IsUseがfalseのものをプールから取ってくればOKですね)
Effect , PopupScoreなども同様にInGame中にはInstantiateせず、Active切り替えで使いまわしてます。
CameraPlay
これ知らなかったんですが、こんな凄いAssetがあるとは…。
有料なんですが、カメラ演出は効果が大きいので、コストに見合う効果が出ます。
画面を揺らす、集中線を出すといったことが1行でかけます。
便利すぎる。最初はやりまくってたんですが、気持ち悪くならない程度にちょっと抑えました。
AudioManager
同時に複数のオブジェクトを破壊するので、音も大量に一度に鳴ります。
AudioSourceは最終的に10個用意していて、その中で使ってないAudioSourceを探して使うようにしてます。
15でも良かったかも。
NCMB
バカやって変な不具合出してた。別記事にまとめたので、そちらをどうぞ
自分のせいです。これもうちょっと作る必要あるなぁ…。通信中表示とかまだ甘い。
その他色々
最後はほんとに不具合直すのと、違和感を削る作業(調整とも言う)に忙殺されますね。
ゲームプレイ > ここ変だな、これ欲しいな > 直す、作る
これを繰り返しまくる。土日はほとんどこれ。
ここでポップアップやらスコア動かしたり操作説明付けたりなど。
この辺りが今まで甘かったのだけど、少しずつ計算に入れて作業を進めることが
出来るようになってきたなと思う。
お絵かき
前回はバリバリ描きまくりで疲れましたが、
今回はほとんど要らなかったのでアイコンしか描けず。
文字は酷いので外したverを。
割とそれっぽく描けたかな…と思います。リリースギリギリに滑り込み。
元ネタは「何でも言うことを聞いてくれるアカネちゃん」です。
反省点
- ステージとかもう一つぐらい作れば良かった
- ボムアイテム作りたかった(どうしてもエフェクトが間に合わず…Blenderに慣れないと…)
- 敵とか居たらもっと良かったかも。むしろ街中で怪獣とユニティちゃんが戦うゲームにするのはどうだろうか
- 甘城ブリリアントパークが面白すぎた
- SEKIROが楽しみすぎてそれどころじゃなかった
- WebGLの日本語入力これか。時間があったらやってみよう
次回
もちろん参加します。
今回はお絵描きがほとんど出来なかったので、
次はノベル要素強めとかやりたいなと思ってます。
xNode使ってエディタ拡張の勉強かなー。
【Unity】 NCMB 405 エラー Method not Allowed 【オンラインランキング】
お疲れ様です。ぐーるです。unity1weekお疲れ様でした。
その記事を書いているのですが、その前に一個メモしておきたいことが。
お恥ずかしいミスなのですが…。
NCMBって何?
クラウド上に用意された機能をAPIで使用するだけでサーバ開発、運用不要で
バックエンドサービスを利用することが出来るサービスのことをmBaasと言いまして、
niftyが提供しているmBaasを
nifty cloud mobile backend == NCMB
と呼んでいるようです。まぁ楽にサーバ機能を使うことが出来るものです。
unity SDKも用意されていて、簡単に使うことが出来ます。
使い方はググると公式もありますし、沢山記事もあるので割愛します。
Method not allowed????
NCMBを利用してデータをテーブルに追加する時に以下のようなコードを書きます。
public void TestSend(string objectId , string name , int score) { NCMBObject obj = new NCMBObject("TestTable"); obj.ObjectId = objectId; obj["name"] = name; obj["score"] = score; obj.SaveAsync((NCMBException e) => { if (e == null) { Debug.Log("success"); } else { //エラー処理 Debug.LogError(e); } }); }
このSaveAsyncで以下のエラーが出てしまいました。
【url】:https://mb.api.cloud.nifty.com/xxxxxxxxxxxxxxx/TestTable/ 【type】:PUT 【content】:{"name":"test user name","score":8307} 【StatusCode】:405 【Error】:NCMB.NCMBException: Method not allowed. 【ResponseData】:
これなんだろう?と。
原因
NCMBを利用してデータを入れる際は以下のように判定されます。
obj.ObjectId = objectId; <- ここ obj.ObjectId [ null ] == 新規レコードを追加 obj.ObjectId [ (存在しているObjectId) ] == 該当レコードを更新
となります。
ちなみに、新規レコード追加した際にobj.ObjectIdに値が入って返ってくるため、
その値を利用して次回以降は更新することが出来ます。
ここが以下の値だとエラーになります
obj.ObjectId [ (存在していないObjectId) ] == 404エラー No data available. 【url】:https://mb.api.cloud.nifty.com/xxxxxxxxxxxxxx/TestTable/aaaaa 【type】:PUT 【content】:{"name":"name","score":2595} UnityEngine.Debug:Log(Object) 【StatusCode】:404 【Error】:NCMB.NCMBException: No data available. 【ResponseData】: UnityEngine.Debug:Log(Object)
obj.ObjectId [ "" ] == 405エラー Method not allowed. 【url】:https://mb.api.cloud.nifty.com/xxxxxxxxxxxx/TestTable/ 【type】:PUT 【content】:{"name":"test user name","score":8307} 【StatusCode】:405 【Error】:NCMB.NCMBException: Method not allowed. 【ResponseData】:
となります。つまりObjectIdが""だと、エラーになってしまう…と。
反省
NCMBを利用する時に、簡単に考えると
「ObjectIdはPrayerPrefsにでも保存しとくか」
と考えてこんなコードを書いてしまいました。
obj.ObjectId = PlayerPrefs.getString("NCMB_ObjectId");
これで""になってしまってたのが原因…。はずかしー。
ちゃんとデフォルト値を入れとけって事ですね。反省。
obj.ObjectId = PlayerPrefs.GetString("NCMB_ObjectId" , null);