【cocos2D-x学习】15.cocos2d-x 2.0 版本的 ShatteredSprite

【目标】:将 ShatteredSprite 移植到 cocos2d-x 2.0.4 版本


【参考】:

superraccoon大神的版本

OpenGL ES for Android研究总结


一、引子

        一直想要做一个爆炸的特效,尤其是看到了superraccoon的那个效果。理想中的效果包括两个部分:粒子特效构成的爆炸火焰,和网格特效构成的碎片效果。其中粒子特效我在 http://blog.csdn.net/ronintao/article/details/9899479 里面讨论过了,自信是能做出来的(实际上在那个夭折的飞机小游戏里面做了一个)。不过碎片效果还没有实现。

       在superraccoon那个网址里面,提供了一个老版本的 ShatteredSprite,可以实现一个爆炸的碎片效果,不过那个版本在我的 cocos2dx 2.0版本上并不能直接正常运行。【现在分析下来主要差异在于 opengl es 2.0的差异,以及 ccpoint 数据结构不能正常工作导致。】 

       一气之下干脆自己花了两个晚上根据原理重新写了一个。这里讨论一下他的实现原理。


二、原理

      其实 ShatteredSprite的原理很简单。首先将整个纹理分割为大量三角形,然后当开始爆炸时,每个三角形以不同的速度和旋转角度飞出去,就形成了爆炸的效果。

      明确了原理,下一步明确我们应该如何实现:首先要将纹理分割为大量三角形,然后考虑如何将这些三角形进行旋转和位移。


三、网格化并通过 drawArray显示

      既然是要分割成三角形,那么最终是要通过glDrawArray来绘制大量三角形,拼接成一个完整纹理。老实说,由于没用过ES2.0,开始的时候完全没有概念。这个时候 cocos的源码是我们最好的老师,这里可以参考的是 ccsprite的 draw函数,由于比较简单,这里就不列出了,简而言之就是调用glVertexAttribPointer设置顶点,然后调用glDrawArrays绘制。

     拿到一个纹理之后,我们要进行分割。我这里写死为分割为横8块,竖8块,然后每个方格又切成两个三角形,总共128块。

【cocos2D-x学习】15.cocos2d-x 2.0 版本的 ShatteredSprite_第1张图片

        其坐标关系如上图所示,一个像上面位置的方块,我们称其位置为(x, y),那么左下角就是(0, 0),如果将二维坐标数组转为一维,那么按照先算第一竖列,再算第二竖列的顺序来算,那么就是第 x*Y + y个方块,其中Y是一列的高度。

        那么再进行三角形的切割,就可以知道两个三角形分别是第 2(x*Y + y) 和 第 2(x* Y +y) + 1个三角形,这两个三角形的坐标(注意逆时针)分别是:

        第2(x*Y + y) 个三角形:{  左下角(x, y+1)、右下角(x+1, y)、左上角(x, y+1)  }

        第2(x* Y +y) + 1个三角形:{ 右上角(x+1, y+1)、 左上角(x, y+1)、右下角(x+1, y)}

        这样知道了整个图片的高和宽,顶点的分布就可以确定了,纹理坐标也对应出来了,需要注意的是,纹理坐标的UV坐标系范围是从0到1,然后是MM_TEXT坐标系,正方向是右下。

        至于颜色数组该如何分配,其实我没有完全搞清。目前已知的情况是如果不画,最终出不来图像,但是只需要制定为默认的全255好像就可以了。

        对应的代码如下,首先在初始化的时候初始化三个数组:

