cocos2d-x 建立自己的层级窗口消息机制

时间:2021-10-08 05:35:40

在开发一些窗口层次比复杂的cocos2d项目时,会发现一些由于没有窗口层次而引起的bug。这些bug让一些从windows平台过来的人觉得很无奈。比如,我们有一个列表控件,我们在其中放了一些菜单,当我们滑动列表控件使菜单选项(称为A)滑出列表控件的可视范围时,按理我们是无法再点击到A的,因为它滚动出了父控件可视范围,不被用户所看到。但是cocos2d的默认情况是能点击到的,这是因为cocos2d的消息管理是优先级消息机制,只要控件登记接收消息,那么cocos2d会一如既往的发给他。所以我们刚才讲的情形在cocos2d看来,它无法根据A被遮挡而不给A发消息。究其根本,是没有一个层级窗口消息机制(当然你能通过其他的方法帮助cocos2d,但我个人觉得有点不够彻底)。


我想建立一个相对完整cocos2d的的层级窗口消息机制,且不改变cocos2d任何源码(因为改变源码的话,不知道以后升级起来是不是很麻烦)。基本思路有如下几条:

  1. 在任何一个场景中,我们会有一个最底层的Layer(我称为祖层),这个Layer将接受cocos2d的触摸消息。并且这个Layer能将触摸消息传递给其所有的子控件。
  2. 一个场景中除了祖层之外,所有其他的控件都将不接受任何触摸消息,其触摸消息的来源于父控件。
  3. 消息将从底层往上层传递。在每层中,节点根据order值从高到低排列(即order值高的表明该控件位于此层中的较上层的节点)并遍历,直到遇到消息感兴趣的节点,并停止本层遍历,进入下一层。
  4. 尽可能兼容已知和未知的cocos2d控件库。
下面看一下类的组织架构图:
cocos2d-x 建立自己的层级窗口消息机制
我们分别说一下各个类的大体作用:
  1. BYTouchDelegate并非继承于ccTouchDelegate或其他类(但消息处理函数名同ccTouchDelegate一样)。它是一个消息传播大使,所有继承于该类的类都能自动地将消息传播到所有的子控件。不继承于ccTouchDelegate主要出于设计原则中避免多重继承中的基类重复。
  2. BYLayer继承于BYTouchDelegate和CCLayer(图中未指出),负责将消息处理函数转接到BYTouchDelegate的消息处理函数,这是因为BYTouchDelegate的消息处理函数同ccTouchDelegate是一样的,而CCLayer已经继承了ccTouchDelegate,这样如果不显示的转接处理函数,C++编译器会提示有两个版本选择的错误。

此消息机制的目的有两点:

  1. 让大型Cocos2d-x网友有一个层级窗口管理机制。
  2. 让所有的Cocos2d-x元素(即各类UI)能够天生融入到这个机制中。

需要通过下面几点修改达到这个目标的:
  1. 屏蔽所有子节点的消息注册。通过实践发现,为了不动源码,必须写一个新BYCocos::addChild函数,来代替原来的addChild函数族,这样就要求项目里面调用addChild的地方更改成BYCocos::addChild。这个函数内,会将子节点的所有子节点的消息都关闭掉。
  2. Cocos2d-x源码修改一处。
    1.注释掉CCLayer的ccTouchBegan中的CCAssert语句。这个语句只是起警醒的作用,为了让Cocos2d-x适应该机制,将此句注释掉。并修改return true为return false。因为在这个机制里面,返回true表明此layer对窗口消息感兴趣,这样会阻止消息往兄弟节点传递。
重点:
  • 由于此消息机制会判断是否点击中窗口,所以窗口大小变得尤为重要。Cocos2d-x的CCControl的各类控件已经做好了这些。但自己写的新的类,需要注意正确设置窗口大小。
