【cocos2D-x学习】10.粒子特效(重力模式)的入门级学习——Read The F**king Code

【目标】:通过阅读源码,来学习粒子特效的重力模式中的N多参数的意义



一、准备工作——各种工具

1、源码

    源码当然是必须的。这里参阅的代码版本是 cocos2d-2.0-x-2.0.4,新版本可能会有一些出入。


2、plist编辑工具

    由于粒子特效主要是各种参数的调配,所以可以使用 plist 来通过 initWithFile 加载。plist文件可以直接用 NotePad ++ 来查看,不过没试过编辑会不会有问题,这里推荐用 plist Editor for Windows。


3、可视化特效编辑工具

    在IOS平台可以使用cocos builder,不过屌丝windows也有一款简单的类似工具 “Particle Editor”。 这里推荐下载 ParticleEditorV1.1 版本。如果一定要使用最新的 ParticleEditor V2.0.7z,则需要安装 VC++ 2012 Redistributable x64 或 x86 版本。以我目前使用的情况来看,两个版本之间没有重要的区别。



二、粒子系统的模式

    最早cocos中的粒子模式仅有 重力模式 kCCParticleModeGravity (当然,那个时候也没有模式这个概念),后来加入了半径模式 kCCParticleModeRadius。两者都有各自专属的一部分属性。我这里只研究重力模式(暂时还用不需要粒子进行旋转)。



三、粒子系统的初始化

    在实际操作的时候,是先观察粒子系统的运作方式,了解各个属性的作用,然后再反过来看初始化的。不过这里还是按照时序来写。

    另外,粒子系统的初始化可以通过两种方式:一是通过plist指定属性列表,二是通过代码逐项指定属性。由于后者比较灵活,所以这里只分析前一种,只有在两种有显著区别的时候才会加以说明。

    这里直接贴代码,略有修改以方便说明,相关的说明以注释的方式出现。

bool CCParticleSystem::initWithDictionary(CCDictionary *dictionary)
{
    ....... //声明几个变量,不关键,以下非关键代码皆不列出
    
    //这个初始化如果失败会直接退出
    //这个函数中主要功能
    //1. 保存最大粒子数目 m_uTotalParticles("maxParticles")
    //2. 为几个主要功能参数设置默认值
    int maxParticles = dictionary->valueForKey("maxParticles")->intValue();
    if ( !this->initWithTotalParticles(maxParticles) )
        return false;
        
        
    //解析以下通用属性:
    //1. 发射角度 m_fAngle("angle") 和变化范围 m_fAngleVar("angleVariance")
    //2. 运行时间 m_fDuration("duration")
    //3. 纹理混合 blend function。两个参数 src 和 dst,对应 glBlendFunc 中的两个参数 sfactor, dfactor
    //4. 起止的颜色,分别指定 RGBA 四个参数
    //5. 发射位置:position和m_tPosVar。注意指定 (sourcePositionx, sourcePositiony) 和 setPosition效果是一样的,当然 setPosition一般在后面,所以是实际生效者
    //6. 自旋起止:m_fStartSpin 和 m_fEndSpin
    //7. 最重要的属性:粒子系统模式解析 m_nEmitterMode("emitterType")
    xxxx = dictionary->valueForKey("???????")->xxxValue();
    
    //如果是重力模式
    if( m_nEmitterMode == kCCParticleModeGravity ) 
    {
        //解析以下重力模式专用属性
        //1. 重力 gravity
        //2. 初速度 speed
        //3. 径向加速度 radialAccel
        //4. 切向加速度 tangentialAccel
        
        modeA.YYYY = dictionary->valueForKey("??????")->floatValue();
    } else {
        .......
    }
    
    //解析粒子的生命长度
    m_fLife = dictionary->valueForKey("particleLifespan")->floatValue();
    m_fLifeVar = dictionary->valueForKey("particleLifespanVariance")->floatValue();

    //解析发射速率 = 粒子数/秒。 注意这里和 setEmissionRate 不同,是用  同时存在的数量/粒子的平均生命 计算出来的
    m_fEmissionRate = m_uTotalParticles / m_fLife;
    
    
    //加载纹理,一般是走 if (!m_pBatchNode) 分支,有两种方法
    //方法一:如果设置了 textureFileName,即指定了纹理源文件,则加载该文件
    const char* textureName = dictionary->valueForKey("textureFileName")->getCString();
    std::string fullpath = CCFileUtils::sharedFileUtils()->fullPathFromRelativeFile(textureName, m_sPlistFile.c_str());
    tex = CCTextureCache::sharedTextureCache()->addImage(fullpath.c_str());

    //方法二:如果没有指定 textureFileName,可以通过 textureImageData 把图片数据写进来
    if (!tex)
    {
        const char *textureData = dictionary->valueForKey("textureImageData")->getCString();
        int decodeLen = base64Decode((unsigned char*)textureData, (unsigned int)dataLen, &buffer);
        ......
        image->initWithImageData(deflated, deflatedLen);
        setTexture(CCTextureCache::sharedTextureCache()->addUIImage(image, fullpath.c_str()));
        ......
    }

    return true;
}
     可以看到整个initWithDictionary函数就是在解析各个属性的值。 需要强调的有以下几点

    1、首先要说明的是dictionary的解析模式,其模式类似如下:

