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.