void RoninShatterSprite::shatterSprite(CCSprite *sprite, float speedVar, float rotVar) {
    initWithTexture(sprite->getTexture());

    //init vertices array
    const int X_PIECE = 8;
    const int Y_PIECE = 8;

    float xVerLength = WIDTH(sprite) / X_PIECE;
    float yVerLength = HEIGHT(sprite) / Y_PIECE;

    //生成一个point数组,省掉后面计算,由于边界的存在,比分出的份要多一
    CCPoint ptArray[ X_PIECE + 1 ][ Y_PIECE + 1 ];
    CCPoint texArray[ X_PIECE + 1 ][ Y_PIECE + 1];
    for ( int x = 0; x <= X_PIECE; x ++ ) {
        for ( int y = 0; y <= Y_PIECE; y ++ ) {
            ptArray[x][y] = ccp( xVerLength * x, yVerLength * y );
            //注意TEXTURE是 MM_TEXT 坐标系,可以参考 http://blog.csdn.net/taibushuang/article/details/6435390
            texArray[x][y] =  ccp( ptArray[x][y].x / WIDTH(sprite), 1.0f - ptArray[x][y].y/HEIGHT(sprite) );
        }
    }

    for ( int x = 0; x < X_PIECE; x ++ ) {
        for ( int y = 0; y < Y_PIECE; y ++ ) {
            mVertices[ (x*Y_PIECE + y) * 2 ] = triVer(  ptArray[x][y],  ptArray[x+1][y],  ptArray[x][y+1] );
            mTexcords[ (x*Y_PIECE + y) * 2 ] = triTex( texArray[x][y], texArray[x+1][y], texArray[x][y+1] );

            mVertices[ (x*Y_PIECE + y) * 2 + 1 ] = triVer(  ptArray[x+1][y+1],  ptArray[x][y+1],  ptArray[x+1][y] );
            mTexcords[ (x*Y_PIECE + y) * 2 + 1 ] = triTex( texArray[x+1][y+1], texArray[x][y+1], texArray[x+1][y] );
        }
    }

    //init color array, fill with 255
    ccColor4B tmpColor = { 255, 255, 255, 255 };
    for ( int i = 0; i < MAX_VERTEX; i ++ )
        mClrArray[i] = triClr(tmpColor, tmpColor, tmpColor);
}
        然后再绘制的时候,绘制这三个数组:

void RoninShatterSprite::draw() {
    //CCSprite::draw();
    CC_NODE_DRAW_SETUP();
    ccGLBlendFunc(m_sBlendFunc.src, m_sBlendFunc.dst);
    ccGLBindTexture2D(getTexture()->getName()); //init时set进去的

    ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );

    glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, mVertices);
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, mTexcords);
    glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, mClrArray);

    glDrawArrays(GL_TRIANGLES, 0, 3 * MAX_VERTEX);
}
         就可以看到图像了。

         这里需要特别说明的就是数据结构,原来的ShatteredSprite是用CCPoint来做的,确实由于cocos提供了很多辅助宏和方法,用起来很方便,但是在我的机器上跑,始终表现不正常,说明可移植性并不好。最好的方法还是老老实实的用 ccVertex2F 来构成,这里也贴一下我的数据结构:

//数据结构
struct RoninTriVer {
    cocos2d::ccVertex2F vertic1;
    cocos2d::ccVertex2F vertic2;
    cocos2d::ccVertex2F vertic3;
};

struct RoninTriTex {
    cocos2d::ccTex2F tex1;
    cocos2d::ccTex2F tex2;
    cocos2d::ccTex2F tex3;
};

struct RoninTriClr {
    cocos2d::ccColor4B color1;
    cocos2d::ccColor4B color2;
    cocos2d::ccColor4B color3;
};

四、让碎片飞

        完成上面的一步,其实就已经完成了50%。下面我们要实现碎片乱飞的效果。从老版本的ShatteredSprite很容易看出来是怎么做的:开始的时候为每一个三角形生成一个速度和旋转量,然后在每次update的时候根据这两个数值,来进行位移和旋转。

       由于非常简单,所以我这里直接贴代码,需要说明的是,原来的 randf 方法在我这里运行貌似也有问题,我没有具体调试,直接替换成了cocos的CCRANDOM_MINUS1_1:

       首先在初始化的时候,为每个三角形碎片生成一个速度分量和旋转分量:

