きょこみのーと

技術に関係ないほうのブログ

Cocos2d-x3.0以降のEventDispatcher制御について(その2)完

前回Cocos2d-x3.0以降のEventDispatcher制御について(その1)の続きです。

addEventListenerWithFixedPriority設定してみる。

ActorSpriteを一番優先度高くしたら順番がどうなるか。

あえてBattleSceneは、addEventListenerWithSceneGraphPriorityのままにする。

イメージ

  • 左の数値は、ローカルZIndex
  • 右の数値は、addEventListenerWithFixedPriorityのPriority
            2  | --------------------------- Layer --------- |        3
                                               |
               0   | - WeaponSprite -|         |                      2
                           |                   |
            1  | ----- ActorSprite ----- |     |                      1
                           |                   |
BattleScene | ---------------------------------------------------- |  -

変更したコード

bool BattleScene::init()
{
     // 〜 省略 〜

     //pActorSprite->getEventDispatcher()->addEventListenerWithSceneGraphPriority(actorListener, pActorSprite);
    pActorSprite->getEventDispatcher()->addEventListenerWithFixedPriority(actorListener, 1);

     // 〜 省略 〜   

     //pWeaponSprite->getEventDispatcher()->addEventListenerWithSceneGraphPriority(weaponListener, pWeaponSprite);
    pWeaponSprite->getEventDispatcher()->addEventListenerWithFixedPriority(weaponListener, 2);

     // 〜 省略 〜   

    //pLayer->getEventDispatcher()->addEventListenerWithSceneGraphPriority(layerListener, pLayer);
    pLayer->getEventDispatcher()->addEventListenerWithFixedPriority(layerListener, 3);

     // 〜 省略 〜   
}

結果

見事Priorityの数値が小さい順に呼ばれます。priority 0が最も優先度が高い。

つまり、addEventListenerWithSceneGraphPriorityが最も優先度が高いということでBattleSceneから呼ばれます。

cocos2d: BattleScene : onTouchBegan(114)
cocos2d: ActorSprite : onTouchBegan(58)
cocos2d: WeaponSprite : onTouchBegan(76)
cocos2d: LayerColor : onTouchBegan(96)
cocos2d: BattleScene : onTouchEnded(127)
cocos2d: ActorSprite : onTouchEnded(65)
cocos2d: WeaponSprite : onTouchEnded(83)

ここでMenuとMenuItemの登場

MenuItemはcreateすると自動的にaddEventListenerWithSceneGraphPriorityが設定されて作られます。

MenuItemはEventDispatcherを使っているけど、ちゃんとボタンの描画されている箇所をタッチしないとスルーする処理が入ってます。

Zindexをちょっと調整して、MenuItemをActorSpriteとかより下にします。あと重ねます。

こんな感じ

f:id:kyokomi:20140223024558p:plain

イメージ

ちょっと複雑になってきた。。。

  • 左の数値は、ローカルZIndex
  • 右の数値は、addEventListenerWithFixedPriorityのPriority
          102  | --------------------------- Layer --------- |              3
                                               |
               0   | - WeaponSprite -|         |                            2
                              |                |   
          101  | ----- ActorSprite ----- |     |                            1
                           |                   |
            1              |  | - Menu - |     |                            -
                           |        |          |
BattleScene | ---------------------------------------------------------- |  -

変更したコード

bool BattleScene::init()
{
     // 〜 省略 〜

     this->addChild(pActorSprite, 101, 1);

     // 〜 省略 〜    

     this->addChild(pLayer, 102, 2);

     // 〜 省略 〜

     // add         

    // menuItemを追加
    auto pA_Button = MenuItemImage::create("ui/a_button.png", "ui/a_button_press.png", [this](Object *pSender) {
        CCLOG("%s : %s(%d)", "A Button", "onClick", __LINE__);
    });
    auto pMenu = Menu::create(pA_Button, NULL);
    this->addChild(pMenu, 1, 1);

    return true;
}

結果

Aボタンがいるところを押しましたが、ZIndexで上にいるはずのActorやLayerを無視してMenuItemイベントだけ実行して終わり。

さらに同じpriorityが0のBattleSceneのEventも実行されない。

cocos2d: A Button : onClick(110)

Menuがそもそもモーダルレイヤーっぽい実装になってることがわかります。