下面来看源码:
  1. BYTouchDelegate:
    BYTouchDelegate.h
    ////  BYTouchDelegate.h
    // TableTest
    //
    // Created by jason on 12-12-25.
    //
    //

    #ifndef __TableTest__BYTouchDelegate__
    #define __TableTest__BYTouchDelegate__

    #include "cocos2d.h"

    USING_NS_CC;

    //by message mechanism
    //node who want to receive by message must derive from BYTouchDelegate.
    //if node is layer, it need use macro( BY_MESSAGE_BRIDGE() ) to connect it to by message instead of ccTouchMessage.
    class BYTouchDelegate
    {
    public:
    BYTouchDelegate( CCNode* pOwner ) :
    m_pOwner( pOwner ),
    m_bDraging( false )
    {
    m_pItemsClaimTouch = CCArray::createWithCapacity( CHILD_MAX );
    assert( m_pItemsClaimTouch );
    m_pItemsClaimTouch->retain();

    m_pMenusClaimTouch = CCArray::createWithCapacity( CHILD_MAX );
    assert( m_pMenusClaimTouch );
    m_pMenusClaimTouch->retain();
    }
    virtual ~BYTouchDelegate()
    {
    CC_SAFE_RELEASE_NULL( m_pItemsClaimTouch );
    CC_SAFE_RELEASE_NULL( m_pMenusClaimTouch );
    }
    protected:
    // default implements are used to call script callback if exist
    virtual bool byTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
    virtual void byTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
    virtual void byTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
    virtual void byTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);

    //return value:
    //true: pParent is touched by user
    //false: pParent isn't touched by user.
    bool passMessage( CCNode* pParent, CCTouch *pTouch, CCEvent *pEvent );
    private:
    CCNode* m_pOwner;
    bool m_bDraging;
    //items claim touch message
    CCArray* m_pItemsClaimTouch;
    CCArray* m_pMenusClaimTouch;
    };
    #endif /* defined(__TableTest__BYTouchDelegate__) */

    BYTouchDelegate.cpp
    ////  BYTouchDelegate.cpp
    // TableTest
    //
    // Created by jason on 12-12-25.
    //
    //

    #include "BYTouchDelegate.h"
    #include "BYUtility.h"
    #pragma mark- input touche
    bool BYTouchDelegate::byTouchBegan(CCTouch *pTouch, CCEvent *pEvent)
    {
    //pass message to all children
    return passMessage( m_pOwner, pTouch, pEvent );
    }

    void BYTouchDelegate::byTouchMoved(CCTouch *pTouch, CCEvent *pEvent)
    {
    //special process for menu, we won't pass ccTouchMoved message to menu. Because we think menu doesn't need ccTouchMoved message in ios device where user always want to dray layer instead menu. The fllowing block for menu will only go once.
    int iNumMenus = m_pMenusClaimTouch->count();
    for( int i = 0; i < iNumMenus; ++i )
    {
    ( ( CCMenu* )m_pMenusClaimTouch->objectAtIndex( i ) )->ccTouchCancelled( pTouch, pEvent );
    }

    if( iNumMenus > 0 )
    {
    m_pMenusClaimTouch->removeAllObjects();
    }


    //pass ccTouchMoved message to un-CCMenu item
    for( int i = 0; i < m_pItemsClaimTouch->count(); ++i )
    {
    ( ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ) )->ccTouchMoved( pTouch, pEvent );
    }
    }

    void BYTouchDelegate::byTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
    {
    //for menus
    for( int i = 0; i < m_pMenusClaimTouch->count(); ++i )
    {
    ( ( CCMenu* )m_pMenusClaimTouch->objectAtIndex( i ) )->ccTouchEnded( pTouch, pEvent );
    }
    m_pMenusClaimTouch->removeAllObjects();

    //for items not menu
    for( int i = 0; i < m_pItemsClaimTouch->count(); ++i )
    {
    ( ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ) )->ccTouchEnded( pTouch, pEvent );
    }
    m_pItemsClaimTouch->removeAllObjects();
    }

    void BYTouchDelegate::byTouchCancelled(CCTouch *pTouch, CCEvent *pEvent)
    {
    //for menus
    for( int i = 0; i < m_pMenusClaimTouch->count(); ++i )
    {
    ( ( CCMenu* )m_pMenusClaimTouch->objectAtIndex( i ) )->ccTouchCancelled( pTouch, pEvent );
    }
    m_pMenusClaimTouch->removeAllObjects();

    //for items not menu
    for( int i = 0; i < m_pItemsClaimTouch->count(); ++i )
    {
    ( ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ) )->ccTouchCancelled( pTouch, pEvent );
    }
    m_pItemsClaimTouch->removeAllObjects();
    }

    bool BYTouchDelegate::passMessage( CCNode* pParent, CCTouch *pTouch, CCEvent *pEvent )
    {
    if( !pParent || !pParent->isVisible() )
    {
    return false;
    }

    //if the item'size > 1, check whether use touches it. Such as TableView.
    //some items doesn't get size. they are medium for maintaining some children. Such as CCTableViewCell.
    if( pParent->getContentSize().width * pParent->getContentSize().height > 1.0f )
    {
    CCPoint pt = pTouch->getLocation();
    CCRect rcBoundingBox( 0, 0, pParent->getContentSize().width, pParent->getContentSize().height );

    rcBoundingBox = CCRectApplyAffineTransform( rcBoundingBox, pParent->nodeToWorldTransform() );

    if( !rcBoundingBox.containsPoint( pt ) )
    {
    return false;
    }
    }

    //hande message to items
    CCArray* pChildren = pParent->getChildren();

    //no children, but user touch this item, so return true.
    if( !pChildren )
    {
    return true;
    }

    //sort all children in ascending order.
    pParent->sortAllChildren();

    CCObject* pObject = NULL;
    //traverse in descending order.
    //we only send message to the first child handling message.
    CCARRAY_FOREACH_REVERSE( pChildren, pObject )
    {
    //if the item claims the touch message
    bool bClaim = false;

    CCLayer* pLayer = NULL;
    CCNode* pNode = NULL;

    pNode = ( CCNode* )pObject;
    assert( pNode );

    //if it's layer, we should invoke its ccTouchBegan()
    //Make sure that you have commented the CCAssertion statement in CCLayer::ccTouchBegan().
    if( ( pLayer = dynamic_cast< CCLayer* >( pNode ) ) )
    {
    bClaim = pLayer->ccTouchBegan( pTouch, pEvent );
    }

    //pass message for its child which doesn't derive BYTouchDelegate since child deriving BYTouchDelegate
 //has pass message via ccTouchBegan().

    //items who doesn't derive from BYTouchDelegate can't pass touch message to its children,
    //so we have to help them to pass touch message.

    //we don't care the result of passMessage() since passMessage here is just
    //passing messages for the node. It doesn't mean the node is interested
    //in the message.
    passMessage( pNode, pTouch, pEvent );


    //if this item is interested in this message, add it to array for other messages
    if( bClaim )
    {
    //we don't use condition of &typeid( *pNode ) == &typeid( CCMenu ) since user may derive CCMenu.
    if ( dynamic_cast< CCMenu* >( pNode ) )
    {
    m_pMenusClaimTouch->addObject( pNode );
    }
    else
    {
    m_pItemsClaimTouch->addObject( pNode );
    }

    //we only send message to the first immediate child claiming message.
    break;
    }
    }

    //if there is any item who claims the message, return true.
    return m_pItemsClaimTouch->count() + m_pMenusClaimTouch->count() > 0 ? true : false;
    }
    其中比较重要的地方解释一下:
    1.首先是m_bDragging是为了判断拖动用的,主要用于菜单处理,当点击了一下之后,菜单对此消息感兴趣,我们记录菜单,并传递后续消息,然后点击完之后传来的是TouchMoved消息,那么就停止菜单消息处理。
    2.passMessage同ccTouchBegan一起来完成消息递归传递。passMessage还将帮助没有继承wmTouchDelegate的类(这些类的ccTouchBegan不具备消息传递功能)传递消息给其子控件。

  2. BYLayer.因为主要功能都有wmTouchDelegate完成了,这些类只是做了简单功能和约束的的添加。
    BYLayer.h
    ////  BYLayer.h
    // TableTest
    //
    // Created by jason on 12-12-21.
    //
    //

    #ifndef __TableTest__BYLayer__
    #define __TableTest__BYLayer__

    #include "cocos2d.h"
    #include "BYTouchDelegate.h"
    #include "BYMacros.h"

    USING_NS_CC;

    //BYLayer can be touched.
    //Every secene should have only one BYLayer to represent message center.
    //All children nodes shouldn't register touche message.
    //To achieve it, you should use BYCocos::addChild() to add child node.

    class BYLayer : public CCLayer, public BYTouchDelegate
    {
    //static
    public:
    CREATE_FUNC( BYLayer );

    //functions
    public:
    BYLayer() : BYTouchDelegate( this ){}
    bool virtual init()
    {
    setTouchEnabled( true );
    return true;
    }

    BY_MESSAGE_BRIDGE();

    protected:
    BY_TOUCH_REGISTER_TWO_MODE( 0 );
    };
    #endif /* defined(__TableTest__BYLayer__) */
    里面通过一些宏,让我编写的更快一些。但希望没能阻碍你阅读。这里面没有太多需要说的,具体的请下载示例工程查看。
