开发日志系列(十二)
环境: cocos2d-x 2.2.3 , win7
碰撞相信大家都不陌生了,无论在手游的游戏按钮对触摸事件的响应 还是页游时代游戏按钮对鼠标点击事件的侦听,都需要使用碰撞来判断.例如以下一个按钮,规格为:200X200,第二张图绿色部分原本为透明
按钮的原图
绿色为透明部分
坐标矩阵碰撞
类似这样一个按钮 如果是单纯使用坐标碰撞的话,只要坐标落在按钮图片内部,就会判断为点中,在cocos2d-x中坐标碰撞的一般做法是:
//pt 为鼠标 或者 触摸 击落时候的屏幕坐标 //cc 为按钮对象 继承CCNode CCPoint pos = cc->convertToNodeSpace(pt); CCRect rect = cc->boundingBox(); rect = CCRectApplyAffineTransform(rect, nodeToParentTransform()); if (rect.containsPoint( pos )) { printf("用户点击了cc"); }
这种处理方式显然并不理想,用户只要触摸或者点击了按钮的任何一个地方 都会被判定为点中.当然可以修改按钮图片,但是现在游戏的很多按钮并不是形状规则的,例如我现在项目的大部分按钮 就需要做成不规则的,动态的,有特效的(一个按钮都要做得那么炫酷…)
像素碰撞
我记得在以前做AS的时候就有像素碰撞的实现,在这里应该也可以实现类似的功能.在cocos2d-x中实现像素碰撞有几种思路:
- 获取坐标点下的像素值,判断alpha值
- 给按钮加一层触摸区域的遮罩,需美术配合
- 把按钮图片的alpha分布剥离出来,当坐标落在按钮的时候判断
总和我目前情况的考虑之后(我目前这种按钮较少,为了几个按钮改动CCSprite和更底层的引擎代码不值当),我决定实现第三种.
剥离图片alpha值
如何解析带通道的图片数据?其实cocos2d-x中的CCImage
已经帮我们做好了.我们发现CCImage
中保存的数据是这个样子的.
//CCImageCommon_cpp 500-514 if (channel == 4) { m_bHasAlpha = true; unsigned int *tmp = (unsigned int *)m_pData; for(unsigned short i = 0; i < m_nHeight; i++) { for(unsigned int j = 0; j < rowbytes; j += 4) { *tmp++ = CC_RGB_PREMULTIPLY_ALPHA( row_pointers[i][j], //R row_pointers[i][j + 1],//G row_pointers[i][j + 2],//B row_pointers[i][j + 3] );//A } } m_bPreMulti = true; } //CC_RGB_PREMULTIPLY_ALPHA 宏定义 // premultiply alpha, or the effect will wrong when want to use other pixel format in CCTexture2D, // such as RGB888, RGB5A1 #define CC_RGB_PREMULTIPLY_ALPHA(vr, vg, vb, va) \ (unsigned)(((unsigned)((unsigned char)(vr) * ((unsigned char)(va) + 1)) >> 8) | \ ((unsigned)((unsigned char)(vg) * ((unsigned char)(va) + 1) >> 8) < < 8) | \ ((unsigned)((unsigned char)(vb) * ((unsigned char)(va) + 1) >> 8) < < 16) | \ ((unsigned)(unsigned char)(va) << 24)) // on ios, we should use platform/ios/CCImage_ios.mm instead
由CCImageCommon_cpp
的代码可以看出 CCImage把图片数据按照RGBA的顺序并且每个占用8位保存起来.
知道这些我们想剥离图片的alpha值就只需要分析CCImage
的图片数据即可.首先按照8字节一个通道定义一个数据结构:
//BYTE 占8位 struct S_RGBA { BYTE vr; BYTE vg; BYTE vb; BYTE va; S_RGBA() { vr = 0; vg = 0; vb = 0; va = 0; } };
剥离实现代码:
/------------------------------------------------------------------------- bool CPNGParse::parse(const string& strCSVFileName) { //strCSVFileName为图片路径 cocos2d::CCImage *pImage = new cocos2d::CCImage(); pImage->initWithImageFile( strCSVFileName.c_str() ); S_RGBA * pData = (S_RGBA*)pImage->getData(); int nWidth = pImage->getWidth(); int nHeight = pImage->getHeight(); int nLen = pImage->getDataLen(); //新建文件 cocos2d::engine::CWareFileWrite File(false); char cFile[1024]; memset(cFile,0,1024); sprintf( cFile, "%s.%s",strCSVFileName.c_str(),"alpha"); if( !File.open( cFile) ) { return false; } char cTmp[20]; sprintf( cTmp, "%d", nWidth); File.write(cTmp,4); sprintf( cTmp, "%d", nHeight); File.write(cTmp,4); int nBit = 0; long nTmp = 0; int lTest = 0; for (int j = 0;j < nHeight;j++) { for (int i = 0; i < nWidth ; i++) { S_RGBA srgba = pData[lTest];//j * nWidth + i if (nBit < 7 ) { nTmp = (nTmp * 2) + (srgba.va > 60 ? 1 : 0); nBit++; } else { nTmp = (nTmp * 2) + (srgba.va > 60 ? 1 : 0); sprintf( cTmp, "%c",(int)nTmp); File.write(cTmp,1); nTmp = 0; nBit = 0; } lTest++; } } delete [] pData; return true; }
为了让我们生成的文件足够小,我们按照 某个像素透明则写入二进制0,不透明则写入二进制1 的原则.生成的文件发现只占原图片的1/10左右,虽然在手机上控制应用包大小非常重要,但是由于我本身这种按钮并不多.所以为每个不规则带透明通道的按钮图片素材增加 1/10 大小的文件是可以接受的.生成的文件用二进制打开大概会是这个样子:
最后就是像素碰撞检测了:
//strFileName 通道文件名字 //pos 用户触摸坐标 bool CPointHitPixel::hitTestByName(std::string& strFileName,cocos2d::CCPoint& pos) { cocos2d::engine::CWareFileRead File(false); if( !File.open( strFileName.c_str() ) ) { return false; } // 行 unsigned int nLine = pos.y; // 所在行的第几位 unsigned int ncolumn = pos.x; unsigned int nBitChat = ncolumn/8 - 1; nBitChat += ncolumn%8 > 0 ? 1 : 0; // 读取 图片寛高 char cData[4]; File.read(cData); unsigned int nWidth = atoi(cData); File.read(cData); unsigned int nHeight = atoi(cData); nLine = nHeight - nLine; // 位置超出了 if (nLine > nHeight || ncolumn > nWidth) { File.close(); return false; } long lSeek = (nLine*nWidth + ncolumn)/8 + 8; if(File.getSize() < lSeek) { File.close(); return false; } File.seek(lSeek,FILE_POS_BEGIN); BYTE cc; File.read(cc); // 判断是否透明 if (cc != 0) { File.close(); return true; } File.close(); return false; }
这个检测碰撞代码其实还不是100%精确的,但是精度已经接近到8像素了.大概就是在你触摸的像素 的周围8个像素中 如果不透明 则判断点中了按钮.如果需要精度更高 可以在取出字节后判断像素对应的位是否为1.
目前这种方式暂时满足我项目的需求.