Particles: Basics to Advanced part 2

Hello. This is the second part of my particles tutorial series

Here is the original link.

And here is the tutorial.

Greetings!

So this will be part 2 of my particle tutorial series. We will be changing the particle class and adding a new class to make things cleaner. We will talk about the many different attributes a particle can have and the many reasons behind these attributes and why we may or may not need them. Let’s just get started.

Requirements:

Finished and understood part 1
A few minutes to read this tutorial

What you get:

A much more complex paritlce
A better example to play with

Now before we write code or change anything we need to talk about particles. Creating effects with particles is very much an art form. You use particles or images that when rendered together, (on top of or next to each other) create some type of effect. Fire. Smoke. Explosions. The problem is that the more particles you have the more processing time it takes to render the effect. Therefor, the goal is to get the best looking effects with the least amount of particles or processing time. Now we need to think of how we want to do this. I like the idea of a fat particle. A big, really fluffy, complex particle that can do a whole lot of stuff. I like this because, you don’t need as many fat particles to create the same effect that would take double or triple the amount of lightweight particles and you don’t need to worry about creating a complex component or inheritance system to make sure you are using the right particle for the job. This is not the best way as there is no “best” way of doing anything really.

Before we change are particle we are going to create a new class. This class will basically be holding two doubles and have a few operations for these two doubles. Sounds very unnecessary right now but it will make everything work a whole lot cleaner as we add more attributes to the particles. The class will be named Vector2d. No, we will not be doing any 3d programming and no we will not do any hard math. We call it Vector2d because it is a vector that holds 2 doubles. Why doubles? Well we want to have more accuracy when it comes to our particles. We want to be able to move them by less than 1. So why not floats? This is because java be default converts everything to doubles when it does math operation. There is no performance boost but it saves us a little time so we don’t have to write that “f” at the end of all our numbers 1f , .045f , 2.5f. If you really want to change it, all it takes is a few Ctrl+fs.

Note: All major 3D languages (like opengl) use floats and not doubles when you need floating point precision. Doubles use a lot more memory and are really not necessary.

Here is the class


public class Vector2d {

	public double x;
	public double y;

	public Vector2d(double num1, double num2){
		this.x = num1;
		this.y = num2;
	}

	public void add(double num){
		x += num;
		y += num;
	}

	public void sub(double num){
		x -= num;
		y -= num;
	}

	public void div(double num){
		x /= num;
		y /= num;
	}

	public void mult(double num){
		x *= num;
		y *= num;
	}

	public void add(Vector2d other){
		x += other.x;
		y += other.y;
	}

	public void sub(Vector2d other){
		x -= other.x;
		y -= other.y;
	}

	public void div(Vector2d other){
		x /= other.x;
		y /= other.y;
	}

	public void mult(Vector2d other){
		x *= other.x;
		y *= other.y;
	}
}

We have our 2 doubles x and y. We named them that because x refers to width and y height. We can use them for things other than width and height.

We make the 2 doubles public so we can access them faster (not in terms of speed but typing). If you want, you can make them private and have a getter.

We have the basic fundamental arithmetic operations. We can pass a number to be applied to the 2 numbers in our Vector2d or by giving an other Vector2d in which case we use that Vectors x for the x value and the y for the y value.

So lets use this in our particle class and see what it looks like.


public class Particle {

	private Vector2d loc;
	private Vector2d vel;
	private Vector2d size;
	private Vector2d life;
	private Color color;


X and y are now one variable called loc which is short for location, dx and dy are now vel which is short for velocity, (getting physicsy there), size is still named size but now we have a width and height, and life is life but we can have a max life and current life.

Let’s add some more stuff to our particle. I want acceleration. Wind/gravity. I want it so our particle can grow or shrink and I want to have a max size our particle can be.


public class Particle {