m_fAngle = dictionary->valueForKey("angle")->floatValue();

    当指定的key值在plist中不存在时,会返回一个空字符串"",该字符串转 int 或者 float 值的时候,会返回默认值0。也就是说,这里解析的默认值是 0

    2、解析文件的时候,emission Rate 是算出来的,而不是指定的,这点和代码中指定有比较大的不一样。

    3、纹理有两种加载方式,可以通过指定纹理文件,也可以直接写入 textureImageData,前者优先级更高

    4、关于纹理混合 blend function 。其实我没怎么用过,没有发言权,只知道可以实现透明等效果。有兴趣的可以参考 NEHE 的中文教程 和 百度百科。另外还找到一篇不错的博客。

    5、注意不是所有属性通过解析文件都可以得到,其他的属性例如 m_ePositionType 都没有指定,使用的都是默认值。



四、粒子系统的运作

    只需要关注一个函数:CCParticleSystem::update。和上面的无脑解析不太一样,这里可以明显分为几个部分,下面分段来看。


1、添加粒子

    if (m_bIsActive && m_fEmissionRate) {
        float rate = 1.0f / m_fEmissionRate;

        if (m_uParticleCount < m_uTotalParticles)
            m_fEmitCounter += dt;
        
        while (m_uParticleCount < m_uTotalParticles && m_fEmitCounter > rate) {
            this->addParticle();
            m_fEmitCounter -= rate;
        }

        m_fElapsed += dt;
        if (m_fDuration != -1 && m_fDuration < m_fElapsed)
            this->stopSystem();
    }
    这段代码是确定系统的状态:是否需要发射新的粒子,还是已经到时间该关闭系统了?其中主要涉及到四个变量

    1)m_fElapsed:就是该系统运行的时间,如果超过了 duration 就结束系统,如果 duration 是-1就永远运行下去。

    2)m_uParticleCount当前屏幕上有的粒子数。如果屏幕上已经有足够多的粒子,就不需要再发射了。

    3)rate发射粒子的时间间隔。它是 m_fEmissionRate 的倒数。m_fEmissionRate = x 的意思是,每秒发射 x 个粒子。那么 rate = 1/x 的意思就是,每隔 1/x 秒发射一个粒子。这样一秒正好发射x个粒子。需要实时计算,大约是考虑到 m_fEmissionRate 是可能实时变化的。

    4)m_fEmitCounter距离上个粒子发射的间隔。既然是每隔 1/x 秒发射一个粒子,自然要知道距离上一次发射间隔的时间了。


2、初始化粒子

    添加粒子 addParticle,就是创建一个粒子initParticle,然后将屏幕上的粒子数 m_uParticleCount 加一。这里主要关注 initParticle,涉及到好几个属性的应用方法。

