[LIBGDX] Particles disappearing and then reappearing

Hey,

I’ve just, hopefully, finished converting my particle system from pure Java to libGDX. Everything seems to be working properly, except for the ‘death’ of the particles. The particles die and then reappear, or so it seems, for some reason unknown to me.

Each particle starts with a certain lifetime, and each update to the particle decreases the remaining lifetime. The alpha of the particle is calculated using (float)(remainingLife/totalLife). When the lifeTime reaches 0 then the particle is deleted because it’s alpha is 0.

Here’s the compiled project, you can see how the particles disappear and reappear. Exit with Alt+F4.
https://mega.co.nz/#!IstGjR6b!VZCkZ9ClDBUedZLHIPnchx9thb4Gs_asFvnlZpX7qo8

I’m thinking that the error is in how I setup libGDX in the Driver/Main classes, but it could also be with how I set the color/alpha in the RainbowSnow/Particle classes. Normally I’d be able to figure this kind of thing out pretty quickly, but I barely know how to use libGDX atm. Thanks for any ideas.

Driver:

package core.desktop;

import java.awt.Dimension;
import java.awt.Toolkit;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;

import core.Main;

public class DesktopLauncher {
	public static void main (String[] arg) {
		Dimension screenDimensions = Toolkit.getDefaultToolkit().getScreenSize();
		
		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
		config.title = "Particle System";
		config.width = (int)screenDimensions.getWidth();
		config.height = (int)screenDimensions.getHeight();
		config.fullscreen = true;
		config.useGL30 = true;
		
        new LwjglApplication(new Main(), config);
	}
}

Main:

package core;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

import effect.Effect;
import effect.RainbowSnow;

public class Main extends ApplicationAdapter {
	Effect e;
	ShapeRenderer shapeRenderer;

	@Override
	public void create() {
		e = new RainbowSnow(0.0, Gdx.graphics.getHeight() + 50, Gdx.graphics.getWidth());
		shapeRenderer = new ShapeRenderer();
	}

	@Override
	public void render() {
		((RainbowSnow)e).update();
		
		Gdx.gl.glClearColor(0, 0, 0, 0);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		
		((RainbowSnow)e).render(shapeRenderer, e.getIsOval());
	}

	@Override
	public void resize(int width, int height) {
	}
	
	@Override
	public void pause() {
	}

	@Override
	public void resume() {
	}

	@Override
	public void dispose() {
	}
}

Effect:

package effect;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import particle.Particle;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL30;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;

public class Effect {
	protected final static Random RANDOM = new Random();
	/** A collection of particles that make up the snow.*/
	protected final List<Particle> PARTICLES = new ArrayList<Particle>();
	/** The origin of this snow on the X-axis.*/
	protected final double ORIGIN_X;
	/** The origin of this snow on the Y-axis.*/
	protected final double ORIGIN_Y;
	/** Whether or not to render the particles as ovals. If not then render as squares. Ovals are extremely CPU intensive for large effects*/
	protected final boolean IS_OVAL;
	protected Iterator<Particle> iterator;
	protected int counter = 0;
	
	public Effect(final double ORIGIN_X, final double ORIGIN_Y, final boolean IS_OVAL) {
		this.ORIGIN_X = ORIGIN_X;
		this.ORIGIN_Y = ORIGIN_Y;
		this.IS_OVAL = IS_OVAL;
	}
	
	/**
	 * Updates the effect.
	 */
	public void Update() {}
	
	/**
	 *  Renders the snow to the screen.
	 * @param g Graphics object with which to draw.
	 */
	public void render(final ShapeRenderer SHAPE_RENDERER, final boolean IS_OVAL) {
		if(counter == 0) { System.out.println(PARTICLES.size()); }
		
		Gdx.gl.glEnable(GL30.GL_BLEND);
		Gdx.gl.glBlendFunc(GL30.GL_SRC_ALPHA, GL30.GL_ONE_MINUS_SRC_ALPHA);
		SHAPE_RENDERER.begin(ShapeType.Filled);
		iterator = PARTICLES.iterator();
		while(iterator.hasNext()) {
			iterator.next().render(SHAPE_RENDERER, IS_OVAL);
		}
		SHAPE_RENDERER.end();
		Gdx.gl.glDisable(GL30.GL_BLEND);
	}
	
