ECS Pattern

Welcome to part 4 of the Design Patterns series.
This episode will be about the ECS (Entity Component System) Pattern

Entity Component System

Entity-Component–System (ECS) is an architectural pattern. This pattern is widely used in game application development. ECS follows the composition over the inheritance principle, which offers better flexibility and helps you to identify entities where all objects in a game’s scene are considered an entity.

Entity Component System is a very well known Pattern because of Unity3D game engine.
But to my surprising not every unity developer knows about it. I think that’s because they never really made one there selves!

Entity Component System is mostly used in Game Development. This is mainly because games often are a bunch of entities with one or more components.

Examples

Computers!
A computer is a great example of a ECS. The case is the entity with all parts inside as components. Some components rely on each other and some boost each other. But you can always remove and/or add components.

Boxes
An ECS is also a pile of boxes with the entity as the outer box, each box inside will be components. Each box has it’s own behaviour and it’s own data stored and manipulated. But the entity (the outer box) is the access point and carries them all.

Intentions of using ECS Pattern

  • Separation of responsibilities
  • Reusability and flexibility
  • Clean architecture

Demo

Demo introduction

In this demo we will continue with where we left previous episode!
But will also clean up some code!
By using ECS we can fully clean up our Missiles which had all their components responsible in it’s main class. Which is not really maintainable. Because we don’t want to rewrite code in the future episodes. 😉

In steps:
  • Develop entity and component base classes
  • Transform current code to entities and components
  • Create components for our turrets
  • Add turrets and a turret factory

Code

Entity Component System

Entity.java
package ecs;

import java.util.List;
import java.awt.Graphics2D;
import java.util.ArrayList;

public class Entity {
	// storing all our components
	private List<Component> components = new ArrayList<>();
	
	// Accessing a component by getting it from our stored components
	// list
	public <T> T getComponent(Class<T> c) throws IllegalArgumentException {
		for (Component component : components) {
			if(c.isInstance(component)) {
				return c.cast(component);
			}
		}
		
        throw new IllegalArgumentException("Component not found " + c.getName());
	}
	

	// Adding a component to our list
    public void addComponent(Component c) {
        if (c.getEntity() != null) {
            throw new IllegalArgumentException("component already attached an entity");
        }
        
        components.add(c);
        c.setEntity(this);
    }
	

	// Removing component from our list
    public <T> T removeComponent(Class<T> c) throws IllegalArgumentException {
    	Component result = null;
		for (Component component : components) {
			if(c.isInstance(component)) {
				result = component;
			}
		}
		
		if(result != null) {
			components.remove(result);
			return c.cast(result);
		}
		
		
        throw new IllegalArgumentException("Component not found " + c.getName());
	}
    
    // asking if this entity contains a certain component
    public boolean hasComponent(Class<?> clazz) {    
        for (Component c : components) {
            if (clazz.isInstance(c)) {
                return true;
            }
        }
        return false;
    }
    
    // update all components
    public void update(double deltaTime) {
    	for (Component component : components) {
			if(component.isActive()) {
				component.update(deltaTime);
			}
		}
    }
    
    //render all components
    public void render(Graphics2D g) {
    	for (Component component : components) {
			if(component.isActive()) {
				component.render(g);
			}
		}
    }
}
Component.java
package ecs;

import java.awt.Graphics2D;

public abstract class Component {

	// to toggle it's activity
	private boolean active = true;
	
	// to access entity in components
	// we use this to access other
	// components within components
    protected Entity entity;

    public boolean isActive() {
    	return active;
    }
    
    public void setEntity(Entity e) {
        entity = e;
        init();
    }
    
    public Entity getEntity() {
        return entity;
    }
    
    // must have methods
    // each components initializes (needed when willing to access/store
    // other components
    public abstract void init();
    
    // each component calls update
    public abstract void update(double deltaTime);
    
    // each component calls render
    public abstract void render(Graphics2D g);
}

Creating components from current code

If we break down our current Missile code from previous episode (Click here to see) we’ll see that the Missile has lots of responsibilities.

  • Storing and manipulating position
  • Rendering image
  • Falling down

This must be split into a Transform, Image and Gravity.

Transform.java

Transform might be familiar to you from Unity3D engine, where each object contains a Transform. So it will do in our Missile Command game! We’ll create a Transform Component to store our position data.

package components;

import java.awt.Graphics2D;

import ecs.Component;
import math.Vector2;

