[Particle System] - Particle "Plateauing" problem

Hey,

I’ve been working on a particle system from scratch off-and-on for the past three days and I’ve got it working quite nicely (most of the time). At the moment I’m working on a little fire effect and in doing so I’ve found a strange bug which I haven’t been able to fix.

As you can see in this image:

A lot of the particles seem to “plateau” at equal intervals. I’ve changed values and modified code all over the place while tying to fix this problem, but I can’t explain it nor fix it.

If anyone has some suggestions or the cause, and hopefully (fingers crossed) a way to fix this problem that would be great.

The Screen class. This is where the game-loop along with the update and render methods are.

package core;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import fire.Fire;

/**
 * Represents a screen on which to draw.
 * @author Valkryst
 * --- Last Edit 14-Jan-2014
 */
public class Screen extends Canvas implements Runnable {
	private static final long serialVersionUID = 4532836895892068039L;
	
	private final JFrame frame;
	private Thread gameThread;
	private boolean isGameRunning = true;
	
	private BufferStrategy BS = getBufferStrategy();
	
	// Testing Stuff:
	private Fire fire = new Fire(256.0, 512.0);
	// End Testing Stuff.
	
	public Screen() {
		frame = new JFrame();
		frame.setTitle("Particle Test");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setSize(new Dimension(512, 544));
		frame.setResizable(false);
		frame.setLocationRelativeTo(null);
		frame.requestFocus();
		
		setSize(new Dimension(512, 544));
		setFocusable(true);
		setVisible(true);
		
		frame.add(this);
		frame.setVisible(true);
		
		start();
	}

	public void run() {
		long lastLoopTime = System.nanoTime();
	    final int TARGET_FPS = 60;
		final long OPTIMAL_TIME = 1000000000 / TARGET_FPS;   
		double delta = 0;
		
		// Keep looping until the game ends.
		while(isGameRunning) {
				long now = System.nanoTime();
				long updateLength = now - lastLoopTime;
				lastLoopTime = now;
			    delta += updateLength / ((double)OPTIMAL_TIME); // Work out how long its been since the last update. This will be used to calculate how far the entities should move this loop.
			    
			    //Update the game's logic and then render the screen.
			    while(delta >= 1) {
			    	updateLogic(delta);
			    	delta--;
			    }
			    
			    render();
			      
			    // we want each frame to take 10 milliseconds, to do this
			    // we've recorded when we started the frame. We add 10 milliseconds
			    // to this and then factor in the current time to give 
			    // us our final value to wait for
			    // remember this is in ms, whereas our lastLoopTime etc. vars are in ns.
			    try {
			    	long tempLong = (lastLoopTime-System.nanoTime() + OPTIMAL_TIME)/1000000;
			    	if(tempLong <= 0) { continue; } // Skips the sleep()
					Thread.sleep(tempLong);
				} catch (InterruptedException e) {
					continue;
				}
		}
		
		stop();
	}
	
	public synchronized void start() {
		setBackground(Color.black);
		isGameRunning = true;
		gameThread = new Thread(this, "Display");
		gameThread.start();
	}

	public synchronized void stop() {
		try {
			gameThread.join();
		} catch(InterruptedException e) {
			Logger.writeError(e.getMessage());
		}
	}

	// When called this updates all of the game's logic.
	public void updateLogic(double delta) {
		fire.update();
	}

	// When called this updates the screen.
	public void render() {
		// Forces the canvas to use triple buffering.
		BS = getBufferStrategy();
        if (BS == null) {
        	SwingUtilities.invokeLater(new Runnable() {
        	    public void run() {
        	        createBufferStrategy(3);
        	    }
        	});
        	return;
        }
		
        // Creates the graphics object and then clears the screen.
        Graphics g = BS.getDrawGraphics();
        g.clearRect(0, 0, getWidth(), getHeight());
        
        fire.render(g);
        
        g.dispose();
		BS.show();
	}
}

The Fire class. This just handles a fire object and all that:

package fire;

import java.awt.Graphics;
import java.awt.Point;
import java.util.Iterator;
import java.util.Random;

import particle.Particle;
import particle.ParticleList;

/**
 * Represents a fire.
 * @author Valkryst
 * --- Last Edit 14-Jan-2014
 */
public class Fire {
	private static Random random = new Random();
	/** A collection of particles that make up the fire.*/
	private ParticleList particles;
	/** The origin of this fire on the X-axis.*/
	private double originX;
	/** The origin of this fire on the Y-axis.*/
	private double originY;
	private int counter = 0;
	
