Cocos2d-x v3 CustomEventについて

こんにちは、sekineです!
今回はCocos2d-xのCustomEventについて記事を書こうとおもいますー!
v2系だとCCNotificationCenterがありましが、v3ではこれは非推奨(deprecated)となってます。
なのでv3系では、このCustomEventの仕組みを使うと良いと思います。
EventDispatcher周りの公式のドキュメントはこちらになります。

EventDispatcher Mechanism | Cocos2d-x

今回も使用するバージョンは3.1.1となります。

記事概要

  • CustomEventでできること
  • 発行(dispatch)の仕方
  • リスナ追加(addListener)の仕方
  • リスナ削除(removeListener)の仕方
  • CustomEventの注意点と雑感

CustomEventで出来ること

EventDispatcherを介し、イベント識別子を指定して発行(dispatch)できます。
また↑の識別子でaddEventListenerすることで、イベント発行のタイミングでリスナ関数を実行できます。

それからイベントを発行する時になんらかのパラメータを付随させたい場合があると思います。
その時はSpriteと同じくsetUserData(void*)を使います。

ケースとしてはHPが減った、ゴールドが増えたなどの時にUIを更新したいケースが考えられます。
そういう場合には例えば”status_changed”などの識別子でイベントを発行してあげて
ステータスを表す何らかのデータモデルをUserDataとして持たせてあげる。
リスナではデータモデルを受け取ってUIを更新する、といった感じです。

発行の仕方

auto evt = EventCustom("test");
auto data = Value("Hello");
evt.setUserData(&data);
getEventDispatcher()->dispatchEvent(&evt);

発行までの流れはこんな感じです。

auto evt = EventCustom("test");

ここでEventCustomクラスのオブジェクトを生成します。
コンストラクタで渡している文字列はイベント識別子となります。

auto data = Value("Hello");
evt.setUserData(&data);

でValue型のオブジェクトを作成し、イベントにセットします。
setUserDataに渡すものはポインタとなりますので、この例では&が必要となります。
もともとポインタならば必要ありません。

getEventDispatcher()->dispatchEvent(&evt);

ここでイベントを発行します。

リスナ追加(addListener)の仕方

リスナ追加には2種類のやり方があります。

  • addCustomEventListener
  • listener作成&addEventListenerWithFixedPriority

addCustomEventListenerを使ったやり方

こちらは比較的簡易的な方法です。

getEventDispatcher()->addCustomEventListener("test", [this](EventCustom* evt){
    auto data = (Value*)evt->getUserData();
    log("addCustomEventListener:%s", data->asString().c_str());
});

コード例としては上記のようになります。

addCustomEventListener(イベント識別子, 実行関数);

でイベント識別子のイベントが発行された時に実行される関数を簡単に登録することができます。

[this](EventCustom* evt){
    auto data = (Value*)evt->getUserData();
    log("addCustomEventListener:%s", data->asString().c_str());
}

リスナの関数の部分だけ抜き出すとこうなります。
先述の例だと、UserDataとしてValue型のポインタを渡してますので、キャストして
asString()メソッドでstd::stringとしてデータを抜き出して
c_str()メソッドでchar*型に変換しログを出しています。

こちらの方法はコード量が少ないのですが、実行優先度を決めることができないのとリスナ単体を削除することができないです。

listener作成&addEventListenerWithFixedPriority

こちらはリスナを使用した方法です。

auto listener = EventListenerCustom::create("test", [this](EventCustom* evt){
    auto data = (Value*)evt->getUserData();
    log("addEventListener:%s", data->asString().c_str());
});
getEventDispatcher()->addEventListenerWithFixedPriority(listener, 1);

コード例は上記のようになります。

EventListenerCustom::create(イベント識別子, 実行関数);

まずこれで、リスナを作成します。

getEventDispatcher()->addEventListenerWithFixedPriority(作成したリスナ, 優先度);

でリスナと優先度を登録します。
ちなみにaddEventListenerWithFixedPriorityした時点でリファレンスカウントが1増える挙動のようです。(当たり前ですが…)

この方法のメリットは優先度が指定できるのとリスナ単体をremoveすることができることの2点です。

addEventListenerWithSceneGraphPriorityについて

調べてみたところaddEventListenerWithSceneGraphPriorityは動かないぽかったです
登録もでき、実行時エラーなども起こりませんがリスナ関数がうまく発行されませんでした。
(やり方が間違ってただけだったらすみません…)