public class Transform extends Component {
	
	// x and y position
	public Vector2 position;
	
	// transform rotation
	public float rotation;
	
	// adding transform on position x=0 and y=0
	public Transform() {
		 position = new Vector2();
	}
	
	// defining position through constructor
	public Transform(float x, float y) {
		 position = new Vector2(x, y);
	}

	// will not use any of the super methods, so they'll stay empty
	@Override
	public void init() {
	}

	@Override
	public void update(double deltaTime) {
	}

	@Override
	public void render(Graphics2D g) {
	}
}

A very basic component but very useful for readability!

Image.java

In our image component we’ll have full responsibility of rendering our entity defined sprite. It will do this in combination with Transform for positioning and rotation data!

package components;

import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;

import ecs.Component;
import math.Vector2;

public class Image extends Component {
	
	// sprite to render
	private BufferedImage sprite;
	// caching transform component
	private Transform transform;

	// render data
	private int width;
	private int height;
	
	private int renderWidth;
	private int renderHeight;
	
	// pivot to change render central position
	// default pivot is 0, 0 (top left corner)
	private Vector2 pivot = new Vector2(0.5f, 0.5f);
	
	public Image(BufferedImage sprite) {
		this.sprite = sprite;

		// width and height from sprite
		width = this.sprite.getWidth();
		height = this.sprite.getHeight();
		
		// render width and height to be able to modify
		// size of image
		renderWidth = width;
		renderHeight = height;
	}
	
	public void setPivot(float x, float y) {
		pivot.x = x;
		pivot.y = y;
	}

	@Override
	public void init() {
		// caching transform by getting component from entity
		transform = entity.getComponent(Transform.class);
	}
	
	public void setScale(float scale) {
		// changing size of image by scaling original size
		// and storing in local variable
		renderWidth = (int) (width * scale);
		renderHeight = (int) (height * scale);
	}

	@Override
	public void update(double deltaTime) {
	}

	@Override
	public void render(Graphics2D g) {
		if(sprite == null) {
			return;
		}
		// cache original render transform
		AffineTransform originalTrans = g.getTransform();

		g.rotate(transform.rotation, transform.position.x , transform.position.y);
		
		// calculating pivot position
		int x = (int) (transform.position.x - (renderWidth * pivot.x));
		int y = (int) (transform.position.y - (renderHeight * pivot.y));
		g.drawImage(sprite, x, y, renderWidth, renderHeight, null);

		// set transform back so not all images are rotated.
		g.setTransform(originalTrans);
	}
}

Pivoting is normally from left top corner as shown in this image:

By changing it to 0.5 we move the central position to the centre of our spirte.

Gravity.java
package components;

import java.awt.Graphics2D;

import ecs.Component;
import math.Vector2;

public class Gravity extends Component {

	private Transform transform;
	private float speed;
	
	// defining gravity speed in constructor
	public Gravity(float speed) {
		this.speed = speed;
	}
	
	@Override
	public void init() {
		transform = entity.getComponent(Transform.class);
	}

	@Override
	public void update(double deltaTime) {
		// modifying transform position to fall down
		Vector2 pos = transform.position;
		pos.y += (float)(speed * deltaTime);
		
		transform.position = pos;
	}

	@Override
	public void render(Graphics2D g) {
	}
}

Applying Entity and Components to Missile

Missile.java

Now we have all previous logic split from our Missile into components,

package missile;

import java.awt.image.BufferedImage;

import ecs.Entity;
import math.Vector2;
import components.Gravity;
import components.Image;
import components.Transform;

public abstract class Missile extends Entity {
	
	private int score;
	
	private boolean stopped;

	private MissileType creationType; // used to recreate

	public Vector2 getPos() { return getComponent(Transform.class).position; }

	public int getScore() { return score; }

	public boolean isStopped() { return stopped; }
	
	public MissileType getCreationType() { return creationType; }
	
	// Constructor
	public Missile(float x, float y, float speed, int score, BufferedImage sprite) {
		// adding components
		// transform for positioning
		addComponent(new Transform(x, y));
		// image for rendering sprite
		addComponent(new Image(sprite));
		// gravity for falling down
		addComponent(new Gravity(speed));
	}
	
	// set creation type which is used to recreate this missile
	public void setCreationType(MissileType type) {
		creationType = type;
	}
	
	// scale missile
	protected void setScale(float scale) {
        // get our image component and change scale
		getComponent(Image.class).setScale(scale);
	}
	