	/** @return Whether or not to render the particles as ovals. If not then render as squares. */
	public boolean getIsOval() { return IS_OVAL; } 
}

RainbowSnow:

package effect;


import com.badlogic.gdx.graphics.Color;

import particle.Particle;

/**
 * Represents rainbow snow fall.
 * @author Valkryst
 * --- Last Edit 05-Apr-2014
 */
public class RainbowSnow extends Effect {
	/** The length (x-axis) of the screen. */
	private final double SCREEN_LENGTH;
	private final double COLOR_CHANGE_CONSTANT = 0.25;
	private float red = 255, green = 0, blue = 0;
	private boolean isRed = false, isGreen = true, isBlue = false;
	
	/**
	 * Constructs a new Snow object.
	 */
	public RainbowSnow(final double ORIGIN_X, final double ORIGIN_Y, final double SCREEN_LENGTH) {
		super(ORIGIN_X, ORIGIN_Y - 50, false);
		this.SCREEN_LENGTH = SCREEN_LENGTH;
	}
	
	/**
	 * Updates the snow.
	 */
	public void update() {
		Color c = new Color();
		c.set(red/255f, green/255f, blue/255f, 1.0f);

		if(counter == 10) {
			for(int i=0;i<RANDOM.nextInt(1800) + 120;i++) { newParticle(c, RANDOM.nextInt(8), 40); }
			counter = 0;
		} else {
			counter++;
		}
		
		if(isRed) {
			if(blue > 0) { blue -= COLOR_CHANGE_CONSTANT; }

			if(red < 255) {
				red += COLOR_CHANGE_CONSTANT;
			} else if(red == 255 && blue == 0) {
				isRed = false;
				isGreen = true;
			}
		} else if(isGreen) {
			if(red > 0) { red -= COLOR_CHANGE_CONSTANT; }
			
			if(green < 255) {
				green += COLOR_CHANGE_CONSTANT;
			} else if(green == 255 && red == 0) {
				isGreen = false;
				isBlue = true;
			}
		} else if(isBlue) {
			if(green > 0) { green -= COLOR_CHANGE_CONSTANT; }
			
			if(blue < 255) {
				blue += COLOR_CHANGE_CONSTANT;
			} else if(blue == 255 && green == 0) {
				isBlue = false;
				isRed = true;
			}
		}

		iterator = PARTICLES.iterator();
		while(iterator.hasNext()) {
			if(iterator.next().update()) {
				iterator.remove();
			}
		}
	}
	
	/**
	 * Creates a new Particle object.
	 * @param sizeIn The size, in pixels^2, of the new Particle.
	 * @param decayTimeIn The number of movements before the new Particle decays.
	 */
	public void newParticle(final Color COLOR, int SIZE, int LIFE) {
		PARTICLES.add(new Particle(RANDOM.nextInt((int)SCREEN_LENGTH), super.ORIGIN_Y, RANDOM.nextDouble() * (RANDOM.nextBoolean() ? -2 : 2), RANDOM.nextDouble() * -2.5, 0.0050 *(RANDOM.nextBoolean() ? -1 : 1), 0.0, SIZE + RANDOM.nextInt(8), LIFE + RANDOM.nextInt(800), COLOR));
	}
}

Particle:

package particle;

import java.awt.Dimension;
import java.awt.Toolkit;

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

/**
 * Represents a particle in 2D space.
 * @author Valkryst
 * --- Last Edit 05-Apr-2014
 */
