Welcome to part 7 of the Design Patterns series. This episode will be about the Observer Pattern!
Observer Pattern
Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object theyβre observing.
In games but also programming applications in general we have many observable subjects. But we don’t want to have direct access to full objects for a “simple” notification.
This is when Observer Pattern comes to play!
Subjects can determine which observers they want to notify. They can do them all, or a set of observers. But shouldn’t really care who all the observers are.
Observer patter in nowadays so common that most languages have integrated it in their native language! C# for example has baked it with the event keyword!
Examples
Musicians
A musician loves a big crowd. But he/she does not care about who is actually watching (observing) them, but they do notify the crowd once it’s time to cheer/clap for their performance. This way every time a musician finishes a song they call an event to all their observers and they can then decide to cheer, clap, scream WE WANT MORE or what ever. π
Auction
When being in a action everyone can observe the Auctioneer! The auctioneer is the subject which broadcasts to all observers a new “highest bid”. Once the highest bid is claimed, all observers will be notified that the auction is closed.
Intentions of using Observer Pattern
- Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Demo
Demo introduction
In this demo we will make observers to listen to our bullets and missiles if they are hitting each other or not.
To make the bullets feel more like the bullets as in the original missile command, we also scale each bullet in and out!
I choose to not use Java native observer pattern to highlight its usage in a simple way.
In steps
- Creating observers and subjects
- Applying subject and observers
- Register bullet observers
- When bullet reached position we scale
- While scaling we check collision
- When bullet dies (hits scale 0) we remove bullet
Code
Note: In some examples, 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!
All interfaces we will add will start with I.
Observers and Subject
For this demo we need to create a subject class to notify the observers.
IObserver.java
This is our main observer, only used for identification that we are storing observers. All the other upcoming observers will inherent from this.
package observers; public interface IObserver { // only for identification for subjects }
IDieObserver.java
We will use the die observer when a bullet “dies” so we can remove it.
package observers; public interface IDieObserver extends IObserver { // when subject dies, it fires onDie method to // all observers public <T> void onDie(Class<T> c); }
IMoveToPositionObserver.java
Whenever a bullet has reached it’s position, we will start scaling.
package observers; public interface IMoveToPositionObserver extends IObserver{ // when subject reaches position, it fires onDie method to // all observers public void onPosition(); }
IScaleObserver.java
When bullet is scaling, we will check collision with missile mediator!
package observers; import components.Image; public interface IScaleObserver extends IObserver { // when subject scaled, it fires onScale method to // all observers passing the image for subjects to check // the image data. public void onScale(Image image); }
Subject.java
This subject class is similar to our entity class which we developed in ECS episode! It stores all observers
package subject; import java.util.ArrayList; import java.util.List; import observers.IObserver; public class Subject { protected List<IObserver> observers = new ArrayList<>(); // adding observers. public void register(IObserver observer) { observers.add(observer); } // remove observer public void unregister(IObserver observer) { if(!observers.contains(observer)) { throw new IllegalArgumentException("This subject does not contain this observer"); } observers.remove(observer); } // each subject can decide what observer it wants // to notify. Therefor they can call this method // to request all observers of a certain type. public<T> List<T> getObservers(Class<T> c){ List<T> result = new ArrayList<>(); for (int i = 0; i < observers.size(); i++) { IObserver observer = observers.get(i); if(c.isInstance(observer)) { result.add(c.cast(observer)); } } return result; } }
Component.java
Our components will be our subjects. Each component can we register an observer interface which each component sub class will fire their methods when needed.
package ecs; ... import subject.Subject; public abstract class Component extends Subject { ... }
Subject components
MoveToPosition.java
package components; ... import observers.IMoveToPositionObserver; public class MoveToPosition extends Component { ... @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; // firing on position to notify all our observers. onPosition(); } } ... private void onPosition() { // notifying all our observers! for (IMoveToPositionObserver observer : getObservers(IMoveToPositionObserver.class)) { observer.onPosition(); } } }
ImageScaler.java
This is a new component we will use on our bullet to scale up and down our bullet image.
package components; import java.awt.Graphics2D; import ecs.Component; import observers.IDieObserver; import observers.IScaleObserver; public class ImageScaler extends Component { private Image image; private float maxScale; private float scaleStep; private float delay; private boolean invertScale; private float time = 0; private float currentScale; private boolean died = false; public ImageScaler(float maxScale, float scaleStep, float delay, boolean invertScale) { this.maxScale = maxScale; this.scaleStep = scaleStep; this.delay = delay; this.invertScale = invertScale; } @Override public void init() { image = entity.getComponent(Image.class); currentScale = image.getScale(); } @Override public void update(double deltaTime) { if(died) { return; } time += deltaTime; if(time < delay) { return; } time = 0; currentScale += scaleStep; if(currentScale > maxScale) { currentScale = maxScale; if(invertScale) { scaleStep *= -1; } else { onDie(); } } else if(currentScale < 0) { currentScale = 0; onDie(); } image.setScale(currentScale); onScaling(); } @Override public void render(Graphics2D g) { } private void onDie() { died = true; for (IDieObserver observer : getObservers(IDieObserver.class)) { observer.onDie(entity.getClass()); } } private void onScaling() { for (IScaleObserver observer : getObservers(IScaleObserver.class)) { observer.onScale(image); } } }
Our scaling effect
Registering observers
Bullet.java
package bullet; import components.Image; import components.ImageScaler; import components.MoveToPosition; import components.Transform; import ecs.Entity; import math.Vector2; import observers.IDieObserver; import observers.IMoveToPositionObserver; import observers.IScaleObserver; import sprite.Sprites; public class Bullet extends Entity implements IMoveToPositionObserver { private static final float speed = 2f; // adding scaling data private static final float maxScale = 7.5f; private static final float scaleStep = .5f; private static final float delay = 10f; private static final boolean invertScale = true; private IScaleObserver scaleObserver; private IDieObserver dieObserver; public Bullet(float x, float y, Vector2 target) { addComponent(new Transform(x, y)); addComponent(new Image(Sprites.instance.getBullet())); MoveToPosition mtp = new MoveToPosition(target, speed); // registering this object because we are listening to when we moved // our target position! mtp.register(this); addComponent(mtp); } // add scale observers to our scaler // can not do it directly because we scale // after we hit our target position public void addScaleObserver(IScaleObserver observer) { this.scaleObserver = observer; } // add die observer which will be added once we start scaling public void addDieObserver(IDieObserver observer) { this.dieObserver = observer; } @Override public void onPosition() { // removing component because it's not needed anymore. removeComponent(MoveToPosition.class, false); ImageScaler scaler = new ImageScaler(maxScale, scaleStep, delay, invertScale); // register cached observers scaler.register(scaleObserver); scaler.register(dieObserver); addComponent(scaler); } }
BulletMediator.java
package bullet; import java.awt.Graphics2D; import java.util.ArrayList; import java.util.List; import components.Image; import missile.MissileMediator; import observers.IDieObserver; import observers.IScaleObserver; public class BulletMediator implements IScaleObserver, IDieObserver { // all bullets stored private List<Bullet> bullets = new ArrayList<>(); private MissileMediator missileMediator; public BulletMediator(MissileMediator missileMediator) { this.missileMediator = missileMediator; } // adding, updating, rendering and disposing // bullets. Must be very much self explaining :) public void addBullet(Bullet bullet) { bullets.add(bullet); bullet.addScaleObserver(this); bullet.addDieObserver(this); } 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(); } @Override public void onScale(Image image) { missileMediator.checkCollision(image); } @Override public <T> void onDie(Class<T> c) { Bullet removeBullet = null; removeBullet = (Bullet) c.cast(removeBullet); if(removeBullet == null) { return; } bullets.remove(removeBullet); } }
Collision detection
MissileMediator.java
package missile; ... public class MissileMediator { ... public void checkCollision(Image image) { for (int i = 0; i < missiles.size(); i++) { Missile missile = missiles.get(i); if(missile.collisionDetection(image)) { // missile is hit! DIE! System.out.println("MISSILE HIT"); // removing missile because we don't need it anymore. It's destroyed! missiles.remove(missile); i--; } } } }
This really highlights the previous episode about mediator pattern!
Instead of letting each bullet have individual reference to each missile to check collision detection, we let the mediators check with each other.
That makes we only have dependency with the two mediators. π
Note: This is not optimised.
Source code – GitHub
You can get all source codes here!
https://github.com/jscotty/DesignPatterns/tree/ObserverPattern
Breakdown
We now have a playable game! π
We can finally destroy our missiles by observing our bullets. But also added lot’s of opportunities with this pattern. We can easily register observers to any subject, which is really nice for our scalability.
But also try to be careful. Don’t overuse this pattern too much, because it will be quite hard to debug at some point. So, try to keep it clean so you have a clear overview as a developer of who’s observing who π. Because if you lose the overview, well.. good luck debugging it!
Resources
Derek Banas
Game Programming Patterns
Game Programming Patterns highlighting the observer pattern greatly with a achievements system!
https://gameprogrammingpatterns.com/observer.html
Source Making
Great and simple explanation of the observer pattern, but also it’s problem.
https://sourcemaking.com/design_patterns/observer
Hope you enjoyed this episode!
Have you ever used this pattern before? Let me know!
If any questions, feel free to message me on Instagram @justinbieshaar or in comments below!
Happy coding everyone! π¨βπ»
Greetings,
Justin Scott
Missile Command – Recap + Challenge! – Justin Scott Bieshaar
[…] opportunity to challenge yourself using a little foundation.As you can see after the last episode (Observer Pattern), the game is far from finished!But that’s a great challenge for […]