【目标】:通过阅读源码,来学习粒子特效的重力模式中的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 没有的时候才起效。另外这两者必须有一个。