Design In-game Entities – The Strategy Pattern
In this part I will try to explain what I understand on good game design elements. I will use droids in the examples and I will script a basic fight simulator to see how they behave.
The problem:
I command a single robot and I want to obliterate my enemies. To face the same type of enemy all over again is boring. I need new challenges and this means new types of enemies. For example in the first level I want only to practice my target. So I need a pretty dumb enemy that does not do much but takes the shots. After I mastered that skill (shooting a helpless droid), I need a bit of a challenge and I want the enemy droid to fight back, but because I am still a beginner I don’t want to die quickly so I need weak droids. After I am over with them I want a tougher challenge. I need better and stronger droids. Not just stronger, but different in behaviour as well as it can get boring killing the same type of enemy over and over again.
The obvious solution:
Create 3 classes for the 3 types of enemy droids. To keep it simple, each droid has 2 abilities: move and attack. It makes sense to create a Droid interface with these two methods and have each droid implement them.
They can move and shoot. Well, not all of them but we can provide an empty implementation for the ones that do nothing.
The droid types:
-
Decoy Droid – will have no weapon and can’t move.
-
Scout Droid – will have a weak weapon and moves fast.
-
Assault Droid – will have a heavy weapon and moves slowly.
Looking at the 3 types we can implement the following simple class diagram:
The Obvious Solution
The interface has 3 simple methods which the droids need to implement:
public interface Droid {
// display some info of the droid
public void display();
// move the droid
public void move(int x, int y);
// attack position
public void shoot(int x, int y);
}
The 3 classes are as follow:
DecoyDroid.java
public class DecoyDroid implements Droid {
@Override
public void display() {
System.out.println("I am a DECOY droid");
}
@Override
public void move(int x, int y) {
System.out.println("Can't move.");
}
@Override
public void shoot(int x, int y) {
System.out.println("Have no weapon.");
}
}
ScoutDroid.java
public class ScoutDroid implements Droid {
private float damage = 0.5f;
@Override
public void display() {
System.out.println("I am a scout droid");
}
@Override
public void move(int x, int y) {
System.out.println("Moving QUICKLY to: " + x + "," + y + ".");
}
@Override
public void shoot(int x, int y) {
System.out.println("Light Laser Canon targeting: " + x + "," + y
+ ". Damage: " + damage);
}
}
AssaultDroid.java
public class AssaultDroid implements Droid {
private float damage = 2.5f;
private boolean loaded = true;
@Override
public void display() {
System.out.println("I am an ASSAULT droid");
}
@Override
public void move(int x, int y) {
System.out.println("Moving SLOWLY to: " + x + "," + y + ".");
}
@Override
public void shoot(int x, int y) {
if (loaded) {
System.out.println("Heavy laser targeting: " + x + "," + y
+ ". Damage: " + damage);
loaded = false;
} else {
System.out.println("Reloading...");
loaded = true;
}
}
}
Both ScoutDroid
and AssaultDroid
have the argument damage
. This holds the value of the damage inflicted by them.
To give the AssaultDroid
a heavy weapon with a slow reload time we added the loaded
variable. This way it takes the assault droid two turns to fire its weapon once.
I have created a simple simulator for the droids to take turns to move and shoot.
Run the simulator for this design:
BadDroidSimulator.java
public class BadDroidSimulator {
public static void main(String[] args) {
// for generating random numbers
Random rand = new Random();
Droid scout = new ScoutDroid();
Droid assailant = new AssaultDroid();
Droid decoy = new DecoyDroid();
scout.display();
assailant.display();
decoy.display();
// shoot-out - each droid fires once per turn
for (int i = 1; i <= 5; i++) {
System.out.println("\n<=== BEGIN TURN " + i + " ===>");
scout.shoot(rand.nextInt(10), rand.nextInt(10)); // we assume this is an enemy position
scout.move(rand.nextInt(10), rand.nextInt(10));
System.out.println();
assailant.shoot(rand.nextInt(10), rand.nextInt(10));
assailant.move(rand.nextInt(10), rand.nextInt(10));
System.out.println();
decoy.shoot(rand.nextInt(10), rand.nextInt(10));
decoy.move(rand.nextInt(10), rand.nextInt(10));
System.out.println("<=== END TURN " + i + " ===>");
}
}
}
The result (console output) will look like this:
I am a scout droid
I am an ASSAULT droid
I am a DECOY droid
<=== BEGIN TURN 1 ===>
Light Laser Canon targeting: 9,0. Damage: 0.5
Moving QUICKLY to: 4,6.
Heavy laser targeting: 6,2. Damage: 2.5
Moving SLOWLY to: 9,1.
Have no weapon.
Can’t move.
<=== END TURN 1 ===>
<=== BEGIN TURN 2 ===>
Light Laser Canon targeting: 3,4. Damage: 0.5
Moving QUICKLY to: 6,5.
Reloading…
Moving SLOWLY to: 1,6.
Have no weapon.
Can’t move.
<=== END TURN 2 ===>
<=== BEGIN TURN 3 ===>
Light Laser Canon targeting: 6,7. Damage: 0.5
Moving QUICKLY to: 9,7.
Heavy laser targeting: 7,1. Damage: 2.5
Moving SLOWLY to: 2,0.
Have no weapon.
Can’t move.
<=== END TURN 3 ===>
<=== BEGIN TURN 4 ===>
Light Laser Canon targeting: 3,7. Damage: 0.5
Moving QUICKLY to: 1,4.
Reloading…
Moving SLOWLY to: 5,9.
Have no weapon.
Can’t move.
<=== END TURN 4 ===>
<=== BEGIN TURN 5 ===>
Light Laser Canon targeting: 0,8. Damage: 0.5
Moving QUICKLY to: 3,9.
Heavy laser targeting: 1,2. Damage: 2.5
Moving SLOWLY to: 3,2.
Have no weapon.
Can’t move.
<=== END TURN 5 ===>
Challenges to extend the design
The droids take turns to move and shoot. This is all good, but:
-
What if you want to create a hybrid droid? A droid that moves as fast as the scout but with a heavy weapon? You will have to create a new class and copy paste the respective methods from the Scout and Assault droid, right?
-
Also imagine that the shooting mechanism is not that simple and it needs collision detection and so on. For each droid the same redundant code needs to be rewritten.
-
What if the fire power could be enhanced with power ups?
-
What if the droid gains self-conscientiousness and finds a weapon to use it instead of the current one?
I am sure you have plenty ideas on how to enhance the gameplay and extend the world but the most obvious solution (described above) seems ill-suited for this. It requires new droid classes to be created and each droid type will implement its methods separately. Many of these methods are identical. The current design doesn’t allow you to change the droid’s internals at runtime without significant effort.
Here is one proposed solution: Composition and the Strategy Pattern.
Designing a Droid (properly)
A very simple droid consists of a weapon put on a chassis. The first design consisted of a “is a” type relationship. A ScoutDroid
is a generic Droid
with some peculiarities.
Composition is based on “has a” relationships. A Droid
has a Chassis
. A Droid
has a Weapon
. What type of components a droid has, determines its type.
Let’s decompose the Scout Droid for example.
The Scout Droid is a Droid which has a Light Laser Canon, has a set of Wheels to move. We want to make the scout move quickly with a light weapon.
The Assault Droid on the other hand is a Droid too but it *has a Heavy Laser Canon and it runs on (has) Tracks. This makes it extremely powerful but a bit slow.
Think from a factory perspective. How does a car production line work? You get the chassis with a specific place for the engine, wheels, drive-shaft, gear-box and so on.
All these components are produced separately. The teams that produce them have no idea of the other parts. They must fulfil one criteria: the gearbox must fit in perfectly in its place and connected up with the engine.. Different makes have different ways of doing this. The connector in this instance is the interface.
The engine has a similar story. If it hooks up nicely with the wheels and gearbox then it can be fitted. Its internal design, capacity, power, fuel consumption can be completely different. The engine is one of the car’s components.
So is our droid. But to keep it simple we have only 2 components. We need one generic droid that has all the wirings built so its components will be triggered by the droid through those interfaces. For example a droid needs to only pull the trigger of the weapon and doesn’t care what type of weapon it is as long as it has a trigger. The droid needs to understand the pullTrigger method and to do this it needs to be implemented, in order for us to give weapons to the droid to use.
The same thing with the changing of location. It needs to trigger the movement. The wheels or track or anti-gravity propulsion will take the droid there. The droid only needs to set the coordinates.
To fulfil this we need a class with implemented methods instead of an interface.
We create the abstract Droid class. We make it abstract because we actually implement the methods that trigger the weapon, and action the moving mechanism but we don’t have concrete weapons and movement mechanisms attached to the droid. The assembly of a concrete droid will be delegated to the type constructor along with the description method.
public abstract class Droid {
protected Weapon weapon; // the weapon which will be used in fights
protected Chassis chassis; // the chassis on which the droid is placed
public void moveToPosition(int x, int y) {
System.out.print(id + " > " );
chassis.moveTo(x, y);
}
/**
* Engages the position on the screen whether it is occupied by
* an enemy or not. Each strategy should decide how to do it.
*/
public void attackPosition(int x, int y) {
System.out.print(id + " > ");
weapon.useWeapon(new Vector2f(x, y));
}
/**
* Displays some info on the droid
*/
public abstract void display();
}
If you examine the class you will see that the Droid
has 3 methods from which 2 are implemented. It also has two components: Weapon
and Chassis
.
The components are interfaces so the droid does not know what it is doing when triggering the actions on them. It is all delegated to the implementation.
The interfaces are as follows:
Weapon.java
public interface Weapon {
/**
* The trigger to use the weapon.
* @param target - where on the map is the target
*/
public void useWeapon(Vector2f target);
/**
* Returns the description of the weapon
*/
public String getDescription();
}
Chassis.java
public interface Chassis {
/**
* Delegates the movement to the supporting chassis and
* tries to move the unit to x,y
*/
public void moveTo(int x, int y);
/**
* Returns the description of the chassis
*/
public String getDescription();
}
We will enhance our base Droid
class. We will add setter and getter methods for both Weapon
and Chassis
. This will allow us to change the droid’s behaviour at runtime. This is what the strategy pattern is all about. A Droid
has behaviours: it can use a weapon and it can move. These two strategies (behaviours) need to be implemented.
We also add an id
which will be unique in our game for each droid instance. I use a very simple id generation strategy. I increment the nextId
static field and append it to the concrete droid type prefix in the constructor for each type.
Here is the new Droid class:
public abstract class Droid {
protected static int nextId = 0; // the next available ID
protected String id; // unique id
protected Weapon weapon; // the weapon which will be used in fights
protected Chassis chassis; // the chassis on which the droid is placed
// the unique ID of the droid in the game
public String getId() {
return id;
}
public Weapon getWeapon() {
return weapon;
}
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
public Chassis getChassis() {
return chassis;
}
public void setChassis(Chassis chassis) {
this.chassis = chassis;
}
public void moveToPosition(int x, int y) {
System.out.print(id + " > " );
chassis.moveTo(x, y);
}
/**
* Engages the position on the screen whether it is occupied by
* an enemy or not. Each strategy should decide how to do it.
*/
public void attackPosition(int x, int y) {
System.out.print(id + " > ");
weapon.useWeapon(new Vector2f(x, y));
}
/**
* Displays some info on the droid
*/
public abstract void display();
}
Let’s build some weapons
NoWeapon.java
/**
* This is a null object. A null object is a dummy that does nothing and it
* is a mere place-holder and eliminates the need to check for null.
* @author impaler
*
*/
public class NoWeapon implements Weapon {
@Override
public void useWeapon(Vector2f target) {
// We are doing nothing
System.out.println("No weapon equipped!");
}
@Override
public String getDescription() {
return "Nothing";
}
}
This is the null object. The class description should give you an idea what it is.
LightLaserCanon.java
/**
* This is a light laser cannon whit a quick reload time and high accuracy
*
* @author impaler
*
*/
public class LightLaserCanon implements Weapon {
private float damage = 0.5f; // the damage inflicted
@Override
public void useWeapon(Vector2f target) {
System.out.println("Shooting my laser canon to " + (int)target.getX() + ","
+ (int)target.getY() + ". Bang on! Done " + damage + " damage.");
}
@Override
public String getDescription() {
return "First generation laser canon. Street use only!";
}
}
HeavyLaserCanon.java
/**
* This is a heavy assault laser cannon with high accuracy but slow reload time.
* @author impaler
*/
public class HeavyLaserCanon implements Weapon {
private boolean loaded = true; // after fire needs to be reloaded
private float damage = 1.5f; // the damage is considerable
@Override
public void useWeapon(Vector2f target) {
if (loaded) {
// we fire the canon
System.out.println("Eat this! Laser beam hit target (" + (int)target.getX() + "," + (int)target.getY() + ") and dealt " + damage + " damage.");
// next time needs reloading
loaded = false;
} else {
System.out.println("Darn! Out of ammo! Reloading...");
loaded = true;
}
}
@Override
public String getDescription() {
return "DASS-5000 - The ultimate in siege weaponry provided by Obviam Enterprises.";
}
}
You might notice the Vector2f
class. This is a very basic 2D vector class which currently holds the x and y coordinates. Nothing more. You can find it in the downloaded source.
Let’s build some chassis
The getDescription()
method says what the chassis is like.
NoChassis.java
– null object (see weapons)
public class NoChassis implements Chassis {
@Override
public void moveTo(int x, int y) {
System.out.println("It's just a frame. Can't move.");
}
@Override
public String getDescription() {
return "It's just a frame.";
}
}
SteelStand.java
public class SteelStand implements Chassis {
@Override
public void moveTo(int x, int y) {
System.out.println("Can't move.");
}
@Override
public String getDescription() {
return "Unmovable reinforced steel stand ideal for turrets and defensive units.";
}
}
Wheels.java
public class Wheels implements Chassis {
@Override
public void moveTo(int x, int y) {
System.out.println("Speeding to " + x + "," + y + " on my wheels!");
}
@Override
public String getDescription() {
return "4 wheel drive, very fast on flat terrain but struggling through obstacles.";
}
}
Track.java
public class Track implements Chassis {
@Override
public void moveTo(int x, int y) {
System.out.println("Don't get in my way! Moving slowly to: " + x + "," + y + ".");
}
@Override
public String getDescription() {
return "Slow moving tracks but able to go through many obstacles.";
}
}
Now we can assemble our droids
First let’s create a DecoyDroid
. This droid will have no weapon and will be placed on a steel stand. It’s for our target practice, remember?
DecoyDroid.java
public class DecoyDroid extends Droid {
public DecoyDroid() {
id = "DCY-" + (++Droid.nextId);
weapon = new NoWeapon();
chassis = new SteelStand();
}
@Override
public void display() {
System.out.println("+--------------------------------------------------------------------------------------------+");
System.out.println("| I am a DECOY droid.");
System.out.println("|\tID: " + id);
System.out.println("|\tWeapon: " + weapon.getDescription());
System.out.println("|\tChassis: " + chassis.getDescription());
System.out.println("+--------------------------------------------------------------------------------------------+");
}
}
Examine the default constructor. It creates an id
and assigns an instance of NoWeapon
and SteelStand
to the droid.
The display()
method is more elaborate than before but just to describe the droid better. It makes use of the components’ descriptions too.
If you instantiate a DecoyDroid
and call its display method you will get a nice description of it.
+——————————————————————————————–+
| I am a DECOY droid.
| ID: DCY-3
| Weapon: Nothing
| Chassis: Unmovable reinforced steel stand ideal for turrets and defensive units.
+——————————————————————————————–+
Let’s build the rest of the types:
ScoutDroid.java
public class ScoutDroid extends Droid {
public ScoutDroid() {
id = "SCT-" + (++Droid.nextId);
weapon = new LightLaserCanon();
chassis = new Wheels();
}
@Override
public void display() {
System.out.println("+--------------------------------------------------------------------------------------------+");
System.out.println("| I am a SCOUT droid.");
System.out.println("|\tID: " + id);
System.out.println("|\tWeapon: " + weapon.getDescription());
System.out.println("|\tChassis: " + chassis.getDescription());
System.out.println("+--------------------------------------------------------------------------------------------+");
}
}
AssaultDroid.java
public class AssaultDroid extends Droid {
public AssaultDroid() {
id = "ASS-" + (++Droid.nextId);
weapon = new HeavyLaserCanon();
chassis = new Track();
}
@Override
public void display() {
System.out.println("+--------------------------------------------------------------------------------------------+");
System.out.println("| I am an ASSAULT droid.");
System.out.println("|\tID: " + id);
System.out.println("|\tWeapon: " + weapon.getDescription());
System.out.println("|\tChassis: " + chassis.getDescription());
System.out.println("+--------------------------------------------------------------------------------------------+");
}
}
You will notice that the only things needed to be implemented are the constructor – which adds the chassis and weapon – and the display()
method.
The following diagram shows the new architecture:
A Better Solution
Let’s create a test script for it. We’ll simulate 5 turns in which each droid will use its weapon and move to a random location. Check the behaviour of each weapon and you will notice that the heavy laser will fire once every 2 turns. To make it interesting, in turn 4 we give a HeavyLaserCanon
to DecoyDroid
. Look at how it changes the droid’s behaviour and it starts firing. This is a hybrid droid created on the fly at runtime.
The simulator code (DroidSimulator.java
):
public class DroidSimulator {
public static void main(String[] args) {
// for generating random numbers
Random rand = new Random();
Droid scout = new ScoutDroid();
Droid assailant = new AssaultDroid();
Droid decoy = new DecoyDroid();
scout.display();
assailant.display();
decoy.display();
// shoot-out - each droid fires once per turn
for (int i = 1; i <= 5; i++) {
System.out.println("\n<=== BEGIN TURN " + i + " ===>");
// in turn 3 decoy droid is given an assault canon
if (i == 4) {
decoy.setWeapon(new HeavyLaserCanon());
System.out.println("* " + decoy.getId() + " acquired " + decoy.getWeapon().getDescription() + "\n");
}
scout.attackPosition(rand.nextInt(10), rand.nextInt(10)); // we assume this is an enemy position
scout.moveToPosition(rand.nextInt(10), rand.nextInt(10));
System.out.println();
assailant.attackPosition(rand.nextInt(10), rand.nextInt(10));
assailant.moveToPosition(rand.nextInt(10), rand.nextInt(10));
System.out.println();
decoy.attackPosition(rand.nextInt(10), rand.nextInt(10));
decoy.moveToPosition(rand.nextInt(10), rand.nextInt(10));
System.out.println("<=== END TURN " + i + " ===>");
}
}
}
The output:
+——————————————————————————————–+
| I am a SCOUT droid.
| ID: SCT-1
| Weapon: First generation laser canon. Street use only!
| Chassis: 4 wheel drive, very fast on flat terrain but struggling through obstacles.
+——————————————————————————————–+
+——————————————————————————————–+
| I am an ASSAULT droid.
| ID: ASS-2
| Weapon: DASS-5000 – The ultimate in siege weaponry provided by Obviam Enterprises.
| Chassis: Slow moving tracks but able to go through many obstacles.
+——————————————————————————————–+
+——————————————————————————————–+
| I am a DECOY droid.
| ID: DCY-3
| Weapon: Nothing
| Chassis: Unmovable reinforced steel stand ideal for turrets and defensive units.
+——————————————————————————————–+
<=== BEGIN TURN 1 ===>
SCT-1 > Shooting my laser canon to 0,3. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 0,2 on my wheels!
ASS-2 > Eat this! Laser beam hit target (3,4) and dealt 1.5 damage.
ASS-2 > Don’t get in my way! Moving slowly to: 3,8.
DCY-3 > No weapon equipped!
DCY-3 > Can’t move.
<=== END TURN 1 ===>
<=== BEGIN TURN 2 ===>
SCT-1 > Shooting my laser canon to 4,0. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 5,0 on my wheels!
ASS-2 > Darn! Out of ammo! Reloading…
ASS-2 > Don’t get in my way! Moving slowly to: 1,6.
DCY-3 > No weapon equipped!
DCY-3 > Can’t move.
<=== END TURN 2 ===>
<=== BEGIN TURN 3 ===>
SCT-1 > Shooting my laser canon to 3,0. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 0,6 on my wheels!
ASS-2 > Eat this! Laser beam hit target (9,1) and dealt 1.5 damage.
ASS-2 > Don’t get in my way! Moving slowly to: 8,0.
DCY-3 > No weapon equipped!
DCY-3 > Can’t move.
<=== END TURN 3 ===>
<=== BEGIN TURN 4 ===>
* DCY-3 acquired DASS-5000 – The ultimate in siege weaponry provided by Obviam Enterprises.
SCT-1 > Shooting my laser canon to 8,6. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 2,3 on my wheels!
ASS-2 > Darn! Out of ammo! Reloading…
ASS-2 > Don’t get in my way! Moving slowly to: 0,6.
DCY-3 > Eat this! Laser beam hit target (9,4) and dealt 1.5 damage.
DCY-3 > Can’t move.
<=== END TURN 4 ===>
<=== BEGIN TURN 5 ===>
SCT-1 > Shooting my laser canon to 1,7. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 1,9 on my wheels!
ASS-2 > Eat this! Laser beam hit target (1,4) and dealt 1.5 damage.
ASS-2 > Don’t get in my way! Moving slowly to: 3,6.
DCY-3 > Darn! Out of ammo! Reloading…
DCY-3 > Can’t move.
<=== END TURN 5 ===>
Note in turn 4 how DecoyDroid
got the new weapon and changed its behaviour. Now you should understand the null object pattern as well.
As it stands now, it is easy to create new weapons, chassis and droids by keeping the code to a minimum. Always favour composition over inheritance in these situations. You can create a very elaborate droid, with shields, sensors, an array of weapons and also an AI component which decides what weapons to use according to the situation. If you have noticed, I used the term “use” instead of fire, because a weapon can be a melee one too and not necessarily a ranged weapon.
The downloadable project contains only the strategy pattern implementation. The simple inheritance approach is left out. Play with it and get the grips of this useful pattern as I will use it extensively and it is considered good design. It is a simple Java application, it is not Android enabled.
Download the source here (TBD).
In the following parts I’ll try to add some AI capabilities and how to compose the final droid from images of its components.