初心者がUnityでなんかしちゃうぞBlog

Unity初心者は、ゲーム開発の夢を見るか?

ルパン三世で学ぶ Unity の Action と Event 購読(ウルトラ便利!!)

ご無沙汰しております。ユウです。仕事ではUnityを使っているのですがブログをめっきり更新できていませんでした。 記事を書こうとすると2-3時間かかるので、仕事終わりに書こうと思うとなかなか進まないのですよね。テヘヘ。

さて、今回は備考録も兼ねて ActionEvent についての記事を書きたいと思います。 最初にいいます。コレくっそ便利だからみんなにも知ってほしい!!!!!1 Action と Event とかいうくっそ便利なもの。みんなにもぜひ知ってほしい!!(大事なことを2回言う)

Eventってなんだろう?

f:id:yuu9048:20190515000417g:plain

ややこしいことより、何ができるのか。それがみなさん知りたいことですよね? わかっております。 ざっくり言うと、特定の行動や処理を お気に入りに追加 することができる機能です。 ……。さすがにざっくりすぎるぞ!

たとえばこんな処理

複数のスクリプトで連携している場合、

  • 「こっちのスクリプトでこれやったらこうしたい」
  • 「敵キャラクターとエンカウントしたら、メッセージを表示させたい」など

イベントの発火を起点に色々やりたいことがあるかと思います。 普通にやるなら、他のスクリプトをGetComponentとかで参照してそこのpublicなメソッドなりを呼んだり、SendMessage したりするのですが、Eventを使えばもっと自然かつ簡単にイベントの発生を監視することができます。

よく購読なんていうふうにも言われていたりします。イベントが発生したら購読している側で処理を行うことができるっていうわけです。 しかし、こんなの文章で書かれてもわからないですよね。サンプルプログラムにいってみましょう!!

ルパン三世で学ぶ、EventとAction

さて、実際にコードを書くのが一番わかりやすい。これ鉄則ですね。正直細かいことはわからないでも、動けば良いのです。 動かないコードより、雑でもいいから動くコードのほうが100万倍価値があります!

では、これから作るプログラムのイメージは下記の通りです 銭形警部がルパンの行動を購読(監視)し、ルパンが動いたのをみたらすぐ動けるようにしてみましょう。

  • 銭形警部(Zenigata.cs)

ルパンを監視するUnity警察であり、インターポールの刑事。 犯人の悪行を感知すると即逮捕だルパ~ン!

f:id:yuu9048:20190514225343j:plain

  • ルパン(Lupin.cs)

様々な悪行を行う犯人。三世。銭形警部に行動を購読(監視)されている。

f:id:yuu9048:20190514225432p:plain

監視される側を作る(ルパン三世

f:id:yuu9048:20190514225432p:plain

まずはルパンを作りましょう。ルパン三世は下記の行動を持っておりそれらを銭形に監視されています

  • 盗み(成功)
  • 盗み(失敗)
  • 予告
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です。僕もわからず使っています(´ω`)

さぁ、これで世紀の大怪盗ルパン三世スクリプトは完成です!

銭形警部を作る

f:id:yuu9048:20190514225343j:plain

ご丁寧にルパンの行動を監視できるイベントが作られたのであとは監視する銭形警部の登場です。 ルパンのほうの行動で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を受け取ってラムダ式の中で処理をわけています。 イベントを発火したことだけじゃなく、たとえばそのイベントがどういう結果だったのかを知りたいときはこのように引数をつけて発火させて、それを踏まえた上で発火したイベントをキャッチして処理を分けて書きます。

いざ逮捕だルパァ~ン!

さて、両方のスクリプトを空のゲームオブジェクトにでもアタッチして準備します。 それでは早速実行してみましょう!!!

f:id:yuu9048:20190515000417g:plain

  • Wキーを押すと、ルパンが盗みを成功させます。それを感知した銭形が憤怒します。
  • Sキーを押すと、ルパンが盗みを失敗します。それを感知した銭形が笑います
  • Aキーを押すと、ルパンが予告状を送ります。インターポールの銭形が警戒を強めます。

gifがちゃんとアップできていればご覧いただけると思いますが、ルパン側しか能動的に動いていないにもかかわらず、銭形警部はルパンの行動を購読しているのでイベントが発火するとそれに対応した動きを行うことができるのです。 これの何が良いのか。それはスクリプト同士の依存度が下がることだと言えます。メソッド名を直接呼び出したり、SendMessageをすれば似たような動きを作ることができますが、それは「それありき」になってしまいます。いじるときには全ていじらなければなりません。

しかし、このイベントの購読によるアプローチならそれぞれスクリプトでは独立性が保たれています。ルパン側はあくまでも行動をしたよというイベントをListenerに向けてお知らせしているだけ。 銭形警部も購読しているルパンのイベントが起こったらそれに対応した行動をとっているだけです。別にいきなり銭形警部がいなくなってもルパンのスクリプトはエラーになりません。

逆にルパンを監視する新しい刑事を簡単に追加することもできます。ただイベントを購読すれば良いだけだからです!

次元大介を追加する

イベントを監視するだけなので追加も簡単です。では次元大介を簡単に追加してみましょう。 f:id:yuu9048:20190514235038p:plain

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("俺は下りるぜ、ルパン"); };
    }
}

こちらもアタッチして実行してみると…?

f:id:yuu9048:20190515000744g:plain

Aキーで予告状を出すと次元がおりてしまいます。このようにイベントを発火する側と、それを購読する側の結びつきが薄めなので追加や削除が容易なのが最大の特徴です。 これは様々なことに利用できます。

  • ステージのロードが終わったらゲーム開始できることを通知する
  • 敵とエンカウントしたら、エンカウント用のUIを表示する
  • ログインが成功したら、ログインボーナスを表示する
  • プレイヤーがゴールしたら、ゴールしたイベントを発火する。UIとシステムの両方でイベントを受け取ってそれぞれ処理を行う

などなど。使い道は無限大。みんなもEventを使ってUnityをもっと楽しもう!