	private Vector2d loc;
	private Vector2d vel;
	private Vector2d acc;
	private Vector2d size;
	private Vector2d maxSize;
	private Vector2d growth;
	private Vector2d life;
	private Color color;


Nice. Lets change the update and render methods. First, the hard one, updating. Now we need to know what happens when our particles do certain things. What happens when they reach maxSize? What happens when their size is 0? When the max size is hit we could stop growing, start shrinking, or die. Or…we could do all of them. Yeah, lets do all of them. We will have 2 booleans, ultSize and default. If default is true, we simply kill particles if they hit max size or 0. If ultSize is true and we reach max size, we stop growing and if we hit 0, stop shrinking. If false and we reach max size, we will reverse our growth and shrink and if we reach 0, we reverse and grow. Simple. Now lets actually implement all this.
And here is the new update()


public boolean update(){
		vel.add(acc);
		loc.add(vel);
		size.add(growth);
		life.x--;
		
		if(life.x <= 0)
			return true;
		
		if(defaultSize){
			if(size.x >= maxSize.x){
				if(size.y >= maxSize.y)
					return true;
				else
					size.x = maxSize.x;
			}
			if(size.y >= maxSize.y) //Note: we already checked if both x and y are bigger. 
				size.y = maxSize.y;
			if(size.x <= 0)
				if(size.y <= 0)
					return true;
				else
					size.x = 1;
			if(size.y <= 0)
				size.y = 1;
			return false; // we are done
		}
		
		if(ultSize){ // We will shrink and grow back and forth
			if(size.x > maxSize.x){
				size.x = maxSize.x;
				growth.x *= -1;
			}
			if(size.y > maxSize.y){
				size.y = maxSize.y;
				growth.y *= -1;
			}
			if(size.x <= 0){
				size.x = 1;
				growth.x *= -1;
			}
			if(size.y <= 0){
				size.y = 1;
				growth.y *= -1;
			}
		}
		else{ //We stop growing or shrinking. 
			if(size.x > maxSize.x)
				size.x = maxSize.x;
			if(size.y > maxSize.y)
				size.y = maxSize.y;
			if(size.x <= 0)
				size.x = 1;
			if(size.y <= 0)
				size.y = 1;
		}
		return false;
	}

Finally, lets change the render code. We just need to make it use the new Vector2d size and cast to ints.

Here is render()


	public void render(Graphics g){
		Graphics2D g2d = (Graphics2D) g.create();

		g2d.setColor(color);
		g2d.fillRect((int)(loc.x-(size.x/2)), (int)(loc.y-(size.y/2)), (int)size.x, (int)size.y);

		g2d.dispose();
	}

Not bad. Now lets add some setters and getters for our particle variables.

Here is our new and improved Particle.


import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;

public class Particle {

	private Vector2d loc;
	private Vector2d vel;
	private Vector2d acc;
	private Vector2d size;
	private Vector2d maxSize;
	private Vector2d growth;
	private Vector2d life;
	private Color color;

	private boolean ultSize = false;
	private boolean defaultSize = false;

	public Particle(double x, double y, double dx, double dy, double size, double life, Color c){
		this.loc = new Vector2d(x,y);
		this.vel = new Vector2d(dx,dy);
		this.acc = new Vector2d(0,0);
		this.life = new Vector2d(life,life);
		this.size = new Vector2d(size,size);
		this.growth = new Vector2d(0,0);
		this.maxSize = new Vector2d(0,0);
		this.color = c;
	}

public boolean update(){
		vel.add(acc);
		loc.add(vel);
		size.add(growth);
		life.x--;
		
		if(life.x <= 0)
			return true;
		
		if(defaultSize){
			if(size.x >= maxSize.x){
				if(size.y >= maxSize.y)
					return true;
				else
					size.x = maxSize.x;
			}
			if(size.y >= maxSize.y) //Note: we already checked if both x and y are bigger. 
				size.y = maxSize.y;
			if(size.x <= 0)
				if(size.y <= 0)
					return true;
				else
					size.x = 1;
			if(size.y <= 0)
				size.y = 1;
			return false; // we are done
		}
		
		if(ultSize){ // We will shrink and grow back and forth
			if(size.x > maxSize.x){
				size.x = maxSize.x;
				growth.x *= -1;
			}
			if(size.y > maxSize.y){
				size.y = maxSize.y;
				growth.y *= -1;
			}
			if(size.x <= 0){
				size.x = 1;
				growth.x *= -1;
			}
			if(size.y <= 0){
				size.y = 1;
				growth.y *= -1;
			}
		}
		else{ //We stop growing or shrinking. 
			if(size.x > maxSize.x)
				size.x = maxSize.x;
			if(size.y > maxSize.y)
				size.y = maxSize.y;
			if(size.x <= 0)
				size.x = 1;
			if(size.y <= 0)
				size.y = 1;
		}
		return false;
	}