PS:
  1. 工程使用时,只需包含BYCocos.h即可。
  2. 确保cocos2d-x中CCLayer.cpp内的CCLayer::ccTouchBegan()函数内的CCAssertion()语句被注释并修改return true为return false。这是新层级窗口消息机制对cocos2d-x源码的唯一修改。
  3. 这个体系中有一点需要注意,因为我们添加了窗体点击判断,所以加入这个体系的窗口应该显示地,正确地设置自己的窗体大小(这经常是一些bug的原因所在)。如果想屏蔽同级节点接受消息,重写ccTouchBegan,并且返回true。
  4. 添加子节点,必须使用BYCocos::addChild()。
  5. 另外由于使用了dynamic_cast,可能会让部分读者担心效率问题。但个人认为dynamic_cast并不是C++的垃圾,其带来的性能影响应该是很有限的。
  6. 代码上有比较多的注释,如果注释有误还请告诉我。习惯练习英文注释了,希望我的注释没太大语法错误,能够让你理解。这里就不多讲述了。
  7. 任何后续修改都会列在下面的修订记录中。
  8. 鉴于很多网友向我寻求源码工程以进行测试,为了方便,我已经将源码工程传入我的网盘空间。【http://www.xujiezhige.ys168.com】->【工具】->【chaos.zip 1,964KB】,
    如果上面链接打不开,请试着下载此csdn资源链接【Cocos2d-x层级窗口消息机制Demo】。(最新更新2013-3-25)
