When I first started making Flash games years ago, I found myself facing the same problem in every game and could not find a solution online.
How do I get game state from anywhere in the code base without using global variables?
Unless your making a game entirely inside one file, you will eventually need a way to get information about the level and things in it, like has the game started or finished? What about how big is the level?
Or event at a lower level, you need to check which entities got hit in a grenade blast, or figure out if this unit is on my team.
Initially a Singleton is a Good Solution
A good example is handling victory conditions. In RTS games the game checks every frame wether you won or lost.
This method checks if you won.
bool HasPlayerWonGame() {
foreach(Entity entity in Entity.Instance.entities) {
if (entity.teamName == "Enemy") {
if (entity.health > 0) {
return false;
}
}
}
return true;
}
Holy bonkers Batman, there are so many things wrong with this.
- Entity.Instance.entities A singleton leaks global state, prevents testing and proper encapsulation
- entity.health > 0 Just write this like what is is, entity.IsAlive
- entity.teamName == “Enemy” Why loop through all entities? Just hold a separate array for each team
As time goes on this type of code will become widespread and headaches will come with it. I never use singletons in my code except for a few select classes, like my Event Manager.
Singletons are the most overused and misused design pattern in programming. Why? Having the ability to access the game state from a global variable leads to Spaghetti code.
Classes should not depend on global variables, or else when that one global variable changes, so does every other class that depends on it. Often leading to runtime crashes or worse subtle bugs. Modular code is good code.
So what is the better solution? This whole problem occurs because a specific thing about game state – in this case entities – is needed in a low-level place in code.
Using the Chain Pattern
My solution is to contain all game state in a parent-child relationship.
+----------+
| |
| Level |
| |
+----------+
+---^ ^---+
| |
+-----+----+ +----+------+
| | | |
| Team List| | Game Mode |
| | | |
+----------+ +-----------+
+--------^ ^-----+
| | +------+-------+ +------+-------+ | | | | | Player Team | | Enemy Team | | | | | +--------------+ +--------------+ ^---+
| +--+-------+ Etc... | Entity 1 | +----------+
Etc…
At the very highest position we have level
. This contains attributes like
levelWidth
, levelHeight
but more importantly references to teamList
and
gameMode
. And teamList
contains a reference to playerTeam
and enemyTeam
.
And so on.
So with this structure, with just an instance of level you can drill down to get anything you are looking for. This entirely removes globals and makes it easier to write well encapsulated unit tests.
Same Example But Using The Chain Pattern
bool HasPlayerWonGame() {
return level.teamList.enemyTeam.IsAllDead();
}
Much cleaner.
This means this instance only needs a reference to the level
to get all the
information it needs.
How Did Game Mode get a Reference to Level?
This method, HasPlayerWonGame()
, is from the gameMode
class. Take a look at
the diagram again.
This is the great thing about the chain pattern, it can go up the chain and down
the chain. The
gameMode
object went up to level
and then drilled down to teamList
.
+----------+
Go down | | And up the chain
+----+ | Level | <---+
| | | |
| +----------+ |
v +---^ ^---+ +
| |
+-----+----+ +----+------+
| | | |
| Team List| | Game Mode |
| | | |
+----------+ +-----------+
Etc...
This means will just one entity you can get a reference to any thing in the game.
entity.team // Return current team
entity.oppositeTeam // Returns opposing team
Implementation
It’s important to note, these classes should never contain game logic, these are strictly data objects. They only hold data about an entity or level but not logic of how it works. That code should be pushed to controllers.
Level
public class Level {
public TeamList teamList { get; private set; }
public Teams playerId { get; private set; }
public Teams enemyId { get; private set; }
private GameMode gameMode;
public LevelData levelData { get; private set; }
public string name { get; private set; }
public int width;
public int length;
public Level() {
}
public Level LoadData(LevelData levelData, Teams playerTeam, Teams enemyTeam) {
this.teamList = new TeamList(this);
this.levelData = levelData;
this.name = levelData.name;
playerId = playerTeam;
enemyId = enemyTeam;
this.gameMode = GameMode.createCondition(levelData.victoryType, this);
return this;
}
public Team playerTeam {
get { return teamList.Find(playerId); }
}
public Team enemyTeam {
get { return teamList.Find(enemyId); }
}
}
On level
creation teamList
and gameMode
are also created. Notice this
is
passed into both, that is for going up the chain.
Note: levelData
is a value-object for storing meta data about a level.
public enum Teams {
Misc = 0,
One,
Two
};
public class TeamList {
public Level level { get; private set; }
public Team misc { get; private set; }
public Team teamOne { get; private set; }
public Team teamTwo { get; private set; }
private Team[] _teams;
public TeamList(Level level){
this.level = level;
misc = new Team(Teams.Misc, "TeamMisc", this);
teamOne = new Team(Teams.One, "TeamOne", this);
teamTwo = new Team(Teams.Two, "TeamTwo", this);
_teams = new Team[3];
_teams[0] = misc;
_teams[1] = teamOne;
_teams[2] = teamTwo;
}
public Team Find(Teams id){
return _teams[(int)id];
}
public Team OpposingTeam(Teams id){
if(id == Teams.One){
return _teams[2];
} else if(id == Teams.Two){
return _teams[1];
}
return _teams[0];
}
public Team OpposingTeam(Team team){
return OpposingTeam(team.id);
}
}
A team list contains all teams in the game. The level
reference is saved on the teamList
instance for chaining up. Also notice this
is passed to each instance of team.
Entity
public class Entity {
public Entity(Team team) {
this.team = team;
}
public Team team;
public Level level { get { return team.teamList.level; } }
public TeamList teamList { get { return team.teamList; } }
public bool IsTeammate(Entity entity){
return team == entity.team;
}
}
You get the idea. I define a getter for level
and teamList
for convenience.
Here are some more examples.
entity.IsTeammate(otherEntity) // true or false
entity.team.oppositeTeam.IsAllDead() // true or false
Conclusion
The Chain pattern is a great way of structuring games, lightly couples, no global instances and completely testable. I have keep using it for years and it has never failed me.
This also ties well into unit testing but that is a post for an other time.