	/**
	 * Constructs a new Fire object.
	 */
	public Fire(double originXIn, double originYIn) {
		particles = new ParticleList();
		
		originX = originXIn;
		originY = originYIn;
	}
	
	/**
	 * Updates the fire.
	 */
	public void update() {
		particles.removeDecayedParticles();

		if(counter == 10) {
			for(int i=0;i<50;i++) { newParticle(159, 70, 24, 100, 4, 30, 40); }
			for(int i=0;i<40;i++) { newParticle(208, 117, 29, 100, 4, 20, 30); }
			for(int i=0;i<25;i++) { newParticle(246, 206, 72, 100, 2, 10, 20); }
			for(int i=0;i<10;i++) { newParticle(251, 239, 169, 100, 2, 5, 10); }
			counter = 0;
		} else {
			counter++;
		}

		Iterator<Particle> it = particles.getParticles().iterator();
		while(it.hasNext()) {
			it.next().update();
		}
	}
	
	/**
	 *  Renders the fire to the screen.
	 * @param g Graphics object with which to draw.
	 */
	public void render(Graphics g) {
		Iterator<Particle> it = particles.getParticles().iterator();
		
		while(it.hasNext()) {
			it.next().render(g);
		}
	}
	
	/**
	 * Creates a new Particle object.
	 * @param redIn The red color value of the new Particle.
	 * @param greenIn The green color value of the new Particle.
	 * @param blueIn The blue color value of the new Particle.
	 * @param alphaIn The alpha value of the new Particle.
	 * @param movementSpeedIn The movement speed of the new Particle.
	 * @param riseIn The number of pixels, up or down, that the new Particle will move per movement.
	 * @param runIn The number of pixels, left or right, that the new Particle will move per movement.
	 * @param xCoordIn The location of the new Particle on the X-axis.
	 * @param yCoordIn The location of the new Particle on the Y-axis.
	 * @param sizeIn The size, in pixels^2, of the new Particle.
	 * @param decayTimeIn The number of movements before the new Particle decays.
	 * @param maximumXAxisMovement The maximum distance, left or right, that the particle can move.
	 */
	public void newParticle(int redIn, int greenIn, int blueIn, int alphaIn, int sizeIn, int decayTimeIn, int maximumXAxisMovement) {
		int totalPoints = random.nextInt(20) + 1;
		double x = originX;
		boolean isNegative;
		Point[] points = new Point[totalPoints];
		
		for(int i=0;i<totalPoints;i++) {
			isNegative = random.nextBoolean();
			x -= (isNegative ? -1 : 1) * random.nextInt(maximumXAxisMovement);
			points[i] = new Point((int)x, (int)originY - (i*20));
			x = originX;
		}
		
		particles.addParticle(new Particle(redIn, blueIn, greenIn, alphaIn, 1, 1, 1, originX, originY, points, sizeIn, decayTimeIn * 10, true));
	}
}

The Particle class:

package particle;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;

import core.Logger;

/**
 * Represents a particle.
 * @author Valkryst
 * --- Last Edit 14-Jan-2014
 */
public class Particle {
	/** A color value. 0-255. */
	private int red, green, blue;
	/** The color of this pixel.*/
	private Color color;
	/** Alpha value. 0-100. */
	private int alpha;
	/** How many pixels/second this particle is moving. */
	private double movementSpeed;
	/** How many pixels up or downwards this particle moves per movement. */
	private double rise;
	/** How many pixels left or right this particle moves per movement. */
	private double run;
	/** Location of this particle on the X-axis. */
	private double xCoord;
	/** Location of this particle on the Y-axis. */
	private double yCoord;
	/** The destinations of this particle. */
	private Point[] destinations;
	/** The number of destinations reached by this particle. */
	private int destinationsReached = 0;
	/** The size of this particle in pixels^2.*/
	private int size;
	/** The number of movements before the particle decays. */
	private int decayTime;
	/** The number of times this particle has moved. */
	private int movementsTaken;
	/** Whether or not to draw this particle as a rectangle or filled rectangle. */
	private boolean isFilled;
	