void CCParticleSystem::initParticle(tCCParticle* particle)
{
    //第一步:确定这个粒子的生命长度 timeToLive
    particle->timeToLive = m_fLife + m_fLifeVar * CCRANDOM_MINUS1_1();


    //第二步:确定粒子的颜色 color ,大小 size,旋转 rotation。
    //这两个属性的确定有类似之处,都是已经给定了初始值和终止值。
    //解析方式都是将当前值设置为初始值,然后根据生命长度计算出变化率,以颜色的R分量为例:
    start.r = clampf(m_tStartColor.r + m_tStartColorVar.r * CCRANDOM_MINUS1_1(), 0, 1);
    end.r = clampf(m_tEndColor.r + m_tEndColorVar.r * CCRANDOM_MINUS1_1(), 0, 1);
    particle->deltaColor.r = (end.r - start.r) / particle->timeToLive;

    //大小唯一特殊的是,可以指定大小不变, kCCParticleStartSizeEqualToEndSize = -1
    if( m_fEndSize == kCCParticleStartSizeEqualToEndSize )
        particle->deltaSize = 0;

        
    //第三步:位置
    //pos 是点的实际位置,初始化在发射点 m_tSourcePosition
    particle->pos.x = m_tSourcePosition.x + m_tPosVar.x * CCRANDOM_MINUS1_1();
    particle->pos.y = m_tSourcePosition.y + m_tPosVar.y * CCRANDOM_MINUS1_1();

    //startPos 是点的参考位置,用来实现 m_ePositionType 的三种模式:
    //1. kCCPositionTypeFree(默认): 已经产生的粒子的运动轨迹固定在世界坐标系上,如果世界移动了,它也移动,不会随着发射器而移动
    //2. kCCPositionTypeRelative:粒子的运动轨迹固定在父节点上。完全跟随父节点移动。
    //3. kCCPositionTypeGrouped:粒子运动轨迹固定在以发射器为原点的坐标系上。完全跟随发射器运动
    if( m_ePositionType == kCCPositionTypeFree )
    {
        //kCCPositionTypeFree 保存开始时,原点在世界中的位置
        particle->startPos = this->convertToWorldSpace(CCPointZero);
    }
    else if ( m_ePositionType == kCCPositionTypeRelative )
    {
        //kCCPositionTypeRelative 保存开始时,原点在父节点中的位置(就是position)
        particle->startPos = m_tPosition;
    }

    
    //第四步:速度
    // Mode Gravity: A
    if (m_nEmitterMode == kCCParticleModeGravity) 
    {
        //v 是单位方向向量,s 是速度的模,两者一乘,得到速度向量 particle->modeA.dir
        CCPoint v(cosf( a ), sinf( a ));
        float s = modeA.speed + modeA.speedVar * CCRANDOM_MINUS1_1();
        particle->modeA.dir = ccpMult( v, s );

        //保存径向加速度 radialAccel,和切向加速度 tangentialAccel
        particle->modeA.radialAccel = modeA.radialAccel + modeA.radialAccelVar * CCRANDOM_MINUS1_1();
        particle->modeA.tangentialAccel = modeA.tangentialAccel + modeA.tangentialAccelVar * CCRANDOM_MINUS1_1();

    }
}
    这里有以下几点值得说明:

    1)颜色、大小、旋转的过渡:

    从代码中可以看到,这些属性的变化率,都是用结束的值减去起始的值,然后除以生命长度得到的,这就意味着:在这个体系下,这些属性只能够进行线性的过渡变化


    2)随机值:

    这里显示了随机值是如何作用的:

