Hello JGO Community, I’ve been meaning to create this tutorial for a while now but didn’t know how to start. I’ll start this tutorial by defining, in most basic terms of what I think a “State” is. A State can be defined as a module, a module with a very basic task. It takes some kind of Object or Objects, it performs some operation and it outputs a result.
Unfortunately, a state will need to know what the object is, what its functions are and most importantly what it can do with it. A state can easilly be programmed via extending an abstract class or implementing an interface. It allows for a design that is both neat, readable and somewhat optimal rather than using nested if/else statements or switch statements.
Lets look at a very basic example, not a very useful example but does give an idea of what I mean by a state.
Lets define a simple class called Operator which understands the OperatorState Interface.
public class Operator {
// our operator holds on to a state
// it understands the type that this interface is
private OperatorState state;
public Operator() {
// set a default state
state = new NullState();
}
public int compute(int number1, int number2) {
// our constructor and set method ensures we dont have to check for null states
// it will always be valid
return state.operate(number1, number2);
}
public void setState(final OperatorState state) {
// safeguard against nulls
if (state != null) {
this.state = state;
return;
}
// else, set a neutral state
this.state = new NullState();
}
}
Below is the OperatorState interface
public interface OperatorState {
public int operate(int number1, int number2);
}
And some Basic states that implement the interface itself
public class NullState implements OperatorState {
// this state does nothing, it simply returns an integer of max value
public int operate(int number1, int number2) {
return Integer.MAX_VALUE;
}
}
public class AddState implements OperatorState {
// this state will add the numbers and return a result
public int operate(int number1, int number2) {
return number1 + number2;
}
}
And this is how we use it – a simple program that shows how the system works.
public static void main(String[] args) {
Operator op = new Operator();
// do operation on default
System.out.println(op.compute(5, 6));
// do operation on add state
op.setState(new AddState());
System.out.println(op.compute(5, 6));
// do operation on null state
op.setState(null);
System.out.println(op.compute(5, 6));
}
Again, I apologise for this terrible example. First we create an Operator object which defaults its inner state to the NullState. We do some computations, change the state and do further computations which all returns different result depending on the state. The great thing about this type of design is it gives the Game Programmer some flexibility.
If you were designing an engine, normally you’d enforce on how certain things are done, when using this type of approach, if your Engine does something that your Game Programmer does not like, they can simply implement their own implementation and attach to the specific module, either at start-up or runtime. One great example is that we allow for example the Programmer to switch the “Collision Solver” module, which performs collision detection resolution. By Implementing a custom “CollisionSolver” state, the programmer can attach it at the Collision Step module and replace the default engine implementation, all without digging into the source code of the engine itself.
Ok, now that we somehow understand where I’m going with this design pattern, we are going to implement a simple yet functional Particle module. The specification is simple, the module should allow the creation of various particle effects, it should be simple to use and to program and lastly it should be nice and functional. For the purposes of this tutorial, I will not go into rendering, I assume that you have a 2D renderer all ready to go.
Let us get started, we are going to define some very simple classes which we will need to create this module. The classes are fairly self-explanatory, however I’ll try to comment them as much as possible.
Lets define a Point class. We will use this Point to hold a position x and y.
public class Point {
public float x;
public float y;
// defaults to 0
public Point() {
x = y = 0.0f;
}
public Point(final Point initial) {
this.x = initial.x;
this.y = initial.y;
}
// set the particles
public void set(final float x, final float y) {
this.x = x;
this.y = y;
}
}
Very simple, it has two constructors as well as a set method. The values x and y are public for simplicity. (and also because I’d like to keep this tutorial nice and simple without plaguing the code with get/set methods).
Next up is our Particle interface.
public interface Particle {
public Point getSpawnPoint();
public Point getDimentions();
public Point getPosition();
// the duration of particle - how long has it lived
public float getDuration();
// maximum life of particle before it dies
public float getLife();
// some helper methods
public void setDuration(final float duration);
public void setLife(final float life);
public boolean isAlive();
}
And finally, our Particle2 class will implement this interface
public class Particle2 implements Particle {
// the spawning point of this particle
public Point spawnPos;
// the dimensions of this particle quad
public Point dimensions;
// the position of this particle
public Point position;
// the maximum lifetime of this particle
public float lifetime;
// the duration of this particle
public float duration;
public Particle2() {
spawnPos = new Point();
dimensions = new Point();
position = new Point();
lifetime = 0.0f;
duration = 0.0f;
}
public Point getSpawnPoint() {
return spawnPos;
}
public Point getDimentions() {
return dimensions;
}
public Point getPosition() {
return position;
}
public float getDuration() {
return duration;
}
public float getLife() {
return lifetime;
}
public void setDuration(float duration) {
this.duration = duration;
}
public void setLife(float life) {
this.lifetime = life;
}
public boolean isAlive() {
return duration <= lifetime;
}
}
Now Before we get into implementing the rest of the module, we will need some classes and interfaces for helping us out. I’ve defined a Renderer interface with a function for drawing a Quad at a set position. I will not implement this function and assume the user has implemented it themselves. I’ve also created a small class that can return a random value between two values. I’m not sure it works but if you’re working on a game, you might have something better anyway, so this will do for the purposes of this tutorial.
We have our Renderer Interface
public interface Renderer {
public void renderQuad(final float width, final float height, final Point position);
}
And our Random Number Generator
// for example purposes only - may wish to replace with something more robust.
public class RandomGenerator {
private final Random rand;
public RandomGenerator() {
rand = new Random();
}
// return a random number between x and y
public float randf(float x, float y) {
if (y > x) {
float temp = x;
x = y;
y = temp;
}
// compute a random number between x and y and return
float n = y - x + 1;
float i = rand.nextInt() % n;
return y + i;
}
}
We will use these a little bit later. Now then, let us get to the main event! We will be defining two fairly important Interfaces and one class. We will call the class ParticleBatch which essentially holds a group of particles and can control them. It has an update() function which takes in a deltaTime and a render function which takes in a Renderer object as defined above. So before we implement our ParticleBatch, lets look at the interfaces.
public interface ParticleBatch {
// updates the particle batch
public void update(final float deltaTime);
// renders the particle batch
public void render(final Renderer renderer);
// add a state to this particle batch
public void addState(final ParticleState state);
// start the batch
public void start();
}
The black sheep of this interface is that ParticleState. ParticleState is the simplest interface you’d ever see, and it is also responsible for most of the black magic that our module will be performing! Lets define this mysterious interface.
public interface ParticleState {
// initialise the particles
public void start(final List<Particle> particles);
// update the particles
public void update(final List<Particle> particles, final float deltaTime);
// reset the particles
public void reset(final List<Particle> particles);
}
Three functions, one for starting particles used when you start the ParticleBatch, one for updating the particles and another for resetting the particles.
Before we implement our ParticleBatch class, let us come up with a nice specification or assumption.
As far as particles are concerned, they have two main modes. Either they are alive (visible) or they are dead (invisible).
Our ParticleState has a few assumptions.
- All Particles passed onto the update function MUST be alive.
- All Particles passed onto start and reset MUST be dead.
- Start() function is only called once when ParticleBatch is ran
- Reset() may be called multiple times. You’ll find that most of the time, reset simply does exact same thing as start().
With that simple specification in mind, lets have a look at our crown jewel, the ParticleBatch class.
// the particle batch holds a group of particles and their execution states
public class ParticleBatch2 implements ParticleBatch {
// list of alive particles
private List<Particle> aliveParticles;
// list of dead particles
private List<Particle> deadParticles;
// list of all states
private List<ParticleState> states;
// constructor
public ParticleBatch2(final int particlesNum) {
aliveParticles = new ArrayList<Particle>();
deadParticles = new ArrayList<Particle>();
states = new ArrayList<ParticleState>();
// initially all our particles are dead - add them to the dead list
for (int i = 0; i < particlesNum; i++) {
deadParticles.add(new Particle2());
}
}
// update the states of the particles
public void update(final float deltaTime) {
// first we update the particles
final Iterator<ParticleState> uit = states.iterator();
// update our alive particles
while (uit.hasNext()) {
uit.next().update(aliveParticles, deltaTime);
}
// check to see if particles have died yet
for (int i = 0; i < aliveParticles.size(); i++) {
final Particle p = aliveParticles.get(i);
if (!p.isAlive()) {
// if no longer alive - add to dead list and remove from alive list
deadParticles.add(aliveParticles.remove(i));
}
}
// get iterator for dead particles
if (!deadParticles.isEmpty()) {
final Iterator<ParticleState> dit = states.iterator();
// update our alive particles
while (dit.hasNext()) {
dit.next().reset(deadParticles);
}
}
// dead list has been updated - particles reset - readd to alive list for next update
aliveParticles.addAll(deadParticles);
// clear the dead particles
deadParticles.clear();
}
// render the particles - it is assumed that Renderer is implemented by user
// actual implementation may be different, same idea
public void render(final Renderer renderer) {
// get an iterator
final Iterator<Particle> it = aliveParticles.iterator();
// while we still have alive particles
while (it.hasNext()) {
final Particle p = it.next();
// draw a Quad with particle dimensions width and height at particle position
renderer.renderQuad(p.getDimentions().x, p.getDimentions().y, p.getPosition());
}
}
// add a state to this batch
public void addState(final ParticleState state) {
if (state != null) {
states.add(state);
}
}
// start our particle batch
public void start() {
final Iterator<ParticleState> it = states.iterator();
while (it.hasNext()) {
it.next().start(deadParticles);
}
// updated - add to alive list
aliveParticles.addAll(deadParticles);
// clean the dead Particle list
deadParticles.clear();
}
}
You may question my naming schemes right about now. ParticleBatch2 is for 2D Particle Batching, same with Particle2. So a 3D variant would be called ParticleBatch3 and Particle3 which may or may not have extra functionality.
I’ll try to dissect this class as much as possible. ParticleBatch2 takes an argument that is the amount of particles it is meant to hold. It has 3 lists, one for alive particles, one for dead particles and another for the execution states.
Everything is fairly clear, the main thing happens at the update() function. Lets dissect it a little bit.
First our update function updates our particles
It then checks to see if any particles have died after the update – if they have, they are removed from the alive list and added to the dead list.
The dead list is then reset according to the states and all contents of dead list are re-added to the alive list. - these newly reset particles will execute as normal in next update() call.
So right about now, if you understand my design, you should already visualise of what I’m attempting, if not, then there is some confusion as to how this thing works. Like I mentioned before, all the magic happens in the states which can be programmed to do… well, whatever you want them to do! I’ve pre-implemented 3 states for creating a “rain” effect.
The States are below
// used to spread the particles a bit, we don't want them all in one place!
public class ParticleSpread implements ParticleState {
// where the particle will spawn
final Point minMaxX;
final Point minMaxY;
// a random number generator
final RandomGenerator rand;
public ParticleSpread(final Point minMaxX, final Point minMaxY) {
this.minMaxX = new Point(minMaxX);
this.minMaxY = new Point(minMaxY);
rand = new RandomGenerator();
}
public void start(List<Particle> particles) {
final Iterator<Particle> it = particles.iterator();
// while we still have particles
while (it.hasNext()) {
final Particle p = it.next();
// we want to reset the position of particle to spawn point
p.getSpawnPoint().x = rand.randf(minMaxX.x, minMaxX.y);
p.getSpawnPoint().y = rand.randf(minMaxY.x, minMaxY.y);
}
}
public void update(List<Particle> particles, float deltaTime) {
// ignored
}
public void reset(List<Particle> particles) {
start(particles);
}
}
The ParticleSpread state simply makes sure the particles are spread apart according to some input and some random number generation.
public class Integrator implements ParticleState {
// the velocity to integrate
public Point velocity;
// we accept an acceleration and velocity
public Integrator(final Point vel) {
velocity = new Point(vel);
}
public void start(final List<Particle> particles) {
final Iterator<Particle> it = particles.iterator();
// while we still have particles
while (it.hasNext()) {
final Particle p = it.next();
// we want to reset the position of particle to spawn point
p.getPosition().set(p.getSpawnPoint().x, p.getSpawnPoint().y);
}
}
public void update(final List<Particle> particles, float deltaTime) {
final Iterator<Particle> it = particles.iterator();
// while we still have particles - integrate acceleration with velocity
while (it.hasNext()) {
// update the position of particle with velocity
final Particle p = it.next();
p.getPosition().x += velocity.x * deltaTime;
p.getPosition().y += velocity.y * deltaTime;
// increment duration
p.setDuration(p.getDuration() + deltaTime);
}
}
public void reset(final List<Particle> particles) {
// same as start function
start(particles);
}
}
The Integrator is responsibe for integrating or moving our particles, as an added bonus, it also increments the particles duration.
public class RandomLife implements ParticleState {
private final float maxLife;
final RandomGenerator rand;
public RandomLife(final float maxLife) {
this.maxLife = maxLife;
rand = new RandomGenerator();
}
public void start(final List<Particle> particles) {
final Iterator<Particle> it = particles.iterator();
// while we still have particles
while (it.hasNext()) {
final Particle p = it.next();
// we want to reset the position of particle to spawn point
p.setLife(rand.randf(0, maxLife));
}
}
public void update(final List<Particle> particles, float deltaTime) {
// ignored
}
public void reset(final List<Particle> particles) {
start(particles);
}
}
RandomLife state will give a particle some random life between 0 and maxLife. This will reset ( and a new random life will be chosen) after the lifetime of the particle. It will give effects a bit more variety!
Before ending this tutorial I’d like to mention a few things. This code is provided for illustration purposes only, while it is a fully working implementation (for whatever it is capable of doing) there are a few downfalls so the following areas could use some optimisations.
When adding the particles to the dead particle list, it is not really necessary to remove it from alive list, since they get reset in the same frame anyway, this could save an O(n) time depending on how many particles are dead (since ArrayList needs to shift elements when you remove them).
Consider replacing ArrayList with something else, we use a custom data structure for our system that does not shift elements when removed, instead it keeps a stack of “empty” array cells. When iterating, those cells are simply ignored. Consider something similar, since the order of the particles does not really matter.
Other than those two obvious optimisations, another is to store states in their own lists according to functions, some states may not use the start() or reset() functions, so there is no need to call those.
As long as SpriteBatching is used to render the particles themselves, the only speed optimisations needed would be for the states themselves.
Let us end this lengthy tutorial by providing an example of how to use the system, this particular example will use the states we wrote above to create a “rain” effect of sorts.
public static void main(String[] args) {
// lets have 500 particles for rain
final ParticleBatch rain = new ParticleBatch2(500);
final Renderer renderer = new Renderer2();
// set velocity to -10,-10 (diagonal)
rain.addState(new Integrator(new Point(-10,-10)));
// set max life to 5 seconds
rain.addState(new RandomLife(5.0f));
// set particle spread -400 - 400 in x and -300 - 300 in y
rain.addState(new ParticleSpread(new Point(-400,400), new Point(-300,300)));
// perform initial initialisation
rain.start();
// the frame loop
while (true) {
// poll input events, update game logic etc etc
float deltaTime; // compute deltaTime
// update the particle effect
rain.update(deltaTime);
// render the particle effect
rain.render(renderer);
}
}
Thank you very much for taking the time to read this tutorial, it is probably my biggest one yet. I mainly wanted to push the idea of this specific design pattern, and how it can come in handy in different situations, for both speed, modularity and flexibility. I hope you take something from this and as always, have a nice day!
Any comments, questions or the like are welcomed happily!
~DrHalfway