Particles are objects which have their own lifetime and specific motion behavior. After the time is up they just simply disappear. Particle system must handle multiple objects considering that they may own different motion behavior and also different lifetime.
Because mobile devices are rather slow I decided to use a bit more memory and created sprite images for each shading/blending level. All sprites are generated by the midlet. There are no PNG files but for making more fancy particles I’d rather go with PNG images. In this case there was no such need. There’s lot to be optimize yet but I’ve made my particle system in half an hour just for checking if the idea works ;).
The code below is responsible for making a single particle with all shading levels:
public static void initSystem() {
PARTICLE_RADIUS = (PARTICLE_SIZE >> 1);
// initialize an array that holds pixel data for all particle frames
particlePixels = new int[SHADING_LEVELS][PARTICLE_SIZE * PARTICLE_SIZE];
// initial alpha value - no transparency
int alphaValue = 0xFF;
int alphaStep = (alphaValue << Maths.FP) / PARTICLE_RADIUS;
for (int i = 0; i < PARTICLE_RADIUS; i++) {
if (i < HIGHEST_BRIGHT_RADIUS) {
alphaValue = 0xFF;
} else {
alphaValue = (int) (0xFF - ((alphaStep * i) >> Maths.FP));
}
// Drawing circles with different alpha. The greater radius, the lower alpha
for (int angle = 0; angle < 360; angle++) {
int x = (PARTICLE_RADIUS) + ((i * Maths.sin(angle)) >> Maths.FP);
int y = (PARTICLE_RADIUS) + ((i * Maths.cos(angle)) >> Maths.FP);
int color = ((alphaValue & 0xFF) << 24) | BASE_COLOR;
particlePixels[0][x + PARTICLE_SIZE * y] = color;
}
}
// Making shading/blend levels
alphaStep = (0xFF << Maths.FP) / SHADING_LEVELS;
alphaValue = 0;
for (int i = 1; i < SHADING_LEVELS; i++) {
alphaValue += alphaStep;
for (int p = 0; p < particlePixels[0].length; p++) {
int originalAlpha = (particlePixels[0][p] >> 24) & 0xFF;
int newAlpha = originalAlpha - (alphaValue >> Maths.FP);
if (newAlpha < 0) {
newAlpha = 0;
}
particlePixels[i][p] = ((newAlpha & 0xFF) << 24) | (particlePixels[0][p] & 0xFFFFFF);
}
}
}
Each particle owns the following properties:
- life time in millis
- x and y coords
- motion listener
The motion listener uses ParticleSprite.ParticleMotionListener inner interface:
public interface ParticleMotionListener {
public void updateParticlePosition(ParticleSprite s, int x, int y, int timeInMillis, int tickNumber);
}
The method updateParticlePosition() takes current sprite, its coordinates, time that tells how “old” particle is in millis and number of tick/update as arguments. Variable timeInMillis and tickNumber are initialized with first particle update. tickNumber is initially set to 0 and timeInMillis takes current time as default.
All particles are part of particles pool. For current case I used Vector type to keep all elements together but it might be better to allocate an array of N elements and instead of adding/removing it from Vector better way could be to enable/disable specific particle. Maybe next time I will update it. I also implemented release pool that holds all elements which are marked to be terminated. In other words the update goes through following steps:
For each particle:
- update current particle
- if particle life time is over, mark particle to be removed and add it to release pool
For each particle marked to be removed:
- get particle object
- remove its reference from particle pool
- after all is done clear release pool
This is how updateSystem() method is implemented:
public static void updateSystem() {
final int size = particleCount = vecParticleSystemPool.size();
if ( size == 0 ) {
return;
}
final long currentTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
ParticleSprite sprite = (ParticleSprite) vecParticleSystemPool.elementAt(i);
if (sprite.m_lastUpdateTime < 0) {
sprite.m_firstUpdateTime = currentTime;
}
sprite.m_lastUpdateTime = currentTime - sprite.m_firstUpdateTime;
sprite.m_tickNum++;
if (sprite.m_lastUpdateTime > sprite.m_lifetime) {
vecParticleRemoveSystemPool.addElement(sprite);
} else {
if (sprite.m_listener != null) {
sprite.m_listener.updateParticlePosition(sprite, sprite.m_x, sprite.m_y, (int) (sprite.m_lastUpdateTime), sprite.m_tickNum);
}
}
}
final int removeSize = vecParticleRemoveSystemPool.size();
for ( int i = 0; i < removeSize; i++ ) {
ParticleSprite sprite = (ParticleSprite)vecParticleRemoveSystemPool.elementAt(i);
vecParticleSystemPool.removeElement( sprite );
}
vecParticleRemoveSystemPool.removeAllElements();
}
Painting is even simpler, I don’t think it requires any additional comment:
public static void paintSystem(Graphics g) {
final int size = vecParticleSystemPool.size();
for (int i = 0; i < size; i++) {
ParticleSprite sprite = (ParticleSprite) vecParticleSystemPool.elementAt(i);
int level = (int) (sprite.m_lastUpdateTime * SHADING_LEVELS) / sprite.m_lifetime;
if (level >= 0 && level < SHADING_LEVELS) {
drawParticle(g, sprite.m_x, sprite.m_y, level);
}
}
}
This is actually everything. As you can see it’s not very complex.
The application handles two modes. First is auto smoke. It just generates smoke-like effect. Second mode is called DRAGGING and lets user make particles by dragging stylus/finger over the screen. Of course it’s only working if device has touch screen. Otherwise there’s no use for DRAGGING mode.
You might be worried about overall performance and if it’s worth using in any game. Certainly it requires good device. It won’t work with low-end phones so particles should be optional for better phones. I did some tests and phones like SE K700i, K750, K610, W890, W910i handled it really well and they can draw particles in game. Moreover, they can call initSystem() method and reinitializing is immediate. It means they may change color or size of particle on fly.
It’s hard to not notice that use of memory is pretty big. Lets say we have 16 shading levels and particles 32×32. This will take 64kb of ram for single particle. If we want to have more particle patterns ( for example different colors ) then we’ll have to use even more memory. Of course we can reduce number of shading levels but there’s one more optimization that comes to my head. Basically, each particle is a circle. It means we can generate only one quarter ( left top quarter ) and draw other quarters using manipulations. This will reduce amount of used memory by 4 times! 32×32 particles will take 16kb of ram. Sure it’s gonna work well only with devices with really fast drawRGB() but this is a battle and all tricks are allowed
To show better use of the system when I find a little of time I’ll try to make the effect a part of game-like application. For now take a look at simple demo: