ルパン三世で学ぶ Unity の Action と Event 購読(ウルトラ便利!!)
ご無沙汰しております。ユウです。仕事ではUnityを使っているのですがブログをめっきり更新できていませんでした。 記事を書こうとすると2-3時間かかるので、仕事終わりに書こうと思うとなかなか進まないのですよね。テヘヘ。
さて、今回は備考録も兼ねて Action
と Event
についての記事を書きたいと思います。
最初にいいます。コレくっそ便利だからみんなにも知ってほしい!!!!!1
Action と Event とかいうくっそ便利なもの。みんなにもぜひ知ってほしい!!(大事なことを2回言う)
Eventってなんだろう?
ややこしいことより、何ができるのか。それがみなさん知りたいことですよね? わかっております。
ざっくり言うと、特定の行動や処理を お気に入りに追加
することができる機能です。 ……。さすがにざっくりすぎるぞ!
たとえばこんな処理
複数のスクリプトで連携している場合、
イベントの発火を起点に色々やりたいことがあるかと思います。
普通にやるなら、他のスクリプトをGetComponentとかで参照してそこのpublicなメソッドなりを呼んだり、SendMessage
したりするのですが、Eventを使えばもっと自然かつ簡単にイベントの発生を監視することができます。
よく購読なんていうふうにも言われていたりします。イベントが発生したら購読している側で処理を行うことができるっていうわけです。 しかし、こんなの文章で書かれてもわからないですよね。サンプルプログラムにいってみましょう!!
ルパン三世で学ぶ、EventとAction
さて、実際にコードを書くのが一番わかりやすい。これ鉄則ですね。正直細かいことはわからないでも、動けば良いのです。 動かないコードより、雑でもいいから動くコードのほうが100万倍価値があります!
では、これから作るプログラムのイメージは下記の通りです 銭形警部がルパンの行動を購読(監視)し、ルパンが動いたのをみたらすぐ動けるようにしてみましょう。
- 銭形警部(Zenigata.cs)
ルパンを監視するUnity警察であり、インターポールの刑事。 犯人の悪行を感知すると即逮捕だルパ~ン!
- ルパン(Lupin.cs)
様々な悪行を行う犯人。三世。銭形警部に行動を購読(監視)されている。
監視される側を作る(ルパン三世)
まずはルパンを作りましょう。ルパン三世は下記の行動を持っておりそれらを銭形に監視されています
- 盗み(成功)
- 盗み(失敗)
- 予告
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class Lupin : MonoBehaviour { // イベント発行 public event Action<bool> OnSteelListener; // 盗みを行った public event Action OnNoticeListener; // 犯行予告を行った // Update is called once per frame void Update() { if (Input.GetKeyDown(KeyCode.W)) { Debug.Log("ぬふふふっ、お宝いただきだぜとっつぁーん♪"); OnSteelListener?.Invoke(true); // 盗み(成功)イベント発火 }else if (Input.GetKeyDown(KeyCode.S)) { Debug.Log("ちっくしょぉーってんなぁろぉ! ふじこに盗まれちった。とほほ"); OnSteelListener?.Invoke(false); // 盗み(失敗)イベント発火 }else if (Input.GetKeyDown(KeyCode.A)) { Debug.Log("[予告状] お宝は明日の3時にいただくぜ。ルパン三世"); OnNoticeListener?.Invoke(); // 予告イベント発火 } } }
まず注意するのがusing System;
を行うことです。これでAction
が利用できるようになります。僕もよくわかってないですが、Actionってのはなんかメソッドみたいなことらしいです。
次に見てもらいたいのが、イベントリスナーの設定です。このスクリプト側が発行するイベントを定義しています。
public event Action<bool> OnSteelListener; // 盗みを行った public event Action OnNoticeListener; // 犯行予告を行った
event
をつけてAction
型で定義しているところがそうです。上のものはBool型の引数みたいなものを持っています。下のはとりあえず発火すれば問答無用でそのまま発火します。
つまり、特定の動作や処理を行ったことをListenerに対して通知するための準備です。お知らせマークというかなんというか。とにかくそんな感じです。
発火するイベントの設定が終ったのであとはスクリプトの好きなタイミングでイベントを発火します。発火するにはInvoke()
を利用します。
if (Input.GetKeyDown(KeyCode.W)) { Debug.Log("ぬふふふっ、お宝いただきだぜとっつぁーん♪"); OnSteelListener?.Invoke(true); // 盗み(成功)イベント発火 }else if (Input.GetKeyDown(KeyCode.S)) { Debug.Log("ちっくしょぉーってんなぁろぉ! ふじこに盗まれちった。とほほ"); OnSteelListener?.Invoke(false); // 盗み(失敗)イベント発火 }else if (Input.GetKeyDown(KeyCode.A)) { Debug.Log("[予告状] お宝は明日の3時にいただくぜ。ルパン三世"); OnNoticeListener?.Invoke(); // 予告イベント発火 }
Wキーを押すと盗みが成功し、Sキーを押すと盗みが失敗。Aキーを押すと予告を行います。それぞれでイベントが発火しています。 Listener名のあとに?がついているのはnullチェックしているだけらしいのでなくてもOKです。僕もわからず使っています(´ω`)
銭形警部を作る
ご丁寧にルパンの行動を監視できるイベントが作られたのであとは監視する銭形警部の登場です。 ルパンのほうの行動でListenerが作られているので、銭形警部はそれをチェックして動きがあれば対応をするというだけにとどめます。 つまり、銭形警部からのほうから能動的に何かをするという動き方はしません。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zenigata : MonoBehaviour { [SerializeField] Lupin lupin; // 監視対象のルパンをアタッチ // Start is called before the first frame update void Start() { // ルパンの予告状を感知した際の銭形警部の行動 lupin.OnNoticeListener += () => { Debug.Log("インターポールの銭形です。予告を送ってきたルパンはかならずや逮捕してみせます。おまかせください。"); }; // ルパンの盗み行為を感知した際の銭形警部の行動 // steelFlag に bool の結果を受け取る。 lupin.OnSteelListener += steelFlag => { if (steelFlag) { Debug.Log("なぁにぃ!? まんまとルパンに盗まれただとぉ。一体何をやっとるんだ。逮捕だルパーン!"); } else { Debug.Log("ガーッハッハッハ! ルパンめ、盗みに失敗したぞ。ワシの目の黒いうちは盗みなどさせんぞ!"); } }; } }
あくまでも銭形警部からは動かず、監視している行動をひたすらチェックして動きがあればこちらも行動を起こします。
参照するために今回はSerializeField
でターゲットをアタッチしています。これは別にスクリプト側で指定して参照をもってきてもOKです。
// ルパンの予告状を感知した際の銭形警部の行動 lupin.OnNoticeListener += () => { Debug.Log("インターポールの銭形です。予告を送ってきたルパンはかならずや逮捕してみせます。おまかせください。"); };
まずは単体のListenerを購読しています。メルマガの登録とかに似ています、購読しとくからなにかあったら連絡してね。みたいな感じです。今回の場合ルパンに動きがあれば通知されるので、銭形警部はそれに対してどう動くかを記述しています。
ラムダ式で書きましたが、メソッドを指定することもできます。ここで大事なのがこの+=
の書き方です。これがいわゆる「購読」を表しています。察しの良い皆様ならおわかりの通り、プラスで購読なら、-=
のマイナスで購読を解除できます。簡単ですね!
一方、Bool型の引数があった盗みのListenerの場合はまずbool型を受け取る必要があります。なのでこんな書き方をしています。
// ルパンの盗み行為を感知した際の銭形警部の行動 // steelFlag に bool の結果を受け取る。 lupin.OnSteelListener += steelFlag => { if (steelFlag) { Debug.Log("なぁにぃ!? まんまとルパンに盗まれただとぉ。一体何をやっとるんだ。逮捕だルパーン!"); } else { Debug.Log("ガーッハッハッハ! ルパンめ、盗みに失敗したぞ。ワシの目の黒いうちは盗みなどさせんぞ!"); } };
書き方は殆ど変わりません。ルパンのListenerを購読し、steelFlag でBoolを受け取ってラムダ式の中で処理をわけています。 イベントを発火したことだけじゃなく、たとえばそのイベントがどういう結果だったのかを知りたいときはこのように引数をつけて発火させて、それを踏まえた上で発火したイベントをキャッチして処理を分けて書きます。
いざ逮捕だルパァ~ン!
さて、両方のスクリプトを空のゲームオブジェクトにでもアタッチして準備します。 それでは早速実行してみましょう!!!
- Wキーを押すと、ルパンが盗みを成功させます。それを感知した銭形が憤怒します。
- Sキーを押すと、ルパンが盗みを失敗します。それを感知した銭形が笑います
- Aキーを押すと、ルパンが予告状を送ります。インターポールの銭形が警戒を強めます。
gifがちゃんとアップできていればご覧いただけると思いますが、ルパン側しか能動的に動いていないにもかかわらず、銭形警部はルパンの行動を購読しているのでイベントが発火するとそれに対応した動きを行うことができるのです。 これの何が良いのか。それはスクリプト同士の依存度が下がることだと言えます。メソッド名を直接呼び出したり、SendMessageをすれば似たような動きを作ることができますが、それは「それありき」になってしまいます。いじるときには全ていじらなければなりません。
しかし、このイベントの購読によるアプローチならそれぞれのスクリプトでは独立性が保たれています。ルパン側はあくまでも行動をしたよというイベントをListenerに向けてお知らせしているだけ。 銭形警部も購読しているルパンのイベントが起こったらそれに対応した行動をとっているだけです。別にいきなり銭形警部がいなくなってもルパンのスクリプトはエラーになりません。
逆にルパンを監視する新しい刑事を簡単に追加することもできます。ただイベントを購読すれば良いだけだからです!
次元大介を追加する
イベントを監視するだけなので追加も簡単です。では次元大介を簡単に追加してみましょう。
Zigen.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zigen : MonoBehaviour { [SerializeField] Lupin lupin; // 監視対象のルパンをアタッチ // Start is called before the first frame update void Start() { lupin.OnNoticeListener += () => { Debug.Log("俺は下りるぜ、ルパン"); }; } }
こちらもアタッチして実行してみると…?
Aキーで予告状を出すと次元がおりてしまいます。このようにイベントを発火する側と、それを購読する側の結びつきが薄めなので追加や削除が容易なのが最大の特徴です。 これは様々なことに利用できます。
- ステージのロードが終わったらゲーム開始できることを通知する
- 敵とエンカウントしたら、エンカウント用のUIを表示する
- ログインが成功したら、ログインボーナスを表示する
- プレイヤーがゴールしたら、ゴールしたイベントを発火する。UIとシステムの両方でイベントを受け取ってそれぞれ処理を行う
などなど。使い道は無限大。みんなもEventを使ってUnityをもっと楽しもう!
Instantiateするならば、一度は夢見るキャッシングという言葉
時代はエコ。それはUnity業界にも
今回作るのはこんな感じのものです。 クリックするとInstantiateされるのですが、ヒエラルキーを見るとひたすら新規にクローンを作るわけでなく非アクティブのものがあれば再利用していることがわかります。
Instantiate連発は避けたい事情
Instantiate
を使ってゲームオブジェクトのクローンを作成するのはゲームを作っているとよくありますね。
たとえば、シューティングゲームで自機から発射する弾などは入力があったらInstantiate
を行って弾を生成して飛ばす、なんていう処理をしますよね
もちろんひたすらクローンを作る処理でも動くは動きます。しかし無秩序にInstantiate
を連発するのはあまりよろしくありません。
入力があるたびにクローンを作っていては、それこそドラえもんのバイバインのように無限に増え続けていく恐ろしい事態になってしまいます。いずれは深刻な宇宙の栗まんじゅう汚染が起こってしまうでしょう。
オブジェクト数の問題だけならひたすらDestroyすれば良いという話になるのですが、Instantiate
自体が重い処理なので再利用できるならそっちを使おうっていう感じの考え方ですね!
解決方法はいろいろある
生成数を最初からキメておく
それを解決するためのアプローチとしては、「作成したオブジェクトを可能な限り再利用する」ということが考えられます。 まず、先程のシューティングゲームであれば「プレイヤーが発射できる弾は10発まで」と明示的に決めてしまう方法が考えられます。 この場合であればまず最初に10回Instantiateで生成して確保しておけばあとはInstantiateをしないでひたすら使い回せばOKです。
動的に生成量を管理する
それ以外に、「入力量に応じて動的に確保数を決定する」という考え方も可能です。 これは、たとえばプレイヤーが20回発射したとします。まず初回はオブジェクトが無いのでInstantiateで1個作ります。このときこのオブジェクトをListなどに入れて管理します。 2発目を発射する際には、Listに入れて管理しているオブジェクトをチェックして「再利用可能」かどうかをチェックします。再利用可能であれば最初に作ったものを再利用し、再利用可能でなければ新たにInstantiateして作成します。
再利用可能かどうかというのは、フラグをもたせて管理するのが良いでしょう。たとえばシューティングゲームだったら「敵にあたった弾」や「画面外に出た弾」などはもう使用済みですよね。非表示にして再利用待機状態にまわしておき、再び発射するときにこの弾を再利用します。
とりあえずサンプルを書いてみよう
今回は、後者のListを使った動的なキャッシングに挑戦します。といってもサンプルプログラムはCubeをただ落とすだけのものです。
1. PrefabのCubeの準備
まずは、Unityプロジェクトを作成し「Cube」を作ります。 作成した「Cube」に「RigidBody」をアタッチしておきましょう。 また、下記「CubeController.cs」をC#Scriptで作成しCubeにアタッチしておきます。
CubeController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CubeController : MonoBehaviour { void Update() { if(gameObject.transform.position.y <= -10) { GetComponent<Rigidbody>().isKinematic = true; gameObject.SetActive(false); } } }
y座標が-10以下になったら、Kinematicかつ、自身を非表示にするだけのシンプルな動きです。
あとは、このCubeをPrefab化しておきます。
2. Cubeを作成するスクリプトの作成
下記コードでスクリプトを作り、適当な空のゲームオブジェクトにでもアタッチしておきます。
Test.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Test : MonoBehaviour { [SerializeField] GameObject prefab; List<GameObject> cubeMaster = new List<GameObject>(); // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { bool spawnFlag = true; // リストに登録されているならチェック if(cubeMaster.Count != 0) { // リストの中身をすべてチェック for(int i=0; i<cubeMaster.Count; i++) { // 非アクティブなら再利用 if (!cubeMaster[i].activeInHierarchy) { cubeMaster[i].transform.position = Vector3.zero; cubeMaster[i].GetComponent<Rigidbody>().isKinematic = false; cubeMaster[i].SetActive(true); spawnFlag = false; break; } } } if (spawnFlag) { var obj = Instantiate(prefab) as GameObject; cubeMaster.Add(obj); } } } }
画面をクリックするとPrefabを作るというだけのシンプルなものです。 ヒエラルキーからPrefabにさきほどのCubeのPrefabをアタッチしておきましょう。
3. 実行してみよう
ヒエラルキーに注目してもらうと、クリックしたときに非アクティブのCubeがあれば再利用していることがわかります。動画の後半ではクリック数を増やしていますが、再利用可能なタイミングであればクローンを作らずに再利用してInstantiate
を避けるようになっています。
Listに再利用可能なものがない場合は新しく作成していますが、それも含めてListに追加するので一時的に連打した場合でもその後は再利用可能なオブジェクトのキャパが増えるのでよっぽど連打しない限りはInstantiate
を避けて再利用するようになります。
このようにユーザー側の入力にあわせてキャッシュするオブジェクトの数が変わるのが、動的なキャッシングです。
4. 結論
このようにキャッシュして再利用することを意識することで、無駄な処理を防ぐことができます。今回は非アクティブなら再利用というやり方になっていますが、内部にbool型で再利用可能かどうかのフラグをもたせてそれで管理するのでも良いでしょう。
Instantiate
を乱発すると処理も重くなってしまいますので、再利用できそうな処理であればキャッシュして使っていくやり方をぜひ試してみましょう!