float startS = m_fStartSize + m_fStartSizeVar * CCRANDOM_MINUS1_1();
    CCRANDOM_MINUS1_1返回的是一个[-1, 1]的随机值。所以是以StartSize为平均值,以Var为半径进行上下浮动


    3)位置类型:

    注释当中也提到了,不过这个概念很重要,这里再拉出来讨论一下:

    位置类型有三种

    [1]  kCCPositionTypeFree:粒子的运动轨迹固定在世界坐标系上,如果世界移动了,它也移动,不会随着发射器而移动
    [2]  kCCPositionTypeRelative:粒子的运动轨迹固定在父节点上。完全跟随父节点移动。
    [3]  kCCPositionTypeGrouped:粒子运动轨迹固定在以发射器为原点的坐标系上。完全跟随发射器运动

    如果发射器的父节点就是世界,那么类型1和类型2的效果完全相同。这就是DEMO程序 TestCpp中的粒子效果看不出FREE MODE和 RELATIVE MODE之间区别的原因。

    至于他们在代码上作用的方式,则是这样的:

    首先在init的时候保存下参考点

    if( m_ePositionType == kCCPositionTypeFree )
    {
        particle->startPos = this->convertToWorldSpace(CCPointZero);
    }
    else if ( m_ePositionType == kCCPositionTypeRelative )
    {
        particle->startPos = m_tPosition;
    }

    如上,当是 kCCPositionTypeFree 的时候,startPos 就是emitter原点在世界坐标系下的坐标值 POSw,如果是 kCCPositionTypeRelative 则是emitter原点在父坐标系下的坐标值 POSp(这里要理解 position 就是原点在父节点坐标系下的坐标值。),如果是 kCCPositionTypeGrouped 则不需要保存 startPos。

    然后在 update 更新位置的时候:

    //currentPosition 是现在的参考点
    CCPoint currentPosition = CCPointZero;
    if (m_ePositionType == kCCPositionTypeFree)
        currentPosition = this->convertToWorldSpace(CCPointZero);
    else if (m_ePositionType == kCCPositionTypeRelative)
        currentPosition = m_tPosition;

    CCPoint    newPos;

    if (m_ePositionType == kCCPositionTypeFree || m_ePositionType == kCCPositionTypeRelative) {
        //diff是参考点的偏移量
        CCPoint diff = ccpSub( currentPosition, p->startPos );
        newPos = ccpSub(p->pos, diff);
    } else {
        newPos = p->pos;
    }
    可以看到,在update中,计算了参考点的偏移量,并且在粒子的新位置中减掉了这个偏移量,也就是说,让粒子运动的轨迹,跟着参考点所参照的坐标系一起运动。而这个参照坐标系,正是前面所提到的三种。


    4)速度向量:

    属性中设置的速度数值和角度数值,体现在了速度向量中。这个速度向量指向angle角度,考虑到右手坐标系的特性,在不发生旋转的前提下,向正右方向发射为0°,向正上方向发射是90°。当然,CCParticleSystem 作为 CCNode的一个子类,当然也是可以进行旋转等操作的,至于这些操作,可以参考 《5.坐标系其三——再看Cocos中的坐标系》。

    后面在update中更新粒子位置时,就不在单独使用 speed 了,而是统一使用 dir 向量


3、更新已有粒子的位置

    update中其他属性,如大小、颜色、旋转什么的更新都很简单,如上面所述,按照线性关系变化即可。这里主要关注位置的更新。

    由于前面已经说过位置类型的意义,这里舍弃参考坐标系移动的影响,统一按照 kCCPositionTypeGrouped 模式来分析。实际运行时,将两者分析叠加即可。

CCPoint tmp, radial, tangential;

//第一步,求加速度,注意:如果粒子还在原点,就没有径向加速度
//这里我对原来的代码做了一点修改,增加了两个单位向量normalRadial 和 normalTangen,提高可读性。功能是一样的。
//径向加速度 radial,模是 radialAccel,方向是 p->pos 的标准化向量
normalRadial = (p->pos.x || p->pos.y) ? ccpNormalize(p->pos) : CCPointZero;
radial = ccpMult(normalRadial, p->modeA.radialAccel);

//切向加速度 tangential,模是 radialAccel,方向向量利用 radial 计算得到:(x, y) ⊥ (-y, x)
normalTangen = ccp(-normalRadial.y, normalRadial.x);
tangential = ccpMult(normalTangen, p->modeA.tangentialAccel);

//第二步,求新的速度。tmp是总加速度向量,是三部分的叠加:径向、切向、重力
tmp = ccpAdd( ccpAdd( radial, tangential), modeA.gravity);
tmp = ccpMult( tmp, dt);

//受到加速度的影响,速度发生了偏移
p->modeA.dir = ccpAdd( p->modeA.dir, tmp);

//第三步,求新的位置。这里复用了tmp变量,现在是路程 = 速度 dir * 时间 dt,根据移动距离可以得到点的新位置 pos
tmp = ccpMult(p->modeA.dir, dt);
p->pos = ccpAdd( p->pos, tmp );


//更新 color, size, rotation,很简单,这里略去
…………

//根据位置类型追踪坐标系,由于前面已经分析了,这里略去,最终得到的位置是 newPos
…………

