像素碰撞的一种实现

开发日志系列(十二)

环境: cocos2d-x 2.2.3 , win7

碰撞相信大家都不陌生了,无论在手游的游戏按钮对触摸事件的响应 还是页游时代游戏按钮对鼠标点击事件的侦听,都需要使用碰撞来判断.例如以下一个按钮,规格为:200X200,第二张图绿色部分原本为透明

按钮的原图

ButtonHasAlpha

绿色为透明部分

ButtonNoAlpha

坐标矩阵碰撞

类似这样一个按钮 如果是单纯使用坐标碰撞的话,只要坐标落在按钮图片内部,就会判断为点中,在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中实现像素碰撞有几种思路:

  1. 获取坐标点下的像素值,判断alpha值
  2. 给按钮加一层触摸区域的遮罩,需美术配合
  3. 把按钮图片的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 大小的文件是可以接受的.生成的文件用二进制打开大概会是这个样子:

ButtonPixel

最后就是像素碰撞检测了:

//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.

目前这种方式暂时满足我项目的需求.

发表评论

邮箱地址不会被公开。 必填项已用*标注