Hello everybody.
I know that there is a lot of topic about that, but i would like to open a new one and share my solution/approach.
First of all, all the thanks goes to Kirill Vainer, the original author of the ParticleEmitter.
Ok, that said, what is the problem ? The problem is : we don’t have control on particles after they are emitted. Also, a change on this part of the code must not break anything.
So, here is my approach:
-
change everything “private” to “protected”. I know it’s not the same thing, and i know what is the difference and why people use private. Actually, you learn to use “private” and it becomes a reflex. It’s a good reflex, no problem here. But, for the ParticleEmitter that so many people complain about it for everything it lacks, it would be a good solution. There is still no “i didn’t notice i shouldn’t touch that and everything is broken” and you have the capability of modify it if you really want it.
-
create a ParticlesUpdater. I know, there is already a ParticleInfluencer but it’s an interface and if i add something on it, i’ll break compatibillity. What is the job of a ParticlesUpdater ? Here is the interface:
public interface ParticlesUpdater
{
/**
* Is called when a particle is newly used. As particles can be reused, don’t rely on “p” being actually a new object.
* @param p The used particle.
/
public void newParticle(Particle p);
/*
* Is called when a particle need an update.
* @param p The particle to update.
* @param tpf The frame duration.
* @param min min size of the particle.
* @param max max size of the particle.
* @param emitter The emitter of the particle.
*/
public void update(Particle p, float tpf, Vector3f min, Vector3f max, ParticleEmitter emitter);
}
For the second method, i really don’t like the “min” and “max” thing, but it comes from the header of the particleUpdate method of the ParticleEmitter.
- Modify the ParticleEmitter. As everything in it was private, i had to copy-past it [and with it ParticleMesh ('case a method of ParticleMesh take a ParticleEmitter as parameter and my class, even if copy pasted, is not the good ParticleEmitter and doesn’t extend it), ParticlePointMesh and ParticleTriMesh (these both cause they are affected to a ParticleMesh objet … yeah, once again my class doesn’t extend the original class].
Here is the modifications:
// The ParticlesUpdater used, with a state-less default value.
protected ParticlesUpdater particlesUpdater = DefaultParticlesUpdater.instance;
// Getter and setter.
public ParticlesUpdater getParticlesUpdater()
{
return particlesUpdater;
}
public void setParticlesUpdater(ParticlesUpdater particlesUpdater)
{
this.particlesUpdater = particlesUpdater;
}
in the emitParticle method:
p.color.set(startColor);
p.size = startSize;
//shape.getRandomPoint(p.position);
particleInfluencer.influenceParticle(p, shape);
particlesUpdater.newParticle(p);// <- MODIFICATION
if (worldSpace)
{
worldTransform.transformVector(p.position, p.position);
worldTransform.getRotation().mult(p.velocity, p.velocity);
And this method was completly replaced:
protected void updateParticle(Particle p, float tpf, Vector3f min, Vector3f max)
{
particlesUpdater.update(p, tpf, min, max, this);
}
-
Profit ! Ok, just befure that, the DefaultParticlesUpdated class
public class DefaultParticlesUpdater extends AbstractParticlesUpdater
{
public static final DefaultParticlesUpdater instance = new DefaultParticlesUpdater();
private transient Vector3f temp = new Vector3f();@Override
protected void applyGravity(Particle p, float tpf, ParticleEmitter emitter)
{
Vector3f gravity = emitter.getGravity();p.velocity.x -= gravity.x * tpf; p.velocity.y -= gravity.y * tpf; p.velocity.z -= gravity.z * tpf; temp.set(p.velocity).multLocal(tpf); p.position.addLocal(temp);
}
@Override
protected void affectColor(Particle p, float lifePercent, ParticleEmitter emitter)
{
ColorRGBA startColor = emitter.getStartColor();
ColorRGBA endColor = emitter.getEndColor();p.color.interpolate(startColor, endColor, lifePercent);
}
@Override
protected void affectSize(Particle p, float lifePercent, ParticleEmitter emitter)
{
float startSize = emitter.getStartSize();
float endSize = emitter.getEndSize();p.size = FastMath.interpolateLinear(lifePercent, startSize, endSize);
}
@Override
protected void affectAngle(Particle p, float tpf, ParticleEmitter emitter)
{
p.angle += p.rotateSpeed * tpf;
}@Override
protected void computeBoundingVolume(Particle p, Vector3f max, Vector3f min)
{
temp.set(p.position).addLocal(p.size, p.size, p.size);
max.maxLocal(temp);
temp.set(p.position).subtractLocal(p.size, p.size, p.size);
min.minLocal(temp);
}@Override
protected void selectImage(Particle p, float lifePercent, ParticleEmitter emitter)
{
boolean selectRandomImage = emitter.isSelectRandomImage();
int imagesX = emitter.getImagesX();
int imagesY = emitter.getImagesY();if (!selectRandomImage) { p.imageIndex = (int) (lifePercent * imagesX * imagesY); }
}
}
Most of its code comes from the old updateParticle method.
The AbstractParticlesUpdater:
public class AbstractParticlesUpdater implements ParticlesUpdater
{
@Override
public void update(Particle p, float tpf, Vector3f min, Vector3f max, ParticleEmitter emitter)
{
// Applying gravity
applyGravity(p, tpf, emitter);
// Affecting color, size and angle
float lifePercent = (p.startlife - p.life) / p.startlife;
affectColor(p, lifePercent, emitter);
affectSize(p, lifePercent, emitter);
affectAngle(p, tpf, emitter);
// Computing bounding volume
computeBoundingVolume(p, max, min);
// Selecting image
selectImage(p, lifePercent, emitter);
}
protected void applyGravity(Particle p, float tpf, ParticleEmitter emitter)
{
}
protected void affectColor(Particle p, float lifePercent, ParticleEmitter emitter)
{
}
protected void affectSize(Particle p, float lifePercent, ParticleEmitter emitter)
{
}
protected void affectAngle(Particle p, float tpf, ParticleEmitter emitter)
{
}
protected void computeBoundingVolume(Particle p, Vector3f max, Vector3f min)
{
}
protected void selectImage(Particle p, float lifePercent, ParticleEmitter emitter)
{
}
@Override
public void newParticle(Particle p)
{
}
}
As you can see, everything is pretty trivial. But now let’s see a class a bit less trivial:
public class AnimSpriteParticlesUpdater extends AbstractParticlesUpdater
{
protected List<Point2i[]> paths;
protected Map<Particle, Point2i[]> particlesPaths;
public AnimSpriteParticlesUpdater()
{
particlesPaths = new HashMap<>();
paths = new ArrayList<>();
}
public void addPath(Point2i ... path)
{
paths.add(path);
}
@Override
protected void selectImage(Particle p, float lifePercent, ParticleEmitter emitter)
{
int imagesX = emitter.getImagesX();
int imagesY = emitter.getImagesY();
Point2i[] particlePath = particlesPaths.get(p);
Point2i tileIndex = particlePath[(int)(lifePercent * particlePath.length)];
p.imageIndex = (imagesX * tileIndex.y) + tileIndex.x;
}
@Override
public void newParticle(Particle p)
{
// Select a random path for the particle
particlesPaths.put(p, selectAPath());
}
protected Point2i[] selectAPath()
{
assert(! paths.isEmpty());
return paths.get((int) (paths.size() * FastMath.nextRandomFloat()));
}
}
Ok, this class is not stateless and should be instanciated for every ParticleEmitter (otherwise you have a risk of memory leak with the map holding references to particles).
What this class does ? It allows the user to define a set of possible series of apparence for a particle. Exemple of use below:
public class Main extends SimpleApplication
{
public static void main(String[] args)
{
SimpleApplication app = new Main();
app.start();
}
@Override
public void simpleInitApp()
{
ParticleEmitter character = new ParticleEmitter("Character", ParticleMesh.Type.Triangle, 2);
character.setParticlesPerSec(1f);
character.setLowLife(2f);
character.setHighLife(2f);
character.setStartSize(3);
character.setEndSize(3);
character.setImagesX(4);
character.setImagesY(4);
character.setShape(new EmitterSphereShape(Vector3f.ZERO, 5));
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setTexture("ColorMap", assetManager.loadTexture("Textures/ogre.png"));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
character.setMaterial(mat);
AnimSpriteParticlesUpdater aspu = new AnimSpriteParticlesUpdater();
for (int i = 0; i < 4; i++)
{
Point2i[] path = new Point2i[4];
for (int j = 0; j < 4; j++)
{
path[j] = new Point2i(j, i);
}
aspu.addPath(path);
}
character.setParticlesUpdater(aspu);
rootNode.attachChild(character);
}
@Override
public void simpleUpdate(float tpf)
{
//TODO: add update code
}
@Override
public void simpleRender(RenderManager rm)
{
//TODO: add render code
}
}
Where the texture “ogre.png” is an ogre character like in rpg maker xp (4*4, walking in every directions). This code create particle showing the ogre walking.
I also created an updater that create like a gravity vortex with particles around, but i need to clean the code.
I can zip and post everything if you need.
I think that this approach would solve almost every limitation the current system has. I think, it’s my opinion, i am not sure. But, for exemple, it’s pretty easy to create a ParticlesUpdater that just delegate most of it’s work to a chain of other updaters.
It would also be nice to have a way to store datas about particles if not in particles directly then in the emitter responsible of thse particles. That way, there would be no risk of memory leak from here.
it was my 2 cent. Comments ? Ideas ? Reproachs ? Everything is good, and i hope that this time we will success in improving the particleemitter system.