	// update missile to move it to the ground
	public void update(double deltaTime) {
		// update all components
		super.update(deltaTime);
		
		// check if reached the ground
		Transform transform = getComponent(Transform.class);
		Vector2 pos = transform.position;
		if(pos.y >= 500) {
			pos.y = 500;
			
			stopped = true;
			// stop moving and force position
			transform.position = pos;
		}
	}
}

Now we have a very clean Missile class with all main responsibilities split in components but still checking if it hits the ground.

Turret component

Our turrets will shoot towards our mouse position. For this episode we will only make the barrel follow our mouse position, so therefore we’ll create a RotateToMouse component!

RotateToMouse.java
package components;

import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

import ecs.Component;
import main.GameWindow;

public class RotateToMouse extends Component implements MouseMotionListener {

	// catching transform to change rotation
	private Transform transform;
	
	@Override
	public void init() {
		transform = entity.getComponent(Transform.class);
		
		// listen to mouse events
		GameWindow.instance.addMouseMotionListener(this);
	}

	@Override
	public void mouseMoved(MouseEvent e) {
		// calculate position difference between me and mouse
		float dx = transform.position.x -  e.getX();
		float dy = transform.position.y - e.getY();
		
		// get radiant rotation by atan our y and x position differences
		transform.rotation = (float) Math.atan2(dy, dx);
		transform.rotation += Math.toRadians(90);
	}

	// ignoring these
	@Override
	public void update(double deltaTime) {;
	}

	@Override
	public void render(Graphics2D g) {
	}

	@Override
	public void mouseDragged(MouseEvent e) {
	}
}

Rotation calculations are done by using atan2 between our mouse and current position.

A simple diagram showing the angle returned by atan2(y, x)

More info about this calculation here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2

Creating our turrets

Our turrets will be based of a “base” and a “barrel”. The base will stay put but the barrel will rotate towards our mouse position.

To show again the beauty of factories, I made a simple factory which will only return a different turret for the middle position(s). (Will be two positions for even count and one position for odd count)

TurretBarrel.java
package turret;

import java.awt.image.BufferedImage;

import components.Image;
import components.RotateToMouse;
import components.Transform;
import ecs.Entity;

public class TurretBarrel  extends Entity {
	
	public TurretBarrel(float x, float y, BufferedImage base) {
		addComponent(new Transform(x, y));
		addComponent(new Image(base));
		addComponent(new RotateToMouse());
		
		// base pivoting
		setPivot(0.5f, 0f);
	}
	
	public void setPivot(float x, float y) {
		getComponent(Image.class).setPivot(x, y);
	}
	
	@Override
	public void update(double deltaTime) {
		// update components
		super.update(deltaTime);
	}
}

Pivoting rotation is very important for our barrel so it looks like it’s still attached to the base.

TurretBase.java
package turret;

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;

import components.Image;
import components.Transform;
import ecs.Entity;

public abstract class TurretBase extends Entity {
	
	protected TurretBarrel turretBarrel;
	
	// passing both base image and barrel image, so both are flexible
	// changable for our subclasses
	public TurretBase(float x, float y, BufferedImage base, BufferedImage barrel) {
		// place barrel in middle of turret
		turretBarrel = new TurretBarrel(x, y, barrel);
		
		// add position for our base
		addComponent(new Transform(x, y));
		
		// render base sprite
		addComponent(new Image(base));
	}
	
	@Override
	public void update(double deltaTime) {
		super.update(deltaTime);
		turretBarrel.update(deltaTime);
	}
	
	@Override
	public void render(Graphics2D g) {
		// render barrel first so it's behind the base
		turretBarrel.render(g);
		
		super.render(g);
	}
}
TurretSquare.java

Squared turrets are the outer turrets in our demo.

package turret;

import sprite.Sprites;

public class TurretSquare extends TurretBase {

	public TurretSquare(float x, float y) {
        // squared base with a red barrel
		super(x, y, Sprites.instance.getTurretSquare(), Sprites.instance.getTurretBarrelRed());
	}
}
TurretCircle.java

Central turrets in our demo

package turret;

import sprite.Sprites;

public class TurretCircle extends TurretBase {

	public TurretCircle(float x, float y) {
		// circled base with a white barrel
		super(x, y, Sprites.instance.getTurretCircle(), Sprites.instance.getTurretBarrelWhite());
		
		// changing pivot so the barrel sticks out more than normal 
		turretBarrel.setPivot(0.5f,  -.5f);
	}
}
TurretFactory.java