public class Particle {
	private final static Dimension SCREEN_DIMENSIONS = Toolkit.getDefaultToolkit().getScreenSize();
	/** The location of the particle on the X-axis. */
	private double x;
	/** The location of the particle on the Y-axis. */
	private double y;
	/** The change in X, per update, of the particle. */
	private double dx;
	/** The change in Y, per update, of the particle. */
	private double dy;
	/** The gravitational pull to the left (negative) and right (positive) acting on this particle. */
	private final double GRAVITY_X;
	/** The gravitational pull to the up (negative) and down (positive) acting on this particle. */
	private final double GRAVITY_Y;
	/** The size in pixels^2 of the particle. */
	private final int SIZE;
	/** The remaining lifetime of the particle. */
	private double currentLife;
	/** The total lifetime of the particle. */
	private final double TOTAL_LIFE;
	/** The color of the particle. */
	private Color color;
	
	/**
	 * Constructs a new particle with the specified data.
	 * @param X The location of the particle on the X-axis.
	 * @param Y The location of the partivcle on the Y-axis.
	 * @param DX The change in X, per update, of the particle.
	 * @param DY The change in Y, per update, of the particle.
	 * @param GRAVITY_X The gravitational pull to the left (negative) and right (positive) acting on this particle.
	 * @param GRAVITY_Y The gravitational pull to the up (negative) and down (positive) acting on this particle.
	 * @param SIZE The size in pixels^2 of the particle.
	 * @param LIFE The remaining lifetime of the particle.
	 * @param COLOR The color of the particle.
	 */
	public Particle(final double X, final double Y, final double DX, final double DY, final double GRAVITY_X, final double GRAVITY_Y, final int SIZE, final double LIFE, final Color COLOR) {
		x = X;
		y = Y;
		dx = DX;
		dy = DY;
		this.GRAVITY_X = GRAVITY_X;
		this.GRAVITY_Y = GRAVITY_Y;
		this.SIZE = SIZE;
		currentLife = LIFE;
		TOTAL_LIFE = LIFE;
		color = COLOR;
	}
	
	/**
	 * Updates the particle's position, change in x, change in y,
	 * remaining lifetime, and color.
	 * @return Whether the particle is 'dead' or not.
	 */
	public boolean update() {
		if(x > SCREEN_DIMENSIONS.width + 32 || x < -32 || y > SCREEN_DIMENSIONS.height + 32) {
			return true;
		} else {
			x += dx;
			y += dy;
			dx += GRAVITY_X;
			dy += GRAVITY_Y;
			currentLife--;
			color.set(color.r, color.g, color.b, (float)(currentLife/TOTAL_LIFE));
			return (currentLife <= 0 ? true : false);
		}
	}
	
	/**
	 * Renders the particle to the screen.
	 * @param G The shaperenderer object to render with.
	 * @param IS_OVAL Whether or not to render the particle as an oval.
	 */
	public void render(final ShapeRenderer SHAPE_RENDERER, final boolean IS_OVAL) {
		SHAPE_RENDERER.setColor(color);

		if(IS_OVAL) {
			SHAPE_RENDERER.circle((int)x-(SIZE / 2), (int)y-(SIZE / 2), SIZE, SIZE); //x-(size/2) & y-(size/2) make sure the particle is rendered at (x, y).
		} else {
			SHAPE_RENDERER.rect((int)x-(SIZE / 2), (int)y-(SIZE / 2), SIZE, SIZE); //x-(size/2) & y-(size/2) make sure the particle is rendered at (x, y).
		}
	}
}

Edit:
Ignore the JavaDocs if they don’t make sense. I haven’t updated them recently.

Fading out and them removing I guess?

If the rgb color values go < 0, they reset to 1.

This could be it.

The rgb colors should never go below 0 with the way I have it set up. I’ll double check that though.

Edit:
It doesn’t seem to be related to the rgb values as far as I’ve seen in my tests.

Disable all texturing and coloring and see if the actual geometry disappears. That way you can rule out a lot of stuff.

Fair enuff, I took a guess as you can probably imagine that amount of code, may as well suggest from my previous experience.

Huh? There are no textures or geometry. I’m just drawing rectangles on the screen using rgba colors and a ShapeRenderer object.

I found your error. Hell that’s a shitty one ;D
I shouldn’t tell you because you shouldn’t code that way :slight_smile:

What do you think that rectangle is? A quad perhaps? Hmm…

Also,

Makes no sense. Just tell him.