	/**
	 * Constructs a new Particle object.
	 * @param redIn The red color value of the new Particle.
	 * @param greenIn The green color value of the new Particle.
	 * @param blueIn The blue color value of the new Particle.
	 * @param alphaIn The alpha value of the new Particle.
	 * @param movementSpeedIn The movement speed of the new Particle.
	 * @param riseIn The number of pixels, up or down, that the new Particle will move per movement.
	 * @param runIn The number of pixels, left or right, that the new Particle will move per movement.
	 * @param xCoordIn The location of the new Particle on the X-axis.
	 * @param yCoordIn The location of the new Particle on the Y-axis.
	 * @param sizeIn The size, in pixels^2, of the new Particle.
	 * @param decayTimeIn The number of movements before the new Particle decays.
	 */
	public Particle(int redIn, int greenIn, int blueIn, int alphaIn, int movementSpeedIn, double riseIn, double runIn, double xCoordIn, double yCoordIn, Point[] destinationsIn,  int sizeIn, int decayTimeIn, boolean isFilledIn) {
		if(isColorValueValid(redIn) && isColorValueValid(greenIn) && isColorValueValid(blueIn) && isAlphaValueValid(alphaIn)) {
			red = redIn;
			green = greenIn;
			blue = blueIn;
			alpha = alphaIn;
		} else {
			Logger.writeError("Invalid color or alpha value. Using default values.");
			red = 255;
			blue = 255;
			green = 255;
			alpha = 0;
		}
		
		color = new Color(red, blue, green, alpha);
		movementSpeed = movementSpeedIn;
		rise = riseIn;
		run = runIn;
		xCoord = xCoordIn;
		yCoord = yCoordIn;
		destinations = destinationsIn;
		size = sizeIn;
		decayTime = decayTimeIn;
		isFilled = isFilledIn;
	}
	
	/**
	 * @param colorValue The color value to check.
	 * @return Whether or not the specified color value is within the range of 0-255.
	 */
	public boolean isColorValueValid(int colorValue) {
		return (colorValue >= 0 && colorValue <= 255 ? true : false);
	}
	
	/**
	 * @param alphaValue The alpha value to check.
	 * @return Whether or not the specified alpha value is within the range of 0-100.
	 */
	public boolean isAlphaValueValid(int alphaValue) {
		return (alphaValue >= 0 && alphaValue <= 100 ? true : false);
	}
	
	/**
	 * Updates the position and alpha of the particle.
	 */
	public void update() {
		double destinationX = destinations[destinationsReached].getX();
		double destinationY = destinations[destinationsReached].getY();
		double movementX = (rise * movementSpeed);
		double movementY = (run * movementSpeed);
		
		// If the particle has reached it's current destination then set it on-course for the next one. If it has reached the final destination then set it to be removed.
		if(destinationX == xCoord && destinationY == yCoord && destinationsReached < destinations.length - 1) {
				destinationsReached++;
		}
		
		// Update position:
		if(xCoord < destinationX) {
			xCoord += movementX;
		} else if (xCoord > destinationX) {
			xCoord -= movementX;
		}
		
		if(yCoord < destinationY) {
			yCoord += movementY;
		} else if (yCoord > destinationY) {
		    yCoord -= movementY;
		}
		
		// Update alpha:
		int newAlpha = (int)(((double)movementsTaken / (double)decayTime) * 100);
		newAlpha = 100 - newAlpha;
		color = new Color(red, blue, green, newAlpha);
		
		movementsTaken++;
	}
	
	/**
	 * Renders this Particle onto the screen.
	 * @param g Graphics object with which to draw.
	 */
	public void render(Graphics g) {
		g.setColor(color);
		if(isFilled) {
			g.fillRect((int)xCoord, (int)yCoord, size, size);
		} else {
			g.drawRect((int)xCoord, (int)yCoord, size, size);
		}
	}

	//////////////////////////////////// Get methods:
	/**
	 * @return The red value of this Particle.
	 */
	public int getRed() {
		return red;
	}
	
	/**
	 * @return The blue value of this Particle.
	 */
	public int getBlue() {
		return blue;
	}
	
	/**
	 * @return The green value of this Particle.
	 */
	public int getGreen() {
		return green;
	}
	
	/**
	 * @return The alpha value of this Particle.
	 */
	public int getAlpha() {
		return alpha;
	}
	
	/**
	 * @return The movement speed of this Particle.
	 */
	public double getMovementSpeed() {
		return movementSpeed;
	}
	
	/**
	 * @return The rise of this Particle.
	 */
	public double getRise() {
		return rise;
	}
	
	/**
	 * @return The run of this Particle.
	 */
	public double getRun() {
		return run;
	}
	
	/**
	 * @return The location of this Particle on the X-axis.
	 */
	public double getXCoord() {
		return xCoord;
	}
	
	/**
	 * @return The location of this Particle on the Y-axis.
	 */
	public double getYcoord() {
		return yCoord;
	}
	
	/**
	 * @return The destinations of this Particle.
	 */
	public Point[] getDestinations() {
		return destinations;
	}
	
	/**
	 * @return The size of this Particle in pixels^2.
	 */
	public int getSize() {
		return size;
	}
	