つまりこの上にどんなレイヤーをaddChildしてもMenuが押されるのを止めることができないということに・・・

↓ でも、これはやりたくない・・・ダサいしフラグ管理めんどい

if (isShowModalLayer)
{
     return;
}
CCLOG("%s : %s(%d)", "A Button", "onClick", __LINE__);

逆に考えるとモーダルレイヤーの正解は、Menuのソースにあるということになります。

ということでソース追ってみたところ正解は、これっすね。

touchListener->setSwallowTouches(true);

こいつ指定すると貫通しないで、最初に実行されたEvent以降が実行されなくなります。

全部のListenerにsetSwallowTouches(true)をつけてみる

あとBattleSceneに一番下の優先度を設定する。

イメージ

  • 左の数値は、ローカルZIndex
  • 右の数値は、addEventListenerWithFixedPriorityのPriority
          102  | --------------------------- Layer --------- |              3
                                               |
               0   | - WeaponSprite -|         |                            2
                              |                |   
          101  | ----- ActorSprite ----- |     |                            1
                           |                   |
            1              |  | - Menu - |     |                            -
                           |        |          |
BattleScene | ---------------------------------------------------------- |  4

変更したコード

bool BattleScene::init()
{

     // 〜 省略 〜

     // BattleSceneも
    listener->setSwallowTouches(true);
//    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
    this->getEventDispatcher()->addEventListenerWithFixedPriority(listener, 4);

     // 〜 省略 〜

    actorListener->setSwallowTouches(true);

     // 〜 省略 〜    

    weaponListener->setSwallowTouches(true);

     // 〜 省略 〜

     layerListener->setSwallowTouches(true);

     // 〜 省略 〜

    return true;
}

結果

Aボタンのところを押した時

cocos2d: A Button : onClick(114)

addEventListenerWithSceneGraphPriorityでイベントが終わって、addEventListenerWithFixedPriorityのイベントがまったく実行されないことがわかります。

Aボタン以外の領域をおした時

cocos2d: ActorSprite : onTouchBegan(61)
cocos2d: ActorSprite : onTouchEnded(68)

Priority > Zindexで一番最初に実行されたイベント以降が実行されないことがわかります。

最後は全部addEventListenerWithSceneGraphPriorityにしてみます

イメージ

  • 左の数値は、ローカルZIndex
  • 右の数値は、addEventListenerWithFixedPriorityのPriority
          102  | --------------------------- Layer --------- |              -
                                               |
               0   | - WeaponSprite -|         |                            -
                              |                |   
          101  | ----- ActorSprite ----- |     |                            -
                           |                   |
            1              |  | - Menu - |     |                            -
                           |        |          |
BattleScene | ---------------------------------------------------------- |  -

変更したコード

bool BattleScene::init()
{

     // 〜 省略 〜

    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
//    this->getEventDispatcher()->addEventListenerWithFixedPriority(listener, 4);

     // 〜 省略 〜

    pActorSprite->getEventDispatcher()->addEventListenerWithSceneGraphPriority(actorListener, pActorSprite);
//    pActorSprite->getEventDispatcher()->addEventListenerWithFixedPriority(actorListener, 1);

     // 〜 省略 〜    

    pWeaponSprite->getEventDispatcher()->addEventListenerWithSceneGraphPriority(weaponListener, pWeaponSprite);
//    pWeaponSprite->getEventDispatcher()->addEventListenerWithFixedPriority(weaponListener, 2);

     // 〜 省略 〜

    pLayer->getEventDispatcher()->addEventListenerWithSceneGraphPriority(layerListener, pLayer);
//    pLayer->getEventDispatcher()->addEventListenerWithFixedPriority(layerListener, 3);

     // 〜 省略 〜
}

結果

Aボタンのところを押した時

cocos2d: LayerColor : onTouchBegan(101)
cocos2d: WeaponSprite : onTouchBegan(80)
cocos2d: WeaponSprite : onTouchEnded(87)

Aボタン敗北してます。しかしまさかのWeaponが呼ばれてます。 これは、WeaponだけActorにaddChiledしたためかと思われます。

つまりNodeの子要素内だけでsetSwallowTouchesは有効とわかります。孫要素は範囲外となります。