A point combined with a width and height that’s sent somewhere as an instruction for which pixels on the screen to color. No idea what a quad is, just started with libGDX a few hours ago.

It took me 1.3 hours to find it, I love debugging. Indeed I just hate to not know the solution ::slight_smile:
So I will go for a quizz ;D
(No that’s not because I am evil, that’s just how some other guy helped me and you learn most)
First hint:
You create an object but you don’t use it every time you create it.

The Color object in the update() method of the RainbowSnow class? Moving that within the if statement gives no change.

Good job, next hint.
add this in Particle.java


lastAlpha =(float)(currentLife/TOTAL_LIFE); //in your update method in Particle

if(lastAlpha != color.a){ //in your render method
		   System.out.println(" DIFF: lastAlpha: " + lastAlpha+ " color.a "+ color.a);
	   }else{
		   System.out.println(" SAME: lastAlpha: " + lastAlpha+ " color.a "+ color.a);
	   }

May I direct you to the recent OGL tut by SHC outlining the basics of what goes on ‘under the covers’ of libGDX:
http://www.java-gaming.org/topics/lwjgl-tutorial-series-the-opengl-rendering-pipeline/32661/view.html

Thanks, I’ll check it out in a few minutes.

hint 3: Your error is pure java

Your methods should look like this


      private float lastAlpha;
   /**
    * Updates the particle's position, change in x, change in y,
    * remaining lifetime, and color.
    * @return Whether the particle is 'dead' or not.
    */
   public boolean update() {
      if(x > SCREEN_DIMENSIONS.width + 32 || x < -32 || y > SCREEN_DIMENSIONS.height + 32) {
         return true;
      } else {
         x += dx;
         y += dy;
         dx += GRAVITY_X;
         dy += GRAVITY_Y;
         currentLife--;
         lastAlpha = (float)(currentLife/TOTAL_LIFE);
         color.set(color.r, color.g, color.b, lastAlpha);
         return (currentLife <= 0 ? true : false);
      }
   }

   /**
    * Renders the particle to the screen.
    * @param G The shaperenderer object to render with.
    * @param IS_OVAL Whether or not to render the particle as an oval.
    */
   public void render(final ShapeRenderer SHAPE_RENDERER, final boolean IS_OVAL) {

	   if(lastAlpha != color.a){
		   System.out.println(" DIFF: lastAlpha: " + lastAlpha+ " color.a "+ color.a);
	   }else{
		   System.out.println(" SAME: lastAlpha: " + lastAlpha+ " color.a "+ color.a);
	   }
	   
	   SHAPE_RENDERER.setColor(color);

      if(IS_OVAL) {
         SHAPE_RENDERER.circle((int)x-(SIZE / 2), (int)y-(SIZE / 2), SIZE, SIZE); //x-(size/2) & y-(size/2) make sure the particle is rendered at (x, y).
      } else {
         SHAPE_RENDERER.rect((int)x-(SIZE / 2), (int)y-(SIZE / 2), SIZE, SIZE); //x-(size/2) & y-(size/2) make sure the particle is rendered at (x, y).
      }
   }

hint 4: Why should the last calculated alpha be different from color.alpha?

Had it slightly different. Yeah, the values are always the same when I change it to how you’ve written it. Still looks like it’s working properly.

Yours should look like:


 DIFF: lastAlpha: 0.9939099 color.a 0.9889625
 DIFF: lastAlpha: 0.9925706 color.a 0.9889625
 DIFF: lastAlpha: 0.9736842 color.a 0.9889625
 DIFF: lastAlpha: 0.9019608 color.a 0.9889625
 DIFF: lastAlpha: 0.97652584 color.a 0.9889625
 DIFF: lastAlpha: 0.875 color.a 0.9889625
 DIFF: lastAlpha: 0.99233127 color.a 0.9889625
 DIFF: lastAlpha: 0.98316497 color.a 0.9889625
 DIFF: lastAlpha: 0.96428573 color.a 0.9889625
 DIFF: lastAlpha: 0.954955 color.a 0.9889625
 DIFF: lastAlpha: 0.9904398 color.a 0.9889625
 DIFF: lastAlpha: 0.99360615 color.a 0.9889625
 DIFF: lastAlpha: 0.9934124 color.a 0.9889625
 DIFF: lastAlpha: 0.99291784 color.a 0.9889625
 SAME: lastAlpha: 0.9889625 color.a 0.9889625