	/**
	 * @return The number of moves before this Particle decays.
	 */
	public int getDecayTime() {
		return decayTime;
	}
	
	/**
	 * @return The number of movements taken so-far by this Particle.
	 */
	public int getMovementsTaken() {
		return movementsTaken;
	}
	
	/**
	 * @return Whether or not to draw this particle as a rectangle or filled rectangle.
	 */
	public boolean getIsFilled() {
		return isFilled;
	}
	
	//////////////////////////////////// Set methods:
	/**
	 * @param redIn The red color value to set to this Particle.
	 */
	public void setRed(int redIn) {
		if(isColorValueValid(redIn)) {
			red = redIn;
		} else {
			Logger.writeError("Invalid color value. Using default value.");
			red = 255;
		}
		color = new Color(red, blue, green, alpha);
	}
	
	/**
	 * @param blueIn The blue color value to set to this Particle.
	 */
	public void setBlue(int blueIn) {
		if(isColorValueValid(blueIn)) {
			blue = blueIn;
		} else {
			Logger.writeError("Invalid color value. Using default value.");
			blue = 255;
		}
		color = new Color(red, blue, green, alpha);
	}
	
	/**
	 * @param greenIn The blue color value to set to this Particle.
	 */
	public void setGreen(int greenIn) {
		if(isColorValueValid(greenIn)) {
			green = greenIn;
		} else {
			Logger.writeError("Invalid color value. Using default value.");
			green = 255;
		}
		color = new Color(red, blue, green, alpha);
	}
	
	/**
	 * @param alphain The alpha value to set to this Particle.
	 */
	public void setTransparency(int alphaIn) {
		if(isAlphaValueValid(alphaIn)) {
			alpha = alphaIn;
		} else {
			Logger.writeError("Invalid alpha value. Using default value.");
			alpha = 255;
		}
		color = new Color(red, blue, green, alpha);
	}
	
	/**
	 * @param movementSpeedIn The movement speed to set to this Particle.
	 */
	public void setMovementSpeed(double movementSpeedIn) {
		movementSpeed = movementSpeedIn;
	}
	
	/**
	 * @param destinationIn The destination to set to this Particle.
	 */
	public void setDestinations(Point[] destinationsIn) {
		destinations = destinationsIn;
	}
	
	/**
	 * @param riseIn The rise to set to this Particle.
	 */
	public void setRise(double riseIn) {
		rise = riseIn;
	}
	
	/**
	 * @param runIn The run to set to this Particle.
	 */
	public void setRun(double runIn) {
		run = runIn;
	}
	
	/**
	 * @param sizeIn The size to set to this Particle.
	 */
	public void setSize(int sizeIn) {
		size = sizeIn;
	}
	
	/**
	 * @param decayTimeIn The decayTime to set to this Particle.
	 */
	public void setDecayTime(int decayTimeIn) {
		decayTime = decayTimeIn;
	}
	
	/**
	 * @param isFilledIn The isFilled to set to this Particle.
	 */
	public void setIsFilled(boolean isFilledIn) {
		isFilled = isFilledIn;
	}
}

Hopefully whoever takes a look at this can understand the code. I’ve added way more comments than I need just for that purpose. =)

Thanks for any help.

I’m guessing that you’re shooting all particles upwards at the exact same speed (X pixels per frame), making them all move X pixels each frame. Since they all line up you get that effect. Try randomizing the starting velocity of the particle.

I’ve just tried randomizing the rise, run, speed, rise & run, rise & speed, and run & speed.
The first three had no effect on the plateaus.
Randomizing both the rise & run at the same time, using random.nextInt(3), on line 98 of the Fire class caused a lot of the particles to become stuck on either the first or second plateau and for the later plateaus to have less and less particles on them.
Everything else had no affect on the plateaus.

Rise & Run:

From this test I’m beginning to think that there may be something wrong with the movement part of the update() method for the particles on line 103 of the Particle class. I don’t see anything that looks like it could cause this though.

     
      if(destinationX == xCoord && destinationY == yCoord && destinationsReached < destinations.length - 1) {
            destinationsReached++;
      }

      // Update position:
      if(xCoord < destinationX) {
         xCoord += movementX;
      } else if (xCoord > destinationX) {
         xCoord -= movementX;
      }
      
      if(yCoord < destinationY) {
         yCoord += movementY;
      } else if (yCoord > destinationY) {
          yCoord -= movementY;
      }