まとめ

  • addEventListenerWithFixedPriority

    • addEventListenerWithSceneGraphPriority > Priorityが小さいもの > ZIndexが大きい物の順に処理される
    • setSwallowTouches(true)を指定すると最も優先度が高いものしか実行されない
    • Nodeでのグルーピングはない。Priorityを指定したものはすべて同じグループ
  • addEventListenerWithSceneGraphPriority

    • ローカルZIndexの大きい順に処理される
    • 第2引数で指定したNodeの子要素でグルーピングされている。孫は別
    • setSwallowTouches(true)を指定するとグループ内で最も優先度が高いものしか実行されない
  • Menu

    • addEventListenerWithSceneGraphPriorityでEvent設定
    • setSwallowTouches(true)になってる
    • MenuItemの描画範囲外はキャンセルされて、同じNodeグループ内で次に優先度が高い子要素のイベントが実行される

という感じでした。

ってことで基本的にtouchイベントのモーダルレイヤー的なのをやりたいときは、 EventListenerを以下に設定しておくのが無難かと思います。

  • addEventListenerWithSceneGraphPriority
  • setSwallowTouches(true)
  • ZIndexで優先度を設定
  • モーダルレイヤーみたいなのは、一番大きいZIndexを設定してonTouchBeganでrerurn false;して制御
  • ベースのSceneの孫に当たるLayerやSpriteのtouchイベントは、addEventListenerWithSceneGraphPriorityを設定しないようにする

addEventListenerWithFixedPriorityの注意点

addEventListenerWithSceneGraphPriorityと違って、Sceneを使いまわした時などに前のSceneで設定したイベントが残ったままになってエラーになるので、メンバ変数とかでListenerを管理しておいて、デストラクタでremoveする必要があります。

addEventListenerWithFixedPriorityを使っている「CCInputDelegate.cpp」とかが参考になります。

裏ワザ?

色々試してたところ見つけたのですが、 Event::stopPropagationを呼ぶと強制的にイベントそのものをstopすることが出来るみたいです。

これ呼ぶとsetSwallowTouches(false)でもイベントが貫通しません。

かなり強制的な雰囲気なのできっと推奨されてない方法かと思いますが。。。

bool ModalLayer::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *unused_event)
{
     unused_event->stopPropagation();
     return true;
}

一応最終版のソースコード

//
//  BattleSecne.cpp
//  Cocos2dRogueLike
//
//  Created by kyokomi on 2014/02/22.
//
//

#include "BattleSecne.h"

#include "ActorSprite.h"

BattleScene::BattleScene()
{
    
}

BattleScene::~BattleScene()
{
    
}

Scene* BattleScene::scene()
{
    Scene *scene = Scene::create();
    BattleScene *layer = BattleScene::create();
    scene->addChild(layer);
    return scene;
}

bool BattleScene::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    Size winSize = Director::getInstance()->getWinSize();
    
    // TouchEvent settings
    auto listener = EventListenerTouchOneByOne::create();
    listener->setSwallowTouches(true);
    listener->onTouchBegan = CC_CALLBACK_2(BattleScene::onTouchBegan, this);
    listener->onTouchMoved = CC_CALLBACK_2(BattleScene::onTouchMoved, this);
    listener->onTouchEnded = CC_CALLBACK_2(BattleScene::onTouchEnded, this);
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
//    this->getEventDispatcher()->addEventListenerWithFixedPriority(listener, 4);
    
    // Spriteを配置
    ActorSprite::ActorDto actorDto = ActorSprite::createDto();
    actorDto.playerId = 4;
    auto pActorSprite = ActorSprite::createWithActorDto(actorDto, 1);
    pActorSprite->setPosition(Point(winSize / 2));
    this->addChild(pActorSprite, 101, 1);
    
    pActorSprite->runBottomAction();
    
    // actorにタッチイベントを設定
    auto actorListener = EventListenerTouchOneByOne::create();
    actorListener->setSwallowTouches(true);
    actorListener->onTouchBegan = [pActorSprite](Touch* touch, Event* event) -> bool {
        CCLOG("%s : %s(%d)", "ActorSprite", "onTouchBegan", __LINE__);
        return true;
    };
    actorListener->onTouchMoved = [pActorSprite](Touch* touch, Event* event) {
        CCLOG("%s : %s(%d)", "ActorSprite", "onTouchMoved", __LINE__);
    };
    actorListener->onTouchEnded = [pActorSprite](Touch* touch, Event* event) {
        CCLOG("%s : %s(%d)", "ActorSprite", "onTouchEnded", __LINE__);
    };
    pActorSprite->getEventDispatcher()->addEventListenerWithSceneGraphPriority(actorListener, pActorSprite);