if your snowflakes work properly it should look like:


 SAME: lastAlpha: 0.9868735 color.a 0.9868735
 SAME: lastAlpha: 0.97713095 color.a 0.97713095
 SAME: lastAlpha: 0.9790476 color.a 0.9790476
 SAME: lastAlpha: 0.97393364 color.a 0.97393364
 SAME: lastAlpha: 0.98256737 color.a 0.98256737
 SAME: lastAlpha: 0.92142856 color.a 0.92142856
 SAME: lastAlpha: 0.98253965 color.a 0.98253965
 SAME: lastAlpha: 0.978 color.a 0.978
 SAME: lastAlpha: 0.98509485 color.a 0.98509485
 SAME: lastAlpha: 0.9586466 color.a 0.9586466
 SAME: lastAlpha: 0.97951585 color.a 0.97951585
 SAME: lastAlpha: 0.9859155 color.a 0.9859155
 SAME: lastAlpha: 0.973301 color.a 0.973301
 SAME: lastAlpha: 0.8513514 color.a 0.8513514
 SAME: lastAlpha: 0.95849055 color.a 0.95849055
 SAME: lastAlpha: 0.9763441 color.a 0.9763441
 SAME: lastAlpha: 0.9691877 color.a 0.9691877
 SAME: lastAlpha: 0.7708333 color.a 0.7708333
 SAME: lastAlpha: 0.98286605 color.a 0.98286605
 SAME: lastAlpha: 0.97759676 color.a 0.97759676
 SAME: lastAlpha: 0.97577095 color.a 0.97577095
 SAME: lastAlpha: 0.98214287 color.a 0.98214287
 SAME: lastAlpha: 0.98651963 color.a 0.98651963
 SAME: lastAlpha: 0.7924528 color.a 0.7924528
 SAME: lastAlpha: 0.9822581 color.a 0.9822581
 SAME: lastAlpha: 0.98073554 color.a 0.98073554
 SAME: lastAlpha: 0.98677886 color.a 0.98677886
 SAME: lastAlpha: 0.9808028 color.a 0.9808028
 SAME: lastAlpha: 0.9740566 color.a 0.9740566
 SAME: lastAlpha: 0.97736627 color.a 0.97736627
 SAME: lastAlpha: 0.9832317 color.a 0.9832317
 SAME: lastAlpha: 0.97002727 color.a 0.97002727
 SAME: lastAlpha: 0.976087 color.a 0.976087
 SAME: lastAlpha: 0.959854 color.a 0.959854
 SAME: lastAlpha: 0.9860936 color.a 0.9860936
 SAME: lastAlpha: 0.98410404 color.a 0.98410404
....

So there should be something wrong with your color.

I have the code modified to test with a single snowflake on the screen. The numbers just reduce down to almost 0.0, and before it can get to 0.0 it’s removed just as it should be.

http://pastebin.com/WsGXAp4K

I assume that you’re testing with the unmodified code which would have a ton of snowflakes falling at once and not show the numbers going straight down.

Yes but the code is working.
Just concentrate on the update-method in rainbowSnow :slight_smile:


       Color c = new Color();
       c.set(red/255f, green/255f, blue/255f, 1.0f);
      if(counter == 10) {
         for(int i=0;i<RANDOM.nextInt(1800) + 120;i++) { 
        	 newParticle(c, RANDOM.nextInt(8), 40);
        	 }
         counter = 0;
      } else {
         counter++;
      }

So, how many particles do I get if counter == 10? How many colors do those particles have?

You should have between 120 to 1920 particles every time the counter hits 10. They all use the same color object so 1 color.

Annnd after a second I thought that since they were all referring to the same color object then I just changed it to create a new color object for every single particle and it was fixed. Never had this bug with the non-libGDX version.