hey,
This piece of code seems quite suspicious to me. I think that the particles that form the plateau are particules that are stuck at some “destinations”. You evaluate that a particule has reach the destination by testing equality on doubles (WRONG!!). I believe that the particles that are stuck never get this equality and are just moving up and down around the destination. Maybe do the test with a threshold ?
like:


      private static final float THRESHOLD = 0.1;

...

      if(Math.abs(destinationX-xCoord) < THRESHOLD && Math.abs(destinationY-yCoord) < THRESHOLD && destinationsReached < destinations.length - 1) {
            destinationsReached++;
      }

You should also change the order in which you do things, here if a destination is reached, the particule will still converge to it’s old destination. In my opinion the above test should be placed after moving the particle.

I’ve tried using your example with the threshold both before the movement and after. I’ve also tried my original way of doing it both before and after the movement. The final thing I did was to remove the destination check.

The first two cases gave no change no matter where they were or what the threshold was set to. As expected, when I remove the check, the particles just stay at the bottom of the screen. I think it may be safe to say that the destination check (seen below) is what’s causing this plateauing bug somehow.

// If the particle has reached it's current destination then set it on-course for the next one. If it has reached the final destination then set it to be removed.
	if(destinationX == xCoord && destinationY == yCoord && destinationsReached < destinations.length - 1) {
			destinationsReached++;
	}

Edit:
I’ve been changing every single thing I can think of and no matter what the plateaus never change.

Edit 2:
The spacing of the plateaus is thanks to “points[i] = new Point(x, originY - (i*20));” the i * 20 on this line of the Fire class. Still searching for anything that could cause the particles to stay on those 20 pixel intervals.

Float coordinates?

They’re already doubles. I made a custom Point class while editing the code. It works the exact same as the normal Point class, but with doubles.

Would you mind posting your particleList and main class too please? I’d like to compile this :smiley:

Hmm…

What exactly is the destination code supposed to do? Usually you’d just spawn a particle with a certain position, velocity and life time, and then update the velocity based on gravity (could be upwards for fire) and wind and position based on velocity. Then you’d remove it when it life ends.

try this:

randomize your destination Points for Y;


public void newParticle(int redIn, int greenIn, int blueIn, int alphaIn, int sizeIn, int decayTimeIn, int maximumXAxisMovement) {
      int totalPoints = random.nextInt(20) + 1;
      double x = originX;
      boolean isNegative;
      Point[] points = new Point[totalPoints];
      Random ran = new Random();
      for(int i=0;i<totalPoints;i++) {
         isNegative = random.nextBoolean();
         x -= (isNegative ? -1 : 1) * random.nextInt(maximumXAxisMovement);
         points[i] = new Point((int)x, (int)originY - (i*20 + ran.nextInt(20)));
         x = originX;
      }
      
      particles.addParticle(new Particle(redIn, blueIn, greenIn, alphaIn, 1, 1, 1, originX, originY, points, sizeIn, decayTimeIn * 10, true));
   }

The reason you have these stripes, is the behavior of the particles to rise and fall at these
same destination boundaries.
There they stick longer, and thus create these seams.

In the Fire class, when a particle is being created there are 20 points(destinations) randomly created for it. When a particle reaches it’s current destination then it is told to go towards the next destination. The Fire is just an effect which I’m creating using the particle system so it uses destinations instead of just sending the particle to go it’s course. The code related to destinations in the Particle class will only be used when the particle actually has destinations. (Haven’t written that part yet.)

@Gjallar
Here is the project folder:
https://mega.co.nz/#!zA8x3aiJ!NN6ZkXRzHGiKIwoFoo3X2QxxMldjYzOqsgdZ0MHrvLQ

@Damocles
Using random.nextInt(20) does seem to make the lines harder to see, but they’re still visible when looking at the fire.

I’ve had to modify the code before posting this to see fi I could fix a few things. The download link to the eclipse project folder doesn’t have this edited code.

public void newParticle(int redIn, int greenIn, int blueIn, int alphaIn, int sizeIn, int decayTimeIn, int maximumXAxisMovement) {
		int totalPoints = random.nextInt(20) + 1;
		double x = originX;
		Point[] points = new Point[totalPoints];
		
		for(int i=0;i<totalPoints;i++) {
			x -= (random.nextBoolean() ? -1 : 1) * random.nextInt(maximumXAxisMovement);
			points[i] = new Point(x, originY - (i*15 + (i==0 ? 0: random.nextInt(40))));
			x = originX;
		}
		
		particles.addParticle(new Particle(redIn, blueIn, greenIn, alphaIn, 1, 1, 1, originX, originY, points, sizeIn, decayTimeIn * 10, true));
	}

Edit: The lines aren’t as visible on the image for some reason, but I notice them when running it.