修订记录:
  1. 2013-2-1, 修改BYTouchDelegate.cpp。
    修订原因:按理来说,一个Layer上应该只有最上面的子节点才能相应消息。
    修订内容:将消息按节点的Zorder降序,只传递给第一个对此消息感兴趣的子节点,解决了一个Layer上,多个子节点相互重叠,都能响应消息的bug。

    修复当没有直接子节点对消息感兴趣时BYTouchDelegate不接受消息的bug。此bug会是的当间接子节点对消息感兴趣出问题。具体:修改BYTouchDelegate.cpp中passMessage(),修订的部分以//revision# began开始,以//revision# end结束标记了(#表示第几次修订。我本想用颜色标记出来,但是csdn的代码框内使用颜色格式有bug)。
  2. 2013-3-25,修改BYLayer.h
    修订原因:
    1.原来的版本重载了onEnter(),并在其中将所有子节点的消息屏蔽掉。但是onEnter是在创建的时候调用,如果创建完成后,再调用addChild(),添加的节点的消息就没有被屏蔽了,也就违背此消息机制。
    2.由于我们在addChild中屏蔽了子节点的消息,所有BYLayerModal没有存在的必要了(因为它原本设计的时候是通过自行注册最高优先级别的消息来达到屏蔽消息的),如果想让某窗口屏蔽消息,只要确保该窗口order最高,并且重写ccTouchBegan返回true。另外BYLayerDescendant的存在增加的机制的复杂度,与其带来的好处想比,还是去掉比较合理。
    3.将BYLayerAncestor合并到BYLayer去,以减少新添加的类数量,让系统精简。
    修订内容:删除原来屏蔽子节点消息注册的做法。添加BYCocos::addChild辅助函数,此函数将在内部屏蔽所有子节点的消息。