//如果是在 batchnode 模式下,还要把 m_tPosition 的影响加进去,从注释来看,是在这个模式下,position不会反应在变换矩阵中
//不过我不会用 batchnode,所以这里不会走(水平差也有好处)
if (m_pBatchNode) {
    newPos.x+=m_tPosition.x;
    newPos.y+=m_tPosition.y;
}
    如上面注释所述,主要分为三步:

    1)求加速度:

    加速度有三个,一个是径向加速度,一个是切向加速度。所谓径向,就是从原点指向当前位置(注意径向并不是指速度方向)。

    最后一个受力来源是重力,同样体现为加速度,与前面两个加速度不同,这个重力加速度是不受位置或速度方向影响的。这是重力加速度的特点,否则就可以用前两者完全替代了。

    这里稍微总结一下:如果是一个方向不变的受力,可以用重力加速度来实现。如果受力方向会随着位置移动而变化,则可以使用切向和径向加速度来实现


    2)求速度:

    在原速度上加上加速度的影响就是新的速度:v2 = v1 + a * t


    3)求位置:

    这里实际上使用微分的概念进行了插值,认为这一段时间内的速度都是新速度 v2,所以直接用 v2 * t 求出了行进的路程作为偏移量,从而求取新的坐标点。



五、小小的总结

    本文中主要分析了几个简单的属性,对于纹理混合等未做研究,而且仅分析重力模式。主要涉及到的几个属性如下:

1、粒子系统属性配置

emitterType:发射类型,可以是 0 = kCCParticleModeGravity 重力模式, 或者是 1 = kCCParticleModeRadius 半径模式

m_ePositionType:位置类型。这个不能通过plist编辑,需要手动指定。可以是 kCCPositionTypeFree、kCCPositionTypeRelative、kCCPositionTypeGrouped


2、生命长度和个数:

duration:整个粒子系统的存活时间,-1表示无穷大

particleLifespan:单个粒子的平均存活时间

particleLifespanVariance:单个粒子存活时间浮动半径

maxParticles:最大粒子数量

m_fEmissionRate:发射速率,单位:个/秒,一般使用 maxParticles / particleLifespan 得到。不能通过plist直接指定。


3、纹理混合

blendFuncSource:源因子

blendFuncDestination:目标因子


4、大小:

startParticleSize:开始大小的平均值

startParticleSizeVariance:开始大小的浮动半径

finishParticleSize:终止时大小的平均值

finishParticleSizeVariance:终止时大小的浮动半径


5、颜色:

startColorXXX:起始的XX颜色分量平均值(XX = Red, Green, Blue, Alpha)

startColorVarianceXXX:起始的xx颜色分量浮动半径(XX = Red, Green, Blue, Alpha)

finishColorXXX:结束时XX颜色分量平均值(XX = Red, Green, Blue, Alpha)

finishColorVarianceXXX:结束的xx颜色分量浮动半径(XX = Red, Green, Blue, Alpha)


6、发射位置

sourcePositionx:发射点的X坐标(作用同setPosition)

sourcePositiony:发射点的Y坐标(作用同setPosition)

sourcePositionVariancex:粒子发射时,对发射点在X分量上的偏移半径

sourcePositionVariancey:粒子发射时,对发射点在Y分量上的偏移半径


7、旋转

rotationStart:开始时,粒子的旋转平均值

rotationStartVariance:开始时,粒子旋转的浮动半径

rotationEnd:结束时,粒子的旋转平均值

rotationEndVariance:结束时,粒子旋转的浮动半径


8、速度

angle:发射角度平均值

angleVariance:发射角度的浮动半径

speed:初速度的大小(即“模”)

speedVariance:初速度大小的浮动半径

gravityx:重力加速度的X分量

gravityy:重力加速度的Y分量

radialAcceleration:径向加速度

radialAccelVariance:径向加速度的浮动半径

tangentialAcceleration:切向加速度

tangentialAccelVariance:切向加速度的浮动半径


9、纹理

textureFileName:纹理文件名称

textureImageData:纹理数据。优先级低于 textureFileName,只有在 textureFileName 没有的时候才起效。另外这两者必须有一个。

你可能感兴趣的:(【cocos2D-x学习】10.粒子特效(重力模式)的入门级学习——Read The F**king Code)