Welcome to part 6 of the Design Patterns series. This episode will be about the Mediator Pattern!
Mediator Pattern
Mediator is a behavioral design pattern that lets you reduce chaotic dependencies between objects. The pattern restricts direct communications between the objects and forces them to collaborate only via a mediator object.
https://refactoring.guru/design-patterns/mediator
In games once you start to have multiple entities flowing around your scene, it starts to become pretty chaotic. All entities will have to watch each other and manipulate each other causing a lot of dependencies.
This is when mediators start to be a perfect fit.
Examples
Traffic Lights
Just like in real life, the public roads are a chaotic mess once there’s no traffic light. Even though there are still rules, not everyone listens to them or forgot to pay attention. This causes a lot of blockages, accidents, annoyance and headaches.
A traffic light is a great mediation to tackle these problems. It tells each individual what to do and manages them all. Also does it have some logic to keep the traffic flow as efficient as possible.
ATC tower
Every airport has an ATC (Air Traffic Controller) tower mediating each vehicle to tell whether it can land or not. But also when it can depart or has to be delayed.
It’s the manager of all aircraft, big to small and planes to choppers.
Intentions of using Mediator Pattern
- Handling communication between related objects
Demo
Demo introduction
For this demo we will add bullets to our turrets!
But we will also create mediators for our missiles and turrets.
The turret mediator will communicate with the bullet mediator in order to fire. But the bullet mediator will communicate back with the missile mediator to decide what to do when they hit each other! But more about that next episode π because we really need observers for that.
Code
Disclaimer: 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!
Bullets
MoveToPosition.java
For this demo, we want the bullets to move to the position we click. So, to do this, we must have a new component which moves the entity (bullet) to a defined position!
package components; import java.awt.Graphics2D; import ecs.Component; import math.Vector2; public class MoveToPosition extends Component { // cached component private Transform transform; // target to move to private Vector2 target; // speed till reached target private float speed; public MoveToPosition(float x, float y, float speed) { target = new Vector2(x, y); this.speed = speed; } public MoveToPosition(Vector2 target, float speed) { this.target = target; this.speed = speed; } @Override public void init() { transform = entity.getComponent(Transform.class); } @Override public void update(double deltaTime) { // moving towards the direction we calculate in Vector class Vector2 direction = transform.position.direction(target); // adding position transform.position.x += direction.x * speed * deltaTime; transform.position.y += direction.y * speed * deltaTime; if(Vector2.getDistanceD(transform.position, target) <= 1.5f) { // checking distance to clamp our position // otherwise it will keep shaking on it's position. // the amount of shake will vary on the speed you choose! transform.position = target; } } @Override public void render(Graphics2D g) { } }
Bullet.java
The actual bullet!
It’ll just be a white dot.
package bullet; import components.Image; import components.MoveToPosition; import components.Transform; import ecs.Entity; import math.Vector2; import sprite.Sprites; public class Bullet extends Entity { private static final float speed = 2f; public Bullet(float x, float y, Vector2 target) { addComponent(new Transform(x, y)); addComponent(new Image(Sprites.instance.getBullet())); addComponent(new MoveToPosition(target, speed)); } }
This really shines the ECS from two episodes again doesn’t it?
Such a simple class for a fun and big addition to our game!
BulletMediator.java
package bullet; import java.awt.Graphics2D; import java.util.ArrayList; import java.util.List; import missile.MissileMediator; import turret.TurretMediator; public class BulletMediator { // all bullets stored private List<Bullet> bullets = new ArrayList<>(); private MissileMediator missileMediator; public BulletMediator(MissileMediator missileMediator) { // caching missile mediator for later use ;) this.missileMediator = missileMediator; } // adding, updating, rendering and disposing // bullets. Must be very much self explaining :) public void addBullet(Bullet bullet) { bullets.add(bullet); } public void update(double deltaTime) { for (int i = 0; i < bullets.size(); i++) { Bullet bullet = bullets.get(i); bullet.update(deltaTime); } } public void render(Graphics2D g) { for (int i = 0; i < bullets.size(); i++) { Bullet bullet = bullets.get(i); bullet.render(g); } } public void dispose() { for (int i = 0; i < bullets.size(); i++) { Bullet bullet = bullets.get(i); bullet.dispose(); } bullets.clear(); } }
Our first mediator! Even though it’s just a manager storing, updating and managing our bullets.
Let’s fire our bullets!
TurretBarrel.java
Of course we will shoot from our barrel!
package turret; ... public class TurretBarrel extends Entity { ... public Bullet shoot(Vector2 target) { Vector2 position = getComponent(Transform.class).position; // get direction from position towards target Vector2 direction = position.direction(target); // add direction so it's aligned with the barrel. // * 5 to make it start lower float x = position.x + direction.x * 5; float y = position.y + direction.y * 5; return new Bullet(x, y, target); } ... }
TurretBase.java
Just a very minor adjustment. Since we store TurretBase to access from outer resources, we will call barrel to shoot here.
package turret; ... public abstract class TurretBase extends Entity { protected TurretBarrel turretBarrel; ... // shooting the barrel! public Bullet shoot(Vector2 target) { return turretBarrel.shoot(target); } ... }
TurretMediator.java
package turret; import java.awt.Graphics2D; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.List; import bullet.BulletMediator; import components.Transform; import main.GameWindow; import math.Vector2; // listening to mouse to shoot our turrets! public class TurretMediator implements MouseListener{ private static final int turretCount = 10; private List<TurretBase> turrets = new ArrayList<>(); private BulletMediator bulletMediator; public TurretMediator(BulletMediator bulletMediator) { // caching bullet mediator which will be used to fire our bullet! this.bulletMediator = bulletMediator; // moved turret creation from gameloop here! TurretFactory turretFactory = new TurretFactory(); for (int i = 0; i < turretCount; i++) { float x = ((800 / turretCount) / 2) + i * (800 / turretCount); turrets.add(turretFactory.getTurret(x, 500, i, turretCount)); } GameWindow.instance.addMouseListener(this); } // shooting turret towards mouse position! public void shootTurret(int x, int y) { // shooting position, used to get distance Vector2 shootPosition = new Vector2(x, y); // caching closest turret TurretBase closestTurret = null; // having a really high number so it will loop through all turrets. // when lower number is found it will cache that distance and found turret until // a new closer distance towards position is found. double closestDistance = 99999999; for (TurretBase turretBase : turrets) { double distance = Vector2.getDistanceD(turretBase.getComponent(Transform.class).position, shootPosition); if(distance < closestDistance) { closestDistance = distance; closestTurret = turretBase; } } // Shoot from turret closest to our mouse position! bulletMediator.addBullet(closestTurret.shoot(shootPosition)); } public void update(double deltaTime) { for (TurretBase turret : turrets) { turret.update(deltaTime); } } public void render(Graphics2D g) { for (TurretBase turret : turrets) { turret.render(g); } } public void dispose() { GameWindow.instance.removeMouseListener(this); for (TurretBase turret : turrets) { turret.dispose(); } turrets.clear(); } @Override public void mousePressed(MouseEvent e) { // - 32 because of top bar size shootTurret(e.getX(), e.getY() - 32); } @Override public void mouseClicked(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } }
Missile mediator and Play state update
Now we have a mediator managing our bullets and turret, it’s best to also add a missile mediator to switch dependencies. It will be used in its full potential next episode when using observer pattern!
MissileMediator.java
We basically move all logic from our play state about missiles to our mediator.
package missile; import java.awt.Graphics2D; import java.util.ArrayList; import java.util.List; import java.util.Random; import bullet.BulletMediator; // moved all logic about missiles from gameloop to this mediator. public class MissileMediator { private static final int normalMissilesCount = 2; private static final int fastMissilesCount = 0; private static final int slowMissilesCount = 0; private static final int randomMissilesCount = 0; private static final boolean loopCreations = true; private List<Missile> missiles = new ArrayList<>(); private List<Missile> keepMissilesAlive = new ArrayList<>(); private MissileFactory missileFactory; private Random random; private float randomX() { return 10 + random.nextFloat() * (750); } private float randomY() { return random.nextFloat() * (-500); } public MissileMediator() { missileFactory = new MissileFactory(normalMissilesCount, fastMissilesCount, slowMissilesCount); random = new Random(); int size = normalMissilesCount + fastMissilesCount + slowMissilesCount + randomMissilesCount; for (int i = 0; i < size; i++) { float x = randomX(); float y = randomY(); missiles.add(missileFactory.getMissile(x, y, i)); } } public void update(double deltaTime) { List<Missile> removeMissiles = new ArrayList<Missile>(); for (Missile missile : missiles) { missile.update(deltaTime); if(missile.isStopped()) { removeMissiles.add(missile); } } loopCreations(removeMissiles); } private void loopCreations(List<Missile> removeMissiles) { if(!loopCreations) { return; } if(removeMissiles.size() > 0) { missiles.removeAll(removeMissiles); keepMissilesAlive.addAll(removeMissiles); for (int i = 0; i < removeMissiles.size(); i++) { missiles.add(missileFactory.getMissile(randomX(), randomY(), removeMissiles.get(i).getCreationType())); } } while (keepMissilesAlive.size() > 15) { keepMissilesAlive.remove(0); // removing missile when it reached a limit for performance } } public void render(Graphics2D g) { for (Missile missile : missiles) { missile.render(g); } for (Missile missile : keepMissilesAlive) { missile.render(g); } } public void dispose() { for (Missile missile : missiles) { missile.dispose(); } for (Missile missile : keepMissilesAlive) { missile.dispose(); } missiles.clear(); keepMissilesAlive.clear(); } }
PlayState.java
Now we have our mediators, we can apply them in our play state!
package gamestate.states; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.List; import java.util.Random; import javax.swing.AbstractAction; import bullet.BulletMediator; import gamestate.GameState; import gamestate.GameStateManager; import missile.Missile; import missile.MissileFactory; import missile.MissileMediator; import sprite.Sprites; import turret.TurretBase; import turret.TurretFactory; import turret.TurretMediator; import ui.Button; public class PlayState extends GameState { private Button backBtn; private TurretMediator turretMediator; private BulletMediator bulletMediator; private MissileMediator missileMediator; private boolean initialized = false; public PlayState(GameStateManager gsm) { super(gsm); } @Override public void init() { missileMediator = new MissileMediator(); bulletMediator = new BulletMediator(missileMediator); turretMediator = new TurretMediator(bulletMediator); backBtn = new Button(100, 50, Sprites.instance.getBtnBackNormal(), Sprites.instance.getBtnBackHover(), Sprites.instance.getBtnBackPressed()); backBtn.setScale(0.5f); backBtn.setOnClickAction(new OpenMenuState()); initialized = true; } // internal class to handle button action. Java doesn't allow lambda expressions as // well known in C#, so this is a work-around option ;) private class OpenMenuState extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { gsm.setState(new MenuState(gsm)); } } @Override public void update(double deltaTime) { if(!initialized) { return; } turretMediator.update(deltaTime); missileMediator.update(deltaTime); bulletMediator.update(deltaTime); if(backBtn != null) { backBtn.update(deltaTime); } } @Override public void render(Graphics2D g) { if(!initialized) { return; } turretMediator.render(g); missileMediator.render(g); bulletMediator.render(g); if(backBtn != null) { backBtn.render(g); } } @Override public void dispose() { backBtn.dispose(); turretMediator.dispose(); missileMediator.dispose(); bulletMediator.dispose(); } }
We now cleaned up 61 lines of codes by only adding mediators. Which makes our play state much easier to maintain and to look at.
Source code – GitHub
You can get all source codes here!
https://github.com/jscotty/DesignPatterns/tree/MediatorPattern
Breakdown
Just like in real life, sometimes we need a mediator to tell us what to do. It’s like a manager. It will communicate with other mediators to come to solutions, as we now done with our turrets and bullets.
Because if we would let them handle them all individually, it would be a mess to handle.
That’s why mediation is so great. It takes away a lot of individual dependencies which if you would create a UML diagram, give you a big headache! π
Comparison
As I complained last episode that we gained to much code by creating states, this mediator pattern was a great solution to solve it.
We separated object creation and data caching to each mediator. They are responsible for the actions of their data, not our play state!
Resources
Derek Banas
It wouldn’t be another resources section without Derek Banas, wouldn’t it? π
Refactoring Guru
Refactoring guru has once again a great explanation about the mediator pattern. Highly recommend to check it out!
https://refactoring.guru/design-patterns/mediator
Code Pumpkin
https://codepumpkin.com/mediator-design-pattern/
Hope you enjoyed this episode!
If any questions, feel free to message me on Instagram @justinbieshaar or in comments below!
Happy coding everyone! π¨βπ»
Greetings,
Justin Scott
Anonymous
Great post! Thanks for sharing