	public void render(Graphics g){
		Graphics2D g2d = (Graphics2D) g.create();

		g2d.setColor(color);
		g2d.fillRect((int)(loc.x-(size.x/2)), (int)(loc.y-(size.y/2)), (int)size.x, (int)size.y);

		g2d.dispose();
	}

	public void setLoc(double x,  double y){
		loc.x = x;
		loc.y = y;
	}

	public void setVel(double x,  double y){
		vel.x = x;
		vel.y = y;
	}

	public void setAcc(double x,  double y){
		acc.x = x;
		acc.y = y;
	}

	public void setSize(double x,  double y){
		size.x = x;
		size.y = y;
	}

	public void setMaxSize(double x,  double y){
		maxSize.x = x;
		maxSize.y = y;
	}

	public void setGrowth(double x,  double y){
		growth.x = x;
		growth.y = y;
	}

	public void setLife(double num){
		life.x = num;
		life.y = num;
	}

	public void setSizeDeault(boolean c){
		defaultSize = c;
	}

	public void setUltSize(boolean c){
		defaultSize = false;
		ultSize = c;
	}

	public Vector2d getLoc(){
		return loc;
	}

	public Vector2d getVel(){
		return vel;
	}

}

Me like. So now it is time to test it. I have edited the test program a little. I have added some random methods. One returns a positive or negative number and the other returns just a positive. I have added a Color, c, so we can change the color of the spawned particles. I have added a boolean so we can pause the test. I have added a KeyListener that we can change stuff with. Pressing “p” pauses and pressing space will change the color. I made the window realizable and finally made it so the particle will bounce off the sizes of the window. Look at it a few times and then run it. After that play with it and the particles. Try changing the way they grow or shrink. Mess around. And think about some other things you want a particle to do and how you would go about making emitters to emit the particles.

The test program



import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.JFrame;

public class Window extends JFrame {

	private ArrayList particles = new ArrayList(500);

	private int x = 0;
	private int y = 0;
	private BufferStrategy bufferstrat = null;
	private Canvas render;

	private Random rnd = new Random(); //used to generate random numbers
	private Color c = Color.blue; // the default particle color
	private boolean running = true; // should we update?

	public static void main(String[] args)
	{
		Window window = new Window(450, 280, "Particles: ");
		window.pollInput();
		window.loop();
	}

	public Window( int width, int height, String title){
		super();
		setTitle(title);
		setIgnoreRepaint(true);
		setResizable(true);

		render = new Canvas();
		render.setIgnoreRepaint(true);
		int nHeight = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
        int nWidth = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
        nHeight /= 2;
        nWidth /= 2;

        setBounds(nWidth-(width/2), nHeight-(height/2), width, height);
		render.setBounds(nWidth-(width/2), nHeight-(height/2), width, height);

		add(render);
		pack();
		setVisible(true);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		render.createBufferStrategy(2);
		bufferstrat = render.getBufferStrategy();
	}

