众所周知,在cocos2d-x中,通过CCMenu(抱歉,笔者至今任然在用2.2.2的版本)创建的菜单,如果遇到数个按钮有重叠,当用户点击重叠区域(包括PNG图标中的透明部分)时,引擎默认是按照按钮的添加顺序来检索确定当前用户点击的按钮的,即在该位置第一个被添加到菜单中的的按钮为被点击按钮,即使用户点击到的是空白部分。</span>
相信很多开发人员均对此很苦恼,笔者最近正在开发一款策略类的手机游戏,游戏界面中的建筑物的位置、层级是通过配置文件的形式进行配置的,当加载进入游戏时,读取配置文件,进而绘制界面。界面中的每一个建筑实际上都是一个按钮,当笔者测试时,发现经常会产生想点击按钮A,却点击到了按钮B,原因就是按钮B比按钮A先添加到了保存按钮列表的CCArray数组中,且绘制按钮B图标的空白部分遮挡住了按钮A;而且,很多时候,点击按钮的空白部分也会触发点击事件,个中原因,相信大家都知道,就不废话了。
那么,能不能通过我们自己定义的优先级来有限出发指定按钮的点击事件呢?能不能过滤掉按钮中的透明部分呢?答案是肯定的,为此,笔者百度了很多相关案例,却都语焉不详,而且写的代码不带注释不说,还运行起来。没办法,只好自己去研究了一下CCMenu的源代码,并在其基础上派生出以下绘制菜单的类----LekoMenu。
以下的代码是LekoMenu类的声明及定义:
LekoMenu.h:
#ifndef _LEKOMENU_H_ #define _LEKOMENU_H_ /************************************************************ Copyright (C),2014-2024,Leko Tech. Co.,Ltd. FileName: Role.h Author: Hejh Version : 1.0 Date: 2015-03-06 Description: 建筑物菜单类,派生自CCMenu,用以绘制游戏中的建筑菜单。 扩展完成了通过Z轴层级觉得触摸优先级和过滤掉按钮图标中的透明部分功能。 Function List: 1.bool init() 对象属性初始化函数 2.bool ccTouchBegan(CCTouch* touch,CCEvent* event) 菜单触摸开始回调函数 3.void ccTouchMoved(CCTouch* touch,CCEvent* event) 菜单触摸滑动回调函数 4.CCMenuItem * itemForTouch(CCTouch *touch) 获取当前被点击的按钮 5.bool isTransparent(CCMenuItem *,CCPoint) 判断对应按钮被点击区域是否为透明 ============================================================ History: <author> <time> <version > <desc> ***********************************************************/ #include "cocos2d.h" USING_NS_CC; class LekoMenu : public CCMenu { public: virtual bool init(); CREATE_FUNC(LekoMenu); virtual bool ccTouchBegan(CCTouch* touch,CCEvent* event); virtual void ccTouchMoved(CCTouch* touch,CCEvent* event); private: CCMenuItem * itemForTouch(CCTouch *touch); //获取当前被点击的按钮 bool isTransparent(CCMenuItem *,CCPoint); //判断对应按钮被点击区域是否为透明 }; #endif
LekoMenu.cpp:
/************************************************************ Copyright (C),Ltd. FileName: Role.cpp Author: Hejh Version : 1.0 Date: 2015-03-06 Description: 建筑物菜单类,派生自CCMenu,用以绘制游戏中的建筑菜单。 扩展完成了通过Z轴层级觉得触摸优先级和过滤掉按钮图标中的透明部分功能。 Function List: 1.bool init() 对象属性初始化函数 2.bool ccTouchBegan(CCTouch* touch,CCPoint) 判断对应按钮被点击区域是否为透明 ============================================================ History: <author> <time> <version > <desc> ***********************************************************/ #include "LekoMenu.h" /************************************************* Function: init Description: 对象属性初始化函数,其实这个函数没用,一开始的时候习惯性写的,但是发现CCMenu类的init函数并不是Virtual的 Calls: 无 Called By: 无 Input: 无 Output: 无 Return: 初始化是否成功 Others: 无 *************************************************/ bool LekoMenu::init() { if ( !CCMenu::init() ) { return false; } return true; } /************************************************* Function: ccTouchBegan Description: 菜单触摸开始回调函数,重载该函数已经下面的ccTouchMoved函数均是完全复制CCMenu中同名函数的代码, 因为这两个函数都调用了itemForTouch(CCTouch *)来获取被点击的按钮,这里是为了能够调用自己重载的itemForTouch(CCTouch *)函数。 Calls: this->itemForTouch(touch) Called By: 菜单点击时自动调用 Input: 略 Output: 略 Return: 略 Others: 无 *************************************************/ bool LekoMenu::ccTouchBegan(CCTouch* touch,CCEvent* event) { CC_UNUSED_PARAM(event); if (m_eState != kCCMenuStateWaiting || ! m_bVisible || !this->isEnabled()) { return false; } for (CCNode *c = this->m_pParent; c != NULL; c = c->getParent()) { if (c->isVisible() == false) { return false; } } m_pSelectedItem = this->itemForTouch(touch); if (m_pSelectedItem) { m_eState = kCCMenuStateTrackingTouch; m_pSelectedItem->selected(); return true; } return false; } /************************************************* Function: ccTouchMoved Description: 菜单触摸滑动回调函数 Calls: 无 Called By: this->itemForTouch(touch) Input: 略 Output: 略 Return: 略 Others: 无 *************************************************/ void LekoMenu::ccTouchMoved(CCTouch* touch,CCEvent* event) { CC_UNUSED_PARAM(event); CCAssert(m_eState == kCCMenuStateTrackingTouch,"[Menu ccTouchMoved] -- invalid state"); CCMenuItem *currentItem = this->itemForTouch(touch); if (currentItem != m_pSelectedItem) { if (m_pSelectedItem) { m_pSelectedItem->unselected(); } m_pSelectedItem = currentItem; if (m_pSelectedItem) { m_pSelectedItem->selected(); } } } /************************************************* Function: itemForTouch Description: 从点击位置检索被点击到的按钮,以按钮的Z轴坐标为第一检测顺序,优先绑定最上层的按钮,若有按钮,则检测点击位置是否是透明的。 若点击位置非透明,则以该按钮为回调事件发起者; 若点击位置透明,则跳过该按钮,继续往下层检索,直到找到符合条件的按钮或忽略掉所有按钮为止。 Calls: 无 Called By: this->ccTouchBegan(CCTouch* touch,CCEvent* event) this->ccTouchMoved(CCTouch* touch,CCEvent* event) Input: touch: 点击事件详情 Output: 无 Return: result 被点击的按钮指针,当无符合条件的按钮时,返回NULL Others: 无 *************************************************/ CCMenuItem * LekoMenu::itemForTouch(CCTouch *touch) { CCMenuItem *result = NULL; //缓存被点击的按钮 CCArray *itemArr = CCArray::create(); //缓存被点击位置的按钮列表 CCPoint touchLocation = touch->getLocation(); //获取被点击位置坐标 //查找当前被点击位置的按钮列表 itemArr->retain(); if (m_pChildren && m_pChildren->count() > 0) { CCObject* pObject = NULL; CCARRAY_FOREACH(m_pChildren,pObject) { CCMenuItem* pChild = dynamic_cast<CCMenuItem*>(pObject); if (pChild && pChild->isVisible() && pChild->isEnabled()) { CCPoint local = pChild->convertToNodeSpace(touchLocation); CCRect r = pChild->rect(); r.origin = CCPointZero; if (r.containsPoint(local)) { itemArr->addObject(pChild); } } } } //查找符合条件的按钮 while(itemArr->count() > 0) { int pos_z = -129; //循环开始时,初始化当前最大Z轴坐标为-129 CCMenuItem *temp = NULL; //缓存当前处于最上层的按钮对象指针 for (size_t i = 0; i < itemArr->count(); ++i) { //找到当前处于最上层的按钮 //虽然建议使用CCARRAT_FOREACH遍历数组,但是个人不太喜欢,因此使用for循环 CCMenuItem* pChild = (CCMenuItem*)itemArr->objectAtIndex(i); if (pChild->getZOrder() > pos_z) { pos_z = pChild->getZOrder(); temp = pChild; } } //判断点击区域是否为透明 if ( !isTransparent(temp,touchLocation) ) { //若点击位置非透明,则该按钮即为我们当前需要点击的按钮 result = temp; break; } else { //若点击位置透明,从按钮列表中剔除该按钮 itemArr->removeObject(temp); } } return result; } /************************************************* Function: isTransparent Description: 判断按钮被点击位置是否为透明 Calls: 无 Called By: this->itemForTouch(CCTouch *) Input: btn: 需要判断的按钮对象指针 point: 用户点击的位置坐标指针 Output: 无 Return: 无 Others: 无 *************************************************/ bool LekoMenu::isTransparent(CCMenuItem *btn,CCPoint point) { CCMenuItemSprite *itemSprite = (CCMenuItemSprite*)btn; //获取按钮位置,笔者在场景中按钮锚点在正中间,需要以(0,0)为锚点计算,若在添加按钮时已经指定锚点为(0,0),则另行计算,不做赘述 CCPoint btnPoint = ccp(btn->getPositionX() - btn->getContentSize().width * 0.5f,btn->getPositionY() - btn->getContentSize().height * 0.5f); //获取点击的位置对应到按钮内部的位置 CCPoint clickPoint = ccp(point.x - btnPoint.x,point.y - btnPoint.y); //获取按钮的色彩数据信息 CCNode *sprite = itemSprite->getSelectedImage(); sprite->setAnchorPoint(CCPointZero); CCRenderTexture *renderer = CCRenderTexture::create(sprite->getContentSize().width,sprite->getContentSize().height,kCCTexture2DPixelFormat_RGBA8888); renderer->begin(); bool visible = sprite->isVisible(); if (visible) { sprite->visit(); } else { sprite->setVisible(true); sprite->visit(); sprite->setVisible(false); } GLubyte pixelColors[4]; #if ( CC_TARGET_PLATFORM != CC_PLATFORM_WIN32) glReadPixels(clickPoint.x,clickPoint.y,1,GL_RGBA,GL_UNSIGNED_BYTE,&pixelColors[0]); #else glReadPixels(clickPoint.x,GL_ALPHA,&pixelColors[0]); #endif //获取透明度 int alpha = pixelColors[0]; renderer->end(); if (alpha <= 0) { //透明 return true; } else { //非透明 return false; } }