リスナ削除(removeListener)の仕方

リスナ削除の仕方は以下の通り

  • removeCustomEventListeners
  • removeEventListener
  • removeAllEventListeners();

removeCustomEventListeners

getEventDispatcher()->removeCustomEventListeners(イベント識別子);

で対応する識別子のリスナを全て削除することが可能です。
先ほど書いたようにaddCustomEventListenerを使ったリスナ追加はこの方法もしくは
後述するremoveAllEventListenersでしかリスナを削除できません。

つまりaddCustomEventListenerを使った追加では、同じ識別子のイベントの1つを削除する、といったことが出来なくなりますので注意が必要です。

removeEventListener

getEventDispatcher()->removeEventListener(リスナ);

でリスナを指定して削除することが可能です。この方法は識別子ではなくリスナを指定するので、
同じ識別子のイベントリスナのうち1つだけ削除したい、という実装をする場合は
面倒ですがリスナを作成して、この方法でリスナを消す必要があります。

removeAllEventListeners

getEventDispatcher()->removeAllEventListeners();

すべてのリスナを消してしまう関数です。
もちろんタッチイベントなどのリスナも消えますので注意が必要です。

removeEventListenersForTypeについて

実はタイプを指定して消すremoveEventListenersForTypeというものもあるのですが
enum定義されてるEventListener::Type::CUSTOMを指定するとInvalid Typeに判定されてしまいました…。
最新版ならば、もしかしたら出来るかもしれませんが、3.1.1では、とりあえずこの方法では削除はできないようです。

CustomEventの注意点と雑感

CustomEventはとても便利な機能なのですが、1つだけ注意があります。
それはシーンでのみ使う場合は必ずリスナを削除する必要があることです。
Cocos2d-x v3 タッチイベントについて | ColcreSoftでも書いたように
通常getEventDispatcherで得られるディスパッチャはDirectorが保持しています。

つまりシーン移動などでは、リスナは削除されません。
なのでシーンAでカスタムイベントを使って、シーンBに移動、再び生成されたシーンAに戻った時に
同じ識別子のカスタムイベントが発行されると以前のシーンのポインタと関連づいたリスナが再度実行される恐れがあるのです。

もちろんラムダ式のキャプチャに通した変数などがシーンに関連が無い場合は平気ですが
そんなケースはあんまりないので十中八九メモリエラーを起こします。

一番安全なのはシーン遷移の時にremoveAllEventListenerを使用することかと思いますが、
モデル間でCustomEventを使ったやりとりをした場合、シーン移動でリスナが削除されて訳が分からなくなる恐れがあるので
もしかしたら自前のクラスを通してaddListenerして、シーン削除時にこのオブジェクトを破棄した時に
関連リスナを一括削除する、などの機構を用意した方が取り回しがしやすいかもしれません。

あと、カビDONではダイアログのコールバックなどをCustomEventを使って実装しました。
その際”dialog/ok”や”dialog/ng”のようにダイアログの結果に応じて識別子を変えたよく分からない実装をしてみたのですが
非常に使いづらかったです(笑)
と、いうのも追加したリスナは必ず削除する必要があるので、このような実装にすると識別子で判定すると識別子が増えた分だけ
必ず削除必要があります。

なので識別子を”dialog_event”のようにして、パラメータでレスポンスの種類を判断した方がスマートです。
もしくはもうダイアログにstd::function型のコールバック用メンバ変数用意して
そこに直接コールバックを指定するのでも良いかもしれません。

EventDispatcherがアプリに1個しか生成されないシングルトンのように扱われてしまう以上、
1シーンだけで有効なカスタムイベント、もしくはオブジェクト毎に有効なカスタムイベントという細かい単位で
使用する場合はちょっと使いにくいのかもな、というのが実装をした際の感想でした。

さいごに

これからもCocos2d-x関連の記事をあげていくつもりですので、
ご愛読頂ければ嬉しいです!

Cocos2d-xを使ったアプリカビDONキャラ&ポップ
開発概要をまとめた記事Cocos2d-x(v3)を使って個人ゲームを開発した話 |も、よろしくお願いいたします。

注意事項

一応、軽く実験&この記事の内容のような理解で開発はしておりますが
もしも情報が間違っていましたら、お教えて頂ければうれしいです!

コメントを残す

メールアドレスが公開されることはありません。