	public void pollInput()
	{
		render.addMouseListener(new MouseListener(){

			public void mouseClicked(MouseEvent e) {
				addParticle();addParticle();addParticle();
				addParticle();addParticle();addParticle();
			}

			public void addParticle(){//play with this method
				Particle p = new Particle(x,y,0,0,0,0,c);
				p.setVel(random(7),random(7));
				p.setAcc(random(.02),random(.02));
				p.setLife(randomPlus(150)+150);
				p.setSize(randomPlus(25)+25, randomPlus(25)+25);
				p.setMaxSize(50,50);
				p.setGrowth(random(2), random(2));
				p.setUltSize(true);
				particles.add(p);
			}

			public void mouseEntered(MouseEvent e) {

			}

			public void mouseExited(MouseEvent e) {

			}

			public void mousePressed(MouseEvent e) {

			}

			public void mouseReleased(MouseEvent e) {

			}

		});

		render.addKeyListener(new KeyListener(){//keylistener

			public void keyPressed(KeyEvent e) {
				int code = e.getKeyCode();
				if(code == 'P'){
					if(running)
						running = false;
					else
						running = true;
				}
				if(code == ' '){//new random color
					c = new Color((int)randomPlus(255),(int)randomPlus(255),(int) randomPlus(255));
				}
			}

			public void keyReleased(KeyEvent e) {

			}

			public void keyTyped(KeyEvent e) {

			}

		});
	}

	public double random( double num ){//random method may not be the best
    	return (num * 2)  * rnd.nextDouble() - num;
    }

	public double randomPlus( double num ){//return only a positive number
		double temp = ((num * 2)  * rnd.nextDouble()) - num;
    	if( temp < 0 )
    		return temp * -1;
    	else
    		return temp;
    }

	//This is a bad game loop example but it is quick to write and easy to understand
	//If you want to know how to do a good one use the all knowing google.
	public void loop(){
		while(true){
			if(running)
				update();
			render();

			try {
				Thread.sleep(1000/60);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

        public void update(){
		Point p = render.getMousePosition();
		if(p !=null ){
			x = p.x;
			y = p.y;
		}
		for(int i = 0; i <= particles.size() - 1;i++){
			Particle part = particles.get(i);
			if(part.update())
				particles.remove(i);	
			if(part != null){
				if(part.getLoc().x <= 0){
					part.getLoc().x = 0;
					part.getVel().x *= -.8;
				}
				if(part.getLoc().x >= render.getWidth()){
					part.getLoc().x = render.getWidth();
					part.getVel().x *= -.8;
				}
				if(part.getLoc().y <= 0){
					part.getLoc().y = 0;
					part.getVel().y *= -.8;
				}
				if(part.getLoc().y >= render.getHeight()){
					part.getLoc().y = render.getHeight();
					part.getVel().y *= -.8;
				}
			}
		}
	}
	public void render(){
		do{
			do{
				Graphics2D g2d = (Graphics2D) bufferstrat.getDrawGraphics();
	            g2d.fillRect(0, 0, render.getWidth(), render.getHeight());

	            renderParticles(g2d);

				g2d.dispose();
	         }while(bufferstrat.contentsRestored());
	          bufferstrat.show();
		}while(bufferstrat.contentsLost());
	}

	public void renderParticles(Graphics2D g2d){
		for(int i = 0; i <= particles.size() - 1;i++){
			particles.get(i).render(g2d);
		}
	}
}

Here are some screens of these particles in action.

http://postimage.org/image/ljk7efvvn/

http://postimage.org/image/ds3hfvrqb/

(guess cant post images in a article)

Here are a few fun settings for your particles. Just copy it over the addParticle method.


public void addParticle(){//play with this method
				Particle p = new Particle(x,y,0,0,0,0,c);
				p.setVel(random(4),random(4));
				p.setAcc(0,randomPlus(.2)+.1);
				p.setLife(randomPlus(150)+150);
				p.setSize(25, 25);
				p.setMaxSize(25,25);
				p.setGrowth(-randomPlus(.2)-.5, -randomPlus(.2)-.5);
				p.setSizeDeault(true);
				//p.setUltSize(false);
				particles.add(p);
			}


	public void addParticle(){//play with this method
				Particle p = new Particle(x+random(32),y+random(32),0,0,0,0,c);
				p.setVel(random(1),random(1));
				p.setAcc(0,-randomPlus(.04)-.02);
				p.setLife(randomPlus(150)+550);
				p.setSize(16, 16);
				p.setMaxSize(25,25);
				p.setGrowth(-.1, -.1);
				p.setSizeDeault(true);
				//p.setUltSize(false);
				particles.add(p);
			}