[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.