//    pActorSprite->getEventDispatcher()->addEventListenerWithFixedPriority(actorListener, 1);
    
    // 武器をプレイヤーにadd
    auto pWeaponSprite = Sprite::create("icon_set/item_768.png");
    pActorSprite->addChild(pWeaponSprite);
    // 武器にタッチイベントを設定
    auto weaponListener = EventListenerTouchOneByOne::create();
    weaponListener->setSwallowTouches(true);
    weaponListener->onTouchBegan = [pWeaponSprite](Touch* touch, Event* event) -> bool {
        CCLOG("%s : %s(%d)", "WeaponSprite", "onTouchBegan", __LINE__);
        return true;
    };
    weaponListener->onTouchMoved = [pWeaponSprite](Touch* touch, Event* event) {
        CCLOG("%s : %s(%d)", "WeaponSprite", "onTouchMoved", __LINE__);
    };
    weaponListener->onTouchEnded = [pWeaponSprite](Touch* touch, Event* event) {
        CCLOG("%s : %s(%d)", "WeaponSprite", "onTouchEnded", __LINE__);
    };
    pWeaponSprite->getEventDispatcher()->addEventListenerWithSceneGraphPriority(weaponListener, pWeaponSprite);
//    pWeaponSprite->getEventDispatcher()->addEventListenerWithFixedPriority(weaponListener, 2);
    
    // 青の半透明レイヤーを追加
    auto pLayer = LayerColor::create(Color4B::BLUE);
    pLayer->setOpacity(128);
    pLayer->setContentSize(winSize);
    this->addChild(pLayer, 102, 2);
    // レイヤーにタッチイベントを設定
    auto layerListener = EventListenerTouchOneByOne::create();
    layerListener->setSwallowTouches(true);
    layerListener->onTouchBegan = [pLayer](Touch* touch, Event* event) -> bool {
        CCLOG("%s : %s(%d)", "LayerColor", "onTouchBegan", __LINE__);
        return false;
    };
    layerListener->onTouchMoved = [pLayer](Touch* touch, Event* event) {
        CCLOG("%s : %s(%d)", "LayerColor", "onTouchMoved", __LINE__);
    };
    layerListener->onTouchEnded = [pLayer](Touch* touch, Event* event) {
        CCLOG("%s : %s(%d)", "LayerColor", "onTouchEnded", __LINE__);
    };
    pLayer->getEventDispatcher()->addEventListenerWithSceneGraphPriority(layerListener, pLayer);
//    pLayer->getEventDispatcher()->addEventListenerWithFixedPriority(layerListener, 3);
    
    // menuItemを追加
    auto pA_Button = MenuItemImage::create("ui/a_button.png", "ui/a_button_press.png", [this](Object *pSender) {
        CCLOG("%s : %s(%d)", "A Button", "onClick", __LINE__);
        return;
    });
    auto pMenu = Menu::create(pA_Button, NULL);
    this->addChild(pMenu, 1, 1);
    
    return true;
}


bool BattleScene::onTouchBegan(Touch *touch, Event *unused_event)
{
    CCLOG("%s : %s(%d)", "BattleScene", __FUNCTION__, __LINE__);
    
    return true;
}

void BattleScene::onTouchMoved(Touch *touch, Event *unused_event)
{
    CCLOG("%s : %s(%d)", "BattleScene", __FUNCTION__, __LINE__);
    
}

void BattleScene::onTouchEnded(Touch *touch, Event *unused_event)
{
    CCLOG("%s : %s(%d)", "BattleScene", __FUNCTION__, __LINE__);
    
}

2014/07/27追記

この記事書いた時まだ3.0alphaだったし、結構前に3.1?か3.0Finalあたりで以下が不具合として修正されてたのを見た記憶。。。

検証しないといけないなー(T_T)

ベースのSceneの孫に当たるLayerやSpriteのtouchイベントは、addEventListenerWithSceneGraphPriorityを設定しないようにする