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.
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
Leave a Reply