void RoninShatterSprite::shatterSprite(CCSprite *sprite, float speedVar, float rotVar) {
    ..............//前面不变

    for ( int x = 0; x < X_PIECE; x ++ ) {
        for ( int y = 0; y < Y_PIECE; y ++ ) {
            mVertices[ (x*Y_PIECE + y) * 2 ] = triVer(  ptArray[x][y],  ptArray[x+1][y],  ptArray[x][y+1] );
            mTexcords[ (x*Y_PIECE + y) * 2 ] = triTex( texArray[x][y], texArray[x+1][y], texArray[x][y+1] );
            //为第 (x*Y_PIECE + y) * 2 个三角形生成速度和旋转分量
            velocityArray[ (x*Y_PIECE + y) * 2 ] = ccp( CCRANDOM_MINUS1_1() * speedVar, CCRANDOM_MINUS1_1() * speedVar );
            rotationArray[ (x*Y_PIECE + y) * 2 ] = CCRANDOM_MINUS1_1() * rotVar;

            mVertices[ (x*Y_PIECE + y) * 2 + 1 ] = triVer(  ptArray[x+1][y+1],  ptArray[x][y+1],  ptArray[x+1][y] );
            mTexcords[ (x*Y_PIECE + y) * 2 + 1 ] = triTex( texArray[x+1][y+1], texArray[x][y+1], texArray[x+1][y] );
            //为第 (x*Y_PIECE + y) * 2 + 1个三角形生成速度和旋转分量
            velocityArray[ (x*Y_PIECE + y) * 2 + 1 ] = ccp( CCRANDOM_MINUS1_1() * speedVar, CCRANDOM_MINUS1_1() * speedVar );
            rotationArray[ (x*Y_PIECE + y) * 2 ] = CCRANDOM_MINUS1_1() * rotVar;
        }
    }

    .......//后面也不变
}
       然后再update的时候,根据速度移动三角形的三个角,然后以三角形的重心为中心进行旋转,注意只要移动顶点数据就可以,纹理是不用变化的,毕竟这个三角形中的内容没有发生变化:

void RoninShatterSprite::update(float dt) {
    for ( int i = 0; i < MAX_VERTEX; i ++ ) {
        mVertices[i].vertic1 = vertex2( mVertices[i].vertic1.x + velocityArray[i].x, mVertices[i].vertic1.y + velocityArray[i].y );
        mVertices[i].vertic2 = vertex2( mVertices[i].vertic2.x + velocityArray[i].x, mVertices[i].vertic2.y + velocityArray[i].y );
        mVertices[i].vertic3 = vertex2( mVertices[i].vertic3.x + velocityArray[i].x, mVertices[i].vertic3.y + velocityArray[i].y );

        CCPoint center = ccp( (mVertices[i].vertic1.x + mVertices[i].vertic2.x + mVertices[i].vertic3.x)/3,
            (mVertices[i].vertic1.y + mVertices[i].vertic2.y + mVertices[i].vertic3.y)/3);

        CCPoint rotatedV1 = ccpRotateByAngle( ccp(mVertices[i].vertic1.x, mVertices[i].vertic1.y), center, rotationArray[i] );
        CCPoint rotatedV2 = ccpRotateByAngle( ccp(mVertices[i].vertic2.x, mVertices[i].vertic2.y), center, rotationArray[i] );
        CCPoint rotatedV3 = ccpRotateByAngle( ccp(mVertices[i].vertic3.x, mVertices[i].vertic3.y), center, rotationArray[i] );

        mVertices[i].vertic1 = vertex2( rotatedV1.x, rotatedV1.y );
        mVertices[i].vertic2 = vertex2( rotatedV2.x, rotatedV2.y );
        mVertices[i].vertic3 = vertex2( rotatedV3.x, rotatedV3.y );
    }
}
         好了,现在你的shatterSprite已经可以碎片乱飞了~~~~


五、随机的碎片大小

       注意到我们现在每个碎片的大小都是一样的,可以再把这个碎片的大小随机化一点,这个是直接抄老的 shatterSprite 的【其实连思想都是抄的,我只是研究了一下而已】。

       在初始化原来生成ptArray的地方稍微修改一下即可:

    CCPoint ptArray[ X_PIECE + 1 ][ Y_PIECE + 1 ];
    CCPoint texArray[ X_PIECE + 1 ][ Y_PIECE + 1];
    for ( int x = 0; x <= X_PIECE; x ++ ) {
        for ( int y = 0; y <= Y_PIECE; y ++ ) {
            ptArray[x][y] = ccp( xVerLength * x, yVerLength * y );
            if ( x > 0 && x < X_PIECE && y > 0 && y < Y_PIECE )
                ptArray[x][y] = ccpAdd( ptArray[x][y], ccp(CCRANDOM_MINUS1_1() * xVerLength * 0.45, CCRANDOM_MINUS1_1() * yVerLength * 0.45 ) );


            //注意TEXTURE是 MM_TEXT 坐标系,可以参考 http://blog.csdn.net/taibushuang/article/details/6435390
            texArray[x][y] =  ccp( ptArray[x][y].x / WIDTH(sprite), 1.0f - ptArray[x][y].y/HEIGHT(sprite) ); //ccp( xTexLength * x, yTexLength * (Y_PIECE - y) );
        }
    }

