粒子系统分为发射器和粒子,发射器的作用就是获取配置文件中的各种数据,然后在特定的时刻产生粒子,并为该粒子赋值,而为了让粒子更加具有随机性,而采用了*Variance变量和CC_RANDOM_MINUS1_!()来保证随机性。
接下来则主要分析粒子的产生、更新和结束。
先看addParticles(int count),这个函数的作用有两个,第一个开辟出足够的空间(只是单纯的增加了_aprticleCount这个变量);第二个作用则是为添加的粒子进行赋值。
void ParticleSystem::addParticles(int count)
{
if (_paused)
return;
//获取随机种子
uint32_t RANDSEED = rand();
//保存当前的粒子数目,从该位置开始对粒子赋值
int start = _particleCount;
_particleCount += count;
//life
for (int i = start; i < _particleCount ; ++i)
{
float theLife = _life + _lifeVar * RANDOM_M11(&RANDSEED);
_particleData.timeToLive[i] = MAX(0, theLife);
}
//position
for (int i = start; i < _particleCount; ++i)
{
_particleData.posx[i] = _sourcePosition.x + _posVar.x * RANDOM_M11(&RANDSEED);
}
for (int i = start; i < _particleCount; ++i)
{
_particleData.posy[i] = _sourcePosition.y + _posVar.y * RANDOM_M11(&RANDSEED);
}
这里面的CC_RANDOM_M11()定义如下
/**
A more effect random number getter function, get from ejoy2d.
*/
inline static float RANDOM_M11(unsigned int *seed) {
*seed = *seed * 134775813 + 1;
union {
uint32_t d;
float f;
} u;
u.d = (((uint32_t)(*seed) & 0x7fff) << 8) | 0x40000000;
return u.f - 3.0f;
}
官方说这是一个更加有效的随机数字生成;而2.x时代使用的是CC_RANDOM_MINUS1_1()这个函数,应该功能相同,都是获取[-1,1]的一个随机值。
这里还需要注意下_sourcePosition的值,按照上一节的分析来说,这个值并不会被改变,所以在这里_sourcePosition应该为(0,0)才对。
#define SET_COLOR(c, b, v)\
for (int i = start; i < _particleCount; ++i)\
{\
c[i] = clampf( b + v * RANDOM_M11(&RANDSEED) , 0 , 1 );\
}
SET_COLOR(_particleData.colorR, _startColor.r, _startColorVar.r);
SET_COLOR(_particleData.colorG, _startColor.g, _startColorVar.g);
SET_COLOR(_particleData.colorB, _startColor.b, _startColorVar.b);
SET_COLOR(_particleData.colorA, _startColor.a, _startColorVar.a);
SET_COLOR(_particleData.deltaColorR, _endColor.r, _endColorVar.r);
SET_COLOR(_particleData.deltaColorG, _endColor.g, _endColorVar.g);
SET_COLOR(_particleData.deltaColorB, _endColor.b, _endColorVar.b);
SET_COLOR(_particleData.deltaColorA, _endColor.a, _endColorVar.a);
clamp(x, min, max)的作用是一个判断语句:
template<typename T>
T clamp(T x, T min, T max)
{
if (x < min)
return min;
else if (x > max)
return max;
return x;
}
#define SET_DELTA_COLOR(c, dc)\
for (int i = start; i < _particleCount; ++i)\
{\
dc[i] = (dc[i] - c[i]) / _particleData.timeToLive[i];\
}
SET_DELTA_COLOR(_particleData.colorR, _particleData.deltaColorR);
SET_DELTA_COLOR(_particleData.colorG, _particleData.deltaColorG);
SET_DELTA_COLOR(_particleData.colorB, _particleData.deltaColorB);
SET_DELTA_COLOR(_particleData.colorA, _particleData.deltaColorA);
//size
for (int i = start; i < _particleCount; ++i)
{
_particleData.size[i] = _startSize + _startSizeVar * RANDOM_M11(&RANDSEED);
_particleData.size[i] = MAX(0, _particleData.size[i]);
}
//特殊化处理,如果_startSize == _endSize,则必有_deltaSize = 0.f
if (_endSize != START_SIZE_EQUAL_TO_END_SIZE)
{
for (int i = start; i < _particleCount; ++i)
{
float endSize = _endSize + _endSizeVar * RANDOM_M11(&RANDSEED);
endSize = MAX(0, endSize);
_particleData.deltaSize[i] = (endSize - _particleData.size[i]) / _particleData.timeToLive[i];
}
}
else
{
for (int i = start; i < _particleCount; ++i)
{
_particleData.deltaSize[i] = 0.0f;
}
}
// rotation旋转
for (int i = start; i < _particleCount; ++i)
{
_particleData.rotation[i] = _startSpin + _startSpinVar * RANDOM_M11(&RANDSEED);
}
for (int i = start; i < _particleCount; ++i)
{
float endA = _endSpin + _endSpinVar * RANDOM_M11(&RANDSEED);
_particleData.deltaRotation[i] = (endA - _particleData.rotation[i]) / _particleData.timeToLive[i];
}
上面设置了color、size、rotation等变量,注意以下上面的Delta类变量的值和该粒子的timeToLive有关,timeToLive表示生命值,那么上面无论是color、size还是rotation的变化都是平hua的变化的。
// position
Vec2 pos;
if (_positionType == PositionType::FREE)
{
pos = this->convertToWorldSpace(Vec2::ZERO);
}
else if (_positionType == PositionType::RELATIVE)
{
pos = _position;
}
for (int i = start; i < _particleCount; ++i)
{
_particleData.startPosX[i] = pos.x;
}
for (int i = start; i < _particleCount; ++i)
{
_particleData.startPosY[i] = pos.y;
}
在前面设置了sourcePosition,这里设置了开始位置,暂时没有发现他们之间的区别。
// Mode Gravity: A
if (_emitterMode == Mode::GRAVITY)
{
// radial accel
for (int i = start; i < _particleCount; ++i)
{
_particleData.modeA.radialAccel[i] = modeA.radialAccel + modeA.radialAccelVar * RANDOM_M11(&RANDSEED);
}
// tangential accel
for (int i = start; i < _particleCount; ++i)
{
_particleData.modeA.tangentialAccel[i] = modeA.tangentialAccel + modeA.tangentialAccelVar * RANDOM_M11(&RANDSEED);
}
// rotation is dir
if( modeA.rotationIsDir )
{
for (int i = start; i < _particleCount; ++i)
{
float a = CC_DEGREES_TO_RADIANS( _angle + _angleVar * RANDOM_M11(&RANDSEED) );
Vec2 v(cosf( a ), sinf( a ));
float s = modeA.speed + modeA.speedVar * RANDOM_M11(&RANDSEED);
Vec2 dir = v * s;
_particleData.modeA.dirX[i] = dir.x;//v * s ;
_particleData.modeA.dirY[i] = dir.y;
_particleData.rotation[i] = -CC_RADIANS_TO_DEGREES(dir.getAngle());
}
}
else
{
for (int i = start; i < _particleCount; ++i)
{
float a = CC_DEGREES_TO_RADIANS( _angle + _angleVar * RANDOM_M11(&RANDSEED) );
Vec2 v(cosf( a ), sinf( a ));
float s = modeA.speed + modeA.speedVar * RANDOM_M11(&RANDSEED);
Vec2 dir = v * s;
_particleData.modeA.dirX[i] = dir.x;//v * s ;
_particleData.modeA.dirY[i] = dir.y;
}
}
}
设置了重力模式下的一些变量。注意这里的CC_DEGREES_TO_RADIANS(),这个hong函数是把角度转化成弧度,而所谓的方向dir就是sinf() cosf()得到的速度分量。
// Mode Radius: B
else
{
//Need to check by Jacky
// Set the default diameter of the particle from the source position
for (int i = start; i < _particleCount; ++i)
{
_particleData.modeB.radius[i] = modeB.startRadius + modeB.startRadiusVar * RANDOM_M11(&RANDSEED);
}
//particleData.modeB.angle保存的是弧度,而配置文件中保存的是角度
//不过angle的翻译不是角度?
for (int i = start; i < _particleCount; ++i)
{
_particleData.modeB.angle[i] = CC_DEGREES_TO_RADIANS( _angle + _angleVar * RANDOM_M11(&RANDSEED));
}
for (int i = start; i < _particleCount; ++i)
{
_particleData.modeB.degreesPerSecond[i] = CC_DEGREES_TO_RADIANS(modeB.rotatePerSecond + modeB.rotatePerSecondVar * RANDOM_M11(&RANDSEED));
}
if(modeB.endRadius == START_RADIUS_EQUAL_TO_END_RADIUS)
{
for (int i = start; i < _particleCount; ++i)
{
_particleData.modeB.deltaRadius[i] = 0.0f;
}
}
else
{
for (int i = start; i < _particleCount; ++i)
{
float endRadius = modeB.endRadius + modeB.endRadiusVar * RANDOM_M11(&RANDSEED);
_particleData.modeB.deltaRadius[i] = (endRadius - _particleData.modeB.radius[i]) / _particleData.timeToLive[i];
}
}
}
}
addParticles(int)这个函数的功能说简单dian就是为一定数量的粒子进行变量的赋值。
void ParticleSystem::stopSystem()
{
_isActive = false;
_elapsed = _duration;
_emitCounter = 0;
}
stopSystem()函数中所谓的停止只是对变量的赋值,那是如何让粒子发射器不再发射粒子和进行更新了呢?主要还是在于update()这个函数。
void ParticleSystem::resetSystem()
{
_isActive = true;
_elapsed = 0;
for (int i = 0; i < _particleCount; ++i)
{
_particleData.timeToLive[i] = 0.0f;
}
}
resetSystem()只是让_elapsed=0,以及前_particleCount个粒子的生命值全部为0,那么这些粒子会在update中进行收回。
接下来看看update函数
void ParticleSystem::update(float dt)
{
CC_PROFILER_START_CATEGORY(kProfilerCategoryParticles , "CCParticleSystem - update");
//当前处于活动状态且_emissionRate != 0.f
if (_isActive && _emissionRate)
{
//等同于float rate = _life / _totalParticles;
//计算出每个粒子的产出平均时间间隔
float rate = 1.0f / _emissionRate;
int totalParticles = static_cast<int>(_totalParticles * __totalParticleCountFactor);
//issue #1201, prevent bursts of particles, due to too high emitCounter
//避免粒子的喷发
if (_particleCount < totalParticles)
{
_emitCounter += dt;
if (_emitCounter < 0.f)
_emitCounter = 0.f;
}
//获取当前要发射的粒子的数目
int emitCount = MIN(totalParticles - _particleCount, _emitCounter / rate);
//添加粒子
addParticles(emitCount);
_emitCounter -= rate * emitCount;
_elapsed += dt;
if (_elapsed < 0.f)
_elapsed = 0.f;
//如果_duration != -1 且持续时间已经被_elapsed超过,则停止系统
//这个_duration表示的和Action的持续时间类似
if (_duration != DURATION_INFINITY && _duration < _elapsed)
{
this->stopSystem();
}
}
int emitCount = MIN(totalParticles - _particleCount, _emitCounter / rate);
Min的作用是避免产生的粒子数量超过totalParticles - _particleCount,rate表示产生粒子的时间间隔,那么_emitCounter / rate则是在这个间隔下要产出的粒子的数目是多少。
_emitCounter -= rate * emitCount;
则是减少发射器计数器的时间,这也就是为什么会有以下代码的原因
if (_particleCount < totalParticles)
{
_emitCounter += dt;
if (_emitCounter < 0.f)
_emitCounter = 0.f;
}
接着往下进行分析
{//不太懂这个{}的意思
//减去每个粒子的生命值
for (int i = 0; i < _particleCount; ++i)
{
_particleData.timeToLive[i] -= dt;
}
//如果存在粒子的生命值为0
for (int i = 0; i < _particleCount; ++i)
{
if (_particleData.timeToLive[i] <= 0.0f)
{
//从后向前找到一个生命值大于0的粒子
int j = _particleCount - 1;
while (j > 0 && _particleData.timeToLive[j] <= 0)
{
_particleCount--;
j--;
}
//把这个粒子赋值给前面的粒子
_particleData.copyParticle(i, _particleCount - 1);
if (_batchNode)
{
//disable the switched particle
int currentIndex = _particleData.atlasIndex[i];
_batchNode->disableParticle(_atlasIndex + currentIndex);
//switch indexes
_particleData.atlasIndex[_particleCount - 1] = currentIndex;
}
//相当于删除第 particleCount - 1那个粒子,因为其不再进行更新
--_particleCount;
//粒子死光且在之后自动删除自己,则先停止update函数的调用
//然后删除自己
if( _particleCount == 0 && _isAutoRemoveOnFinish )
{
this->unscheduleUpdate();
_parent->removeChild(this, true);
return;
}
}
}
这一块进行的主要是对“死亡”粒子的更新和粒子数组的安排,用的方法很是巧妙。
//如果是重力模式,则进行重力模式相关变量的更新
//根据上面的代码,能保证当前的粒子数组生命值都大于0
if (_emitterMode == Mode::GRAVITY)
{
for (int i = 0 ; i < _particleCount; ++i)
{
particle_point tmp, radial = {0.0f, 0.0f}, tangential;
// radial acceleration
if (_particleData.posx[i] || _particleData.posy[i])
{
normalize_point(_particleData.posx[i], _particleData.posy[i], &radial);
}
tangential = radial;
radial.x *= _particleData.modeA.radialAccel[i];
radial.y *= _particleData.modeA.radialAccel[i];
// tangential acceleration
std::swap(tangential.x, tangential.y);
tangential.x *= - _particleData.modeA.tangentialAccel[i];
tangential.y *= _particleData.modeA.tangentialAccel[i];
// (gravity + radial + tangential) * dt
tmp.x = radial.x + tangential.x + modeA.gravity.x;
tmp.y = radial.y + tangential.y + modeA.gravity.y;
tmp.x *= dt;
tmp.y *= dt;
_particleData.modeA.dirX[i] += tmp.x;
_particleData.modeA.dirY[i] += tmp.y;
// this is cocos2d-x v3.0
// if (_configName.length()>0 && _yCoordFlipped != -1)
// this is cocos2d-x v3.0
tmp.x = _particleData.modeA.dirX[i] * dt * _yCoordFlipped;
tmp.y = _particleData.modeA.dirY[i] * dt * _yCoordFlipped;
_particleData.posx[i] += tmp.x;
_particleData.posy[i] += tmp.y;
}
}
else
{
//Why use so many for-loop separately instead of putting them together?
//When the processor needs to read from or write to a location in memory,
//it first checks whether a copy of that data is in the cache.
//And every property's memory of the particle system is continuous,
//for the purpose of improving cache hit rate, we should process only one property in one for-loop AFAP.
//It was proved to be effective especially for low-end machine.
//对低端机 的优化??
for (int i = 0; i < _particleCount; ++i)
{
_particleData.modeB.angle[i] += _particleData.modeB.degreesPerSecond[i] * dt;
}
for (int i = 0; i < _particleCount; ++i)
{
_particleData.modeB.radius[i] += _particleData.modeB.deltaRadius[i] * dt;
}
for (int i = 0; i < _particleCount; ++i)
{
_particleData.posx[i] = - cosf(_particleData.modeB.angle[i]) * _particleData.modeB.radius[i];
}
for (int i = 0; i < _particleCount; ++i)
{
_particleData.posy[i] = - sinf(_particleData.modeB.angle[i]) * _particleData.modeB.radius[i] * _yCoordFlipped;
}
}
这个是环形模式下的变量的更新,不过看注释说为了低端机,所以每个循环仅仅处理一个字段。没有测试过,就不多说了。
//color r,g,b,a
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.colorR[i] += _particleData.deltaColorR[i] * dt;
}
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.colorG[i] += _particleData.deltaColorG[i] * dt;
}
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.colorB[i] += _particleData.deltaColorB[i] * dt;
}
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.colorA[i] += _particleData.deltaColorA[i] * dt;
}
//size
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.size[i] += (_particleData.deltaSize[i] * dt);
_particleData.size[i] = MAX(0, _particleData.size[i]);
}
//angle
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.rotation[i] += _particleData.deltaRotation[i] * dt;
}
updateParticleQuads();
_transformSystemDirty = false;
}
}//end bolck
这一块进行的是通用属性的更新。
// only update gl buffer when visible
if (_visible && ! _batchNode)
{
postStep();
}
CC_PROFILER_STOP_CATEGORY(kProfilerCategoryParticles , "CCParticleSystem - update");
}
这一块则是为其子类留下的一个接口。
ParticleSystem的分析基本结束,上面主要讲到了粒子是通过addParticles()这个函数产生的、在update中的操作主要是先根据流逝的时间产出对应的粒子,然后减少每个粒子的生命值,之后收回已经死亡的粒子,然后才对剩下的粒子进行属性更新。
这点和2.x不一样,在2.x中,减少生命值、删除生命值小于0的元素以及进行粒子的更新是在一块的,部分代码如下:
//如果显示标记为true
if (this->isVisible())
{
//遍历每一个粒子,进行更新
while (_particleIdx < _particleCount)
{
//取得对应的粒子
Particle* p = &_pParticles[_particleIdx];
//减少生命值
p->timeToLive -= dt;
//生命值大于0
if (p->timeToLive > 0.f)
{
//重力加速度模式
if (_emitterMode == Mode::GRAVITY)
{
Point tmp, radial, tangential;
//旋转角度的计算 向量归一化
if (p->pos.x || p->pos.y)
{
radial = p->pos;
radial.normalize();
}
tangential = radial;
//获取旋转角度
radial = radial * p->modeA.radialAccel;
//切角度变化计算
float newy = tangential.x;
tangential.x = -tangential.x;
tangential.y = newy;
tangential = tangential * p->modeA.tangentialAccel;
//位置的计算
tmp = radial + tangential + modeA.gravity;
tmp = tmp * dt;
p->modeA.dir = p->modeA.dir + tmp;
tmp = p->modeA.dir * dt;
p->pos = p->pos + tmp;
}
else//环形模式
{
//更新粒子的角度
p->modeB.angle += p->modeB.degreePerSec * dt;
p->modeB.radius += p->modeB.deltaRadius * dt;
//计算粒子的位置
p->pos.x = -cosf(p->modeB.angle) * p->modeB.radius;
p->pos.y = -sinf(p->modeB.angle) * p->modeB.radius;
}
//颜色的变化计算
p->color.r += p->deltaColor.r * dt;
p->color.g += p->deltaColor.g * dt;
p->color.b += p->deltaColor.b * dt;
//大小的变化计算
p->size += p->deltaSize * dt;
p->size = MAX(0, p->size);
//角度
p->rotation += p->deltaRotation * dt;
Point newPos;
//是自由模式或相对模式
if (_positionType == PositionType::FREE
|| _positionType == PositionType::RELATIVE)
{
Point diff = currentPosition - p->startPos;
newPos = p->pos - diff;
}
else
{
newPos = p->pos;
}
//TODO:应该要加
newPos += _position;
++_particleIdx;
}//生命值小于0
else
{
if (_particleIdx != _particleCount - 1)
{
_pParticles[_particleIdx] = _pParticles[_particleCount - 1];
}
--_particleCount;
//如果粒子数量为0,则设置自动删除粒子系统
if (_particleCount == 0 && _isAutoRemoveOnFinish)
{
//this->unscheduleUpdate();
this->removeFromParent();
return ;
}
}
}
_transformSystemDirty = false;
}
}
尤其在于对粒子的删除操作上,2.x默认认为后面那部分的粒子是活zhe的,然后直接进行了交换,而此时的_articleIdx并没有增加,那么下一次循环依然是这个粒子,而3.x则是分成了数个for循环。
我虽然借助cocos2dx实现了一个粒子引擎,但是我发现一个粒子文件一般要渲染数百次,对有批次渲染的还好,没有批次渲染的就差得多了。。。