粒子系统
基本结构
- Node类是场景对象的基础类,所有场景的元素都必须是Node或Node的基类
- BlendProtocol:提供了设置openGL混合方式的基类
- TextureProtocol:抽象类,如果需要设置纹理的混合方式,可以继承改类,需要重写setTexture接口
- PlayableProtocol:interface for playable items,抽象类,提供了start和stop方法
ParticleSystem
完成粒子系统基础功能的类,实现了粒子数据的管理、初始状态的设置、每帧的变化、粒子的消亡、渲染等功能。
枚举介绍
- 首先使用枚举类的方式定义了Mode粒子类型:重力模式和半径模式
enum class Mode
{
GRAVITY,
RADIUS,
};
- 然后定义了粒子的position可能的移动位置类型
enum class PositionType
{
FREE, /** 已产生粒子附着在世界上,不受发射器重新定位的影响 */
RELATIVE, /** 已产生的粒子附着在世界上,但会跟随发射器重新定位。
用例:将一个发射器附加到一个精灵上,并且你希望发射器跟随精灵。*/
GROUPED, /** 有生命的粒子附着在发射器上,并与它一起被移动。 */
};
- 一些设置枚举的定义
enum {
/** 粒子发射器永远存在。 */
DURATION_INFINITY = -1,
/** 粒子的开始大小等于结束大小。 */
START_SIZE_EQUAL_TO_END_SIZE = -1,
/**粒子的开始半径等于结束半径。*/
START_RADIUS_EQUAL_TO_END_RADIUS = -1,
};
数据结构和内存
定义了ParticleData
类来存储所有的粒子数据,包括位置、颜色、大小、持续时间等。提供了初始化和复制接口。
其定义的属性都是指针:float* posx;
在初始化时根据粒子系统传入的最大数量申请空间:
bool ParticleData::init(int count)
{
maxCount = count;
posx= (float*)malloc(count * sizeof(float));
...
modeB.radius= (float*)malloc(count * sizeof(float));
return posx && posy && startPosY && startPosX && colorR && colorG && colorB && colorA &&
deltaColorR && deltaColorG && deltaColorB && deltaColorA && size && deltaSize &&
rotation && deltaRotation && timeToLive && atlasIndex && modeA.dirX && modeA.dirY &&
modeA.radialAccel && modeA.tangentialAccel && modeB.angle && modeB.degreesPerSecond &&
modeB.deltaRadius && modeB.radius;
}
以数组的方式管理所有粒子,新产生的粒子、消亡的粒子、改变的粒子具有连续性,会加快之后的更新逻辑。
整个粒子系统运行过程中,不会再新增空间,粒子系统的内存大小只和最大粒子数有关,这也就意味着之后不能修改粒子最大数量。
此外,ParticleSystem类中,还声明了各种成员变量,用来存储设置文件中的初始值。
初始状态的设置
提供了2种设置的方式,一种是读取plist配置文件,一种只设置最大粒子数。调用bool ParticleSystem::initWithDictionary(ValueMap& dictionary, const std::string& dirname)
接口设置ParticleSystem
的初始成员变量。
根据不同配置设置mode:
_emitterMode = (Mode) dictionary["emitterType"].asInt();
// Mode A: Gravity + tangential accel + radial accel
if (_emitterMode == Mode::GRAVITY)
{
// gravity
modeA.gravity.x = dictionary["gravityx"].asFloat();
...
modeA.rotationIsDir = dictionary["rotationIsDir"].asBool();
}
// or Mode B: radius movement
else if (_emitterMode == Mode::RADIUS)
{
if (!_configName.empty())
{
modeB.startRadius = dictionary["maxRadius"].asInt();
}
else
{
modeB.startRadius = dictionary["maxRadius"].asFloat();
}
modeB.startRadiusVar = dictionary["maxRadiusVariance"].asFloat();
if (!_configName.empty())
{
modeB.endRadius = dictionary["minRadius"].asInt();
}
else
{
modeB.endRadius = dictionary["minRadius"].asFloat();
}
if (dictionary.find("minRadiusVariance") != dictionary.end())
{
modeB.endRadiusVar = dictionary["minRadiusVariance"].asFloat();
}
else
{
modeB.endRadiusVar = 0.0f;
}
if (!_configName.empty())
{
modeB.rotatePerSecond = dictionary["rotatePerSecond"].asInt();
}
else
{
modeB.rotatePerSecond = dictionary["rotatePerSecond"].asFloat();
}
modeB.rotatePerSecondVar = dictionary["rotatePerSecondVariance"].asFloat();
} else {
CCASSERT( false, "Invalid emitterType in config file");
CC_BREAK_IF(true);
}
当非batchNode时,会加载需要用到的纹理,或者从base64-gzipped data数据中读取纹理。
每帧的变化
首先,在onEnter
接口中,会调用scheduleUpdateWithPriority
注册监听
在Update接口中对粒子的改变进行计算:
新粒子的产生
- 计算需要产生的粒子数量
简化了下粒子计算,产生的新粒子数不能超过最大粒子数量,参数的数量为时间*发射率:
int emitCount = MIN(totalParticles - _particleCount, dt * _emissionRate);
- 调用
addParticles(emitCount);
添加粒子,该接口增加当前粒子数,设置随机数,使用优化的随机函数进行随机,设置粒子的各种初始状态。
/**
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;
}
- 更新_elapsed 已存活时间,如果超过_duration持续时间,则关闭新粒子的产生,并不关闭粒子系统,已产生的粒子还需要更新。
粒子的改变
- 遍历所有粒子,更新所有粒子的存活时间
- 遍历所有粒子,如果有存活时间小于0的粒子,将其后的粒子从数组中向前拷贝,并减少粒子计数,如果粒子数为0并设置了自动移除,则取消计时器结束粒子系统
- 根据粒子设置的模式,有2种更新位置的方式:
- 如果是重力模式:根据各种加速度计算出速度,再计算出位置
- 否则直接累乘计算:代码如下。贴代码是发现有个地方值得学习:
这部分的_particleData更新使用了多个单独的for循环,目的是为了提高cache命中率,前文说过,我们的particleData是一块连续的内存,cache读取数据时一次读取一块,连续的数据会提前加载进内存,单独遍历有助于提高命中率
//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;
}
- 更新颜色、size、angle。采用了个上面一样的提高缓存命中率的方法单独循环更新:
//color r,g,b,a
for (int i = 0 ; i < _particleCount; ++i)
{
_particleData.colorR[i] += _particleData.deltaColorR[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;
}
粒子的消亡
渲染
ParticleSystem作为粒子系统的基类,并不提供渲染功能,其子类ParticleSystemQuad进行了封装。
ParticleSystemQuad
游戏内都是使用ParticleSystemQuad
进行粒子的创建
- 增加了setTotalParticles接口,可以设置粒子最大数量,内部是对_particleData空间的重新分配
- 增加了draw接口进行渲染:
void ParticleSystemQuad::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
//quad command
if(_particleCount > 0)
{
auto programState = _quadCommand.getPipelineDescriptor().programState;
programState->setTexture(_textureLocation, 0, _texture->getBackendTexture());
cocos2d::Mat4 projectionMat = Director::getInstance()->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
programState->setUniform(_mvpMatrixLocaiton, projectionMat.m, sizeof(projectionMat.m));
_quadCommand.init(_globalZOrder, _texture, _blendFunc, _quads, _particleCount, transform, flags);
renderer->addCommand(&_quadCommand);
}
}
- 顶点数据_quads,在updateParticleQuads接口中进行更新,根据不同的positiontype调用updatePosWithParticle(quadStart, newPos, *s, *r);
inline void updatePosWithParticle(V3F_C4B_T2F_Quad *quad, const Vec2& newPosition,float size,float rotation)
{
// vertices
float size_2 = size/2;
float x1 = -size_2;
float y1 = -size_2;
float x2 = size_2;
float y2 = size_2;
float x = newPosition.x;
float y = newPosition.y;
float r = (float)-CC_DEGREES_TO_RADIANS(rotation);
float cr = cosf(r);
float sr = sinf(r);
float ax = x1 * cr - y1 * sr + x;
float ay = x1 * sr + y1 * cr + y;
float bx = x2 * cr - y1 * sr + x;
float by = x2 * sr + y1 * cr + y;
float cx = x2 * cr - y2 * sr + x;
float cy = x2 * sr + y2 * cr + y;
float dx = x1 * cr - y2 * sr + x;
float dy = x1 * sr + y2 * cr + y;
// bottom-left
quad->bl.vertices.x = ax;
quad->bl.vertices.y = ay;
// bottom-right vertex:
quad->br.vertices.x = bx;
quad->br.vertices.y = by;
// top-left vertex:
quad->tl.vertices.x = dx;
quad->tl.vertices.y = dy;
// top-right vertex:
quad->tr.vertices.x = cx;
quad->tr.vertices.y = cy;
}
使用
cocos2dx内置了几种粒子。
auto fire = ParticleFire::create();
fire->setTexture(Director::getInstance()->getTextureCache()->addImage("Images/fire.png"));
fire->setPosition( Vec2(80.0f, s.height/2-50) );
quad是一个V3F_C4B_T2F_Quad结构,存放了顶点的颜色坐标和纹理信息