六、炸过来、炸回去

        为了给同学演示而加的一个额外的小功能,可以再把炸开的碎片收回来。如果你已经理解了前面的流程,这里想必可以立刻明白:只要把原来做的位移和旋转操作再反过来做一遍即可。

        我这里用一个 isBoom来记录状态,确定到底是在炸开的状态还是收缩的状态,然后用一个 boomFrameCount 记录炸开动作的帧数,当需要炸回来的时候,再反向操作boomFrameCount帧就可以回到原始状态。

        当然,由于浮点操作的误差,多次这样炸过来炸过去,还是会出现一定误差, 表现是图像上出现三角形间的小缝隙。解决的方法是把最原始的位置记录一下,在炸回去的最后一帧,做一个彻底恢复的动作。

       具体代码如下:

void RoninShatterSprite::update(float dt) {
    //如果已经炸回原形
    if ( !isBoom && boomFrameCount <= 0 ) {
        restore();
        unscheduleUpdate();
        return;
    }

    int boomFlag = isBoom ? 1 : -1;

    boomFrameCount += boomFlag;

    for ( int i = 0; i < MAX_VERTEX; i ++ ) {
        mVertices[i].vertic1 = vertex2( mVertices[i].vertic1.x + boomFlag * velocityArray[i].x, mVertices[i].vertic1.y + boomFlag * velocityArray[i].y );
        mVertices[i].vertic2 = vertex2( mVertices[i].vertic2.x + boomFlag * velocityArray[i].x, mVertices[i].vertic2.y + boomFlag * velocityArray[i].y );
        mVertices[i].vertic3 = vertex2( mVertices[i].vertic3.x + boomFlag * velocityArray[i].x, mVertices[i].vertic3.y + boomFlag * velocityArray[i].y );

        CCPoint center = ccp( (mVertices[i].vertic1.x + mVertices[i].vertic2.x + mVertices[i].vertic3.x)/3,
            (mVertices[i].vertic1.y + mVertices[i].vertic2.y + mVertices[i].vertic3.y)/3);

        CCPoint rotatedV1 = ccpRotateByAngle( ccp(mVertices[i].vertic1.x, mVertices[i].vertic1.y), center, boomFlag * rotationArray[i] );
        CCPoint rotatedV2 = ccpRotateByAngle( ccp(mVertices[i].vertic2.x, mVertices[i].vertic2.y), center, boomFlag * rotationArray[i] );
        CCPoint rotatedV3 = ccpRotateByAngle( ccp(mVertices[i].vertic3.x, mVertices[i].vertic3.y), center, boomFlag * rotationArray[i] );

        mVertices[i].vertic1 = vertex2( rotatedV1.x, rotatedV1.y );
        mVertices[i].vertic2 = vertex2( rotatedV2.x, rotatedV2.y );
        mVertices[i].vertic3 = vertex2( rotatedV3.x, rotatedV3.y );
    }
}
        恢复的动作,很简单:

void RoninShatterSprite::restore() {
    boomFrameCount = 0;
    for ( int i = 0; i < MAX_VERTEX; i ++ )
        mVertices[i] = mOriginVertices[i];
}

七、源码

       简易版cocos2dx 2.0版本 ShatterSprite,可以实现sprite的炸成碎片的效果(以及恢复到原体的效果)。自带一个简单的DEMO(当然你要先配环境才能运行),按下屏幕开始爆炸,手指离开屏幕开始恢复。

      地址:http://download.csdn.net/detail/ronintao/6548381


你可能感兴趣的:(【cocos2D-x学习】15.cocos2d-x 2.0 版本的 ShatteredSprite)