In this turret factory we’ll instantiate a turret based on it’s index. When the index is the middle of the size, we’ll return and instantiate a round turret and otherwise we’ll return and instantiate a squared turret.

package turret;

public class TurretFactory {
  
	public TurretBase getTurret(float x, float y, int index, int size) {
		if(size > 2) {
			// get middle of our size
			// - 1 because we count from 0 ;)
			float middle = (size - 1) / 2f;
			
			// check ceil and floored numbers
			// ceil as in ceiling and floor as in floored
			// so example size is 6
			// middle is 2.5
			// middle indexes will be 2 and 3
			// square, square, circle, circle, square, square
			if(index == Math.floor(middle) || index == Math.ceil(middle)) {
				return new TurretCircle(x, y);
			} else {
				return new TurretSquare(x, y);
			}
		} else {
			// default
			return new TurretSquare(x, y);
		}
	}
}

Adding turrets to our gamefield

Now we’ve got created all our turrets, factory and components. All we must do is creating turrets to render and update!

GameLoop.java
...
import sprite.Sprites;
import turret.TurretBase;
import turret.TurretFactory;

public class GameLoop  extends Loop {

    // amount of turrets we want to display.
	private static final int turretCount = 6;
	
	// to keep track of our turrets
	private ArrayList<TurretBase> turrets = new ArrayList<TurretBase>();

	...
	
	@Override
	public void init() {
		super.init();
		
		Sprites sprites = new Sprites();
		sprites.Init(); // creates singleton instance and sprites
		
		...
		
		TurretFactory turretFactory = new TurretFactory();
		int turretCount = 6;
		for (int i = 0; i < turretCount; i++) {
			// calculating x position with offset based on amount
			// and frame width
			float x = ((Main.width / turretCount) / 2) + i * (Main.width / turretCount);
			turrets.add(turretFactory.getTurret(x, 500, i, turretCount));
		}
	}
	
	@Override
	public void tick(double deltaTime) {
		...
		
        // update turrets
		for (TurretBase turret : turrets) {
			turret.update(deltaTime);
		}
		
		...
	}
	
	@Override
	public void render() {
		...
        
        // render turrets
		for (TurretBase turret : turrets) {
			turret.render(graphics2D);
		}
      
        ...
	}
  ...
}

The dots indicating code which is written in previous episode to highlight what’s new. If wanting to have full code, check out previous episode or GitHub link below!

Source code – GitHub

You can get all source codes here!

https://github.com/jscotty/DesignPatterns/tree/ECSPattern

Breakdown

This was quite a post. But ECS is quite an impressive and maybe little bit advanced pattern! But oh so useful.

We split up our entire missile object into little pieces, which was already super useful! As with the turrets, we didn’t have to care about calling rendering, caching position again and all sorts of things! Everything was already set up with an Image and Transform component. All we had to do was passing the data.

If we compare our missile class before and after. You’ll notice it’s much much more cleaner now!
There’s much less caching going on, and almost no logic apart from the positioning check.

This also helps a lot for scalability!
We simple have to add or remove components to change the whole behaviour of each entity!

For example!
Remember the RotateToMouse component?
Simply add it to our Missile class and see what happens! 😉

As you can see, all the missiles are now rotating towards our mouse position too. Just like the turret barrels.

You can do this for example some power up missiles which you want to give eyes looking at the mouse position. 😄

Resources

ECS is quite a big topic and I tried to explain it as simple as possible. But see here some resources which are great if you want to know everything about it and/or dive deeper into it.

The Cherno

A great in depth video about adding ECS to a C++ game engine.

Board To Bits Games

A great overview about what ECS is and how it’s compared to Unity’s usage of ECS.

Unity docs

The images used in the beginning are from Unity documentation.
https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/


Hope you enjoyed this episode!
How well did you know about ECS? And did you already used ECS by building it yourself?
I hope this episode gave you an overview of how it works and how to use it properly!

If any questions, feel free to message me on Instagram @justinbieshaar or in comments below!

Happy coding everyone! 👨‍💻

Greetings,
Justin Scott

Follow Justin Scott:

I love to learn and share. - 01001010 01010011 01000010

Latest posts from

Leave a Reply

Your email address will not be published. Required fields are marked *