The Bomberman series of games are simple games with an interesting set of mechanics. Having used an ECS framework in a few projects, I thought it would be useful to see how we can implement these mechanics using this pattern.
I won't go into a detailed introduction of ECS here. For a great primer on the topic, have a look at
Understanding Component-Entity-Systems.
I also provide a working game that contains the bulk of the PvP core mechanics, and look at what value ECS provided us (and where there is room for improvement). The game leverages ECS in lots of other ways, but for the purpose of this article I will only discuss the core game mechanics.
For the purposes of clarity and language-independence, code samples in the article will be a sort of pseudo-code. For the full C# implementation, see the sample itself.
Also, I use bold capitalized names to refer to components. So a
Bomb refers to the official component, while if I mention a bomb I'm just talking about the concept or the entity that represents the concept.
Let's get to work
Before designing any system, it's necessary to understand the scope of the problem you're trying to solve. This means listing out all the interactions between various game objects. From there, we can figure out the right abstractions to use. Of course, knowing all the interactions is impossible if your game is under development - or even if you're trying to make a clone - because there will be things you missed at first. One of the advantages of ECS is that it lends itself to making changes as needed while affecting a minimum of other code.
A rough summary of the PvP Bomberman gameplay is as follows. Players plant bombs that destroy other players and disintegrate blocks. Disintegrated blocks leave behind various powerups that augment the destructive power of your bombs, or affect player speed. Players complete to see who is the last to survive as a clock counts down to total destruction.
Without further ado, let's look into the basic bomb mechanics Bomberman and come up with a design we can implement using the ECS pattern.
Explosions
Explosion showing behavior with (1) hard blocks, (2) soft blocks, and (3) empty space
When a bomb explodes, the explosion shoots out in various directions. The following things can happen:
- "Hard blocks" block the path of the explosion
- "Soft blocks" block the path of the explosion (usually), and are destroyed; they randomly reveal a powerup
- Un-triggered bombs will detonate
- Any players hit by the explosion will die
One bomb's explosion can trigger another bomb
It seems there are two concepts here: how a particular object reacts to the explosion (dies, triggers, disintegrates), and how a particular object affects the path of the explosion (blocks it, or doesn't).
We can create an
ExplosionImpact component to describe this. There are only a few ways an object can affect the path of the explosion, but many ways it can then react. It's doesn't make sense to describe the myriad ways an object can react to an explosion in a component, so we'll leave that up to a custom message handler on each object. So
ExplosionImpact might look like this:
enum ExplosionBarrier
{
None = 0,
Soft = 1,
Hard = 2,
}
class ExplosionImpact : Component
{
ExplosionBarrier Barrier;
}
That's pretty simple. Next, let's look at the innate properties of an explosion. This basically depends on the type of the bomb. But it's useful to distinguish bombs vs explosions, since bombs have additional properties like a countdown timer and an owner.
Explosions can:
- propagate any or all of 8 directions
- sometimes propagate through soft blocks (pass-through bombs, which have blue explosions)
- different propagation ranges (or an infinite range)
- sometimes propagate through hard blocks!
Power Bomb or Full Fire gives bombs infinite range
There are two obvious ways to model an explosion. You could have a single entity for an explosion, or an entity for each piece of the explosion as it propagates out (Bomberman is grid-based, so the latter is feasible). Given that an explosion can propagate unevenly depending on what it hits (as previously discussed), it would be somewhat tricky to represent with a single entity. It seems then, that one entity per explosion square would be reasonable. Note: this may make it seem tricky to render a cohesive image of an explosion to the screen, but you'd actually have a similar problem if your explosion was a single entity. A single entity would still need a complicated way to describe the shape of the overall explosion.
Dangerous Bombs explode in a square instead of a line
So let's take a stab at an
Explosion component:
class Explosion : Component
{
bool IsPassThrough; // Does it pass through soft blocks?
bool IsHardPassThrough; // Does it pass through hard blocks?
int Range; // How much further will it propagate?
PropagationDirection PropagationDirection;
float Countdown; // How long does it last?
float PropagationCountdown; // How long does it take to propagate to the next square?
}
enum PropagationDirection
{
None = 0x00000000,
Left = 0x00000001,
UpperLeft = 0x00000002,
Up = 0x00000004,
UpperRight = 0x00000008,
Right = 0x00000010,
BottomRight = 0x00000020,
Bottom = 0x00000040,
BottomLeft = 0x00000080,
NESW = Left | Up | Right | Bottom,
All = 0x000000ff,
}
Of course, since we're using ECS, things like position and the explosion image are handled by other components.
I've added two more fields to the
Explosion component that bear some mentioning:
Countdown represents how long an explosion lasts. It's not an instant in time - it lasts a short duration, during which a player can die if they walk into it. I also added a
PropagationCountdown. In Bomberman, from what I can tell, explosions propagate instantaneously. For no particular reason, I've decided differently.
So how does this all tie together? In an ECS, systems provide the logic to manipulate the components. So we'll have an
ExplosionSystem that operates over the
Explosion components. You can look at the sample project for the entire code, but let's briefly outline some of the logic it contains. First of all, it's responsible for propagating explosions. So for each
Explosion component:
- Update Countdown and PropagationCountdown
- If Countdown reaches zero, delete the entity
- Get any entities underneath the explosion, and send them a message telling them they are in an explosion
- If PropagationCountdown reaches zero, create new child explosion entities in the desired directions (see below)
ExplosionSystem also contains the propagation logic. It needs to look for any entities underneath it with an
ExplosionImpact component. Then it compares the
ExplosionImpact's
ExplosionBarrier with properties of the current
Explosion (
IsHardPassThrough, etc...) and decides if it can propagate and in what directions. Any new
Explosions have one less Range, of course.
Powerups
Next, we'll trace the path from collecting powerups to the player dropping bombs. I've used a subset of 12 of the typical Bomberman powerups (I've left out the ones that let you kick, punch and pick up bombs - I didn't have time to implement them for this article, but it could be a good follow-up). As before, let's look at our scenarios - what the powerups can do - and come up with a design.
- Bomb-Up: Increase by one the number of simultaneous bombs the player can place
- Fire-Up: Increase the blast radius (propagation range) of the bombs' explosions
- Speed-Up: Increase player speed
- (the above three also have "Down" versions)
- Full-Fire: Bombs have infinite range (except when combined with Dangerous-Bomb)
- Dangerous Bomb: Blast expands in a square, and goes through hard blocks
- Pass-Through Bomb: Blast propagates through soft blocks
- Remote Bomb: Bombs only detonate when you trigger them
- Land Mine Bomb: Your first bomb only detonates when someone walks over it
- Power Bomb: Your first bomb has infinite range (like Full-Fire but only for the first bomb)
Various powerups that have been revealed under disintegrated soft blocks
You'll see that while most powerups affect the kinds of bombs you place, they can also affect other things like player speed. So powerups are different concepts than bombs. Furthermore, powerups are not exclusive, they combine. So if you have a couple of Fire-Ups with a Dangerous Bomb, you get a bomb that explodes in a bigger square.
Pass-Through bombs propagate through soft blocks.
So essentially the player has a set of attributes that indicate what kinds of bombs they can place. The powerups modify those attributes. Let's take a stab at what a
PlayerInfo component would look like. Keep in mind, this won't contain information like position, current speed or texture. That information exists in other components attached to the player entity. The
PlayerInfo component, on the other hand, contains information that is specific to the player entities in the game.
class PlayerInfo : Component
{
int PlayerNumber; // Some way to identify the player - this could also be a player name string
float MaxSpeed;
int PermittedSimultaneousBombs;
bool FirstBombInfinite;
bool FirstBombLandMine;
bool CanRemoteTrigger;
bool AreBombsPassThrough;
bool AreBombsHardPassThrough;
int BombRange;
PropagationDirection BombPropagationDirection;
}
When a player drops a bomb, we look at its
PlayerInfo component to see what kind of bomb we should drop. The logic to do so is a bit complicated. There are lots of conditionals: for instance, Land Mine bombs look different than Dangerous Bombs that explode in all directions. So when you have a Land Mine that's also a Dangerous Bomb, what texture is used? Also, Power Bombs powerups give us infinite BombRange, but we don't want an infinite range when the bomb propagates in all directions (or else everything on the board will be destroyed).
So there can be some fairly complex logic here. The complexity arises from the nature of the Bomberman rules though, and not from any problem with code. It exists as one isolated piece of code. You can make changes to the logic without breaking other code.
We also need to consider how many bombs the player currently has active (undetonated): we need to cap how many they place, and also apply some unique attributes to the first bomb they place. Instead of storing a player's current undetonated bomb count, we can just calculate how many there are by enumerating through all
Bomb components in the world. This avoids needing to cache an
UndetonatedBombs value in the
PlayerInfo component. This can reduce the risk of bugs caused by this getting out of sync, and avoids cluttering the
PlayerInfo component with information that happens to be needed by our bomb-dropping logic.
With that in mind, let's take a look at the final piece of our puzzle: the bombs.
class Bomb : Component
{
float FuseCountdown; // Set to float.Max if the player can remotely trigger bombs.
int OwnerId; // Identifies the player entity that owns the bomb. Lets us count
// how many undetonated bombs a player has on-screen
int Range;
bool IsPassThrough;
bool IsHardPassThrough;
PropagationDirection PropagationDirection;
}
Then we'll have a
BombSystem that is responsible for updating the
FuseCountdown for all
Bombs. When a
Bomb's countdown reaches zero, it deletes the owning entity and creates an new explosion entity.
In my ECS implementation, systems can also handle messages. The
BombSystem handles two messages: one sent by the
ExplosionSystem to entities underneath an explosion (which will trigger the bomb so we can have chain reactions), and one sent by the player's input handler which is used for remotely triggering bombs (for remote control bombs).
One thing you'll notice is that the
Explosion,
Bomb and
Player components share a lot in common: range, propagation direction,
IsPassThrough,
IsHardPassThrough. Does this suggest that they should actually all be the same component? Not at all. The logic that operates over those three types of components is very different, so it makes sense to separate them. We
could create a
BombState component that contains the similar data. So an explosion entity would contain both an
Explosion component and a
BombState component. However, this just adds extra infrastructure for no reason - there is no system that would operate only over
BombState components.
The solution I've chosen is just to have a
BombState struct (not a full on Component), and
Explosion,
Bomb and
PlayerInfo have this inside them. For instance,
Bomb looks like:
struct BombState
{
bool IsPassThrough;
bool IsHardPassThrough;
int Range;
PropagationDirection PropagationDirection;
}
class Bomb : Component
{
float FuseCountdown; // Set to float.Max if the player can remotely trigger bombs.
int OwnerId; // Identifies the player entity that owns the bomb. Lets us count
// how many undetonated bombs a player has on-screen
BombState State;
}
One more note on players and bombs. When a bomb is created, it inherits the abilities of its player at the time it is placed (
Range, etc...) instead of referencing the player abilities. I believe the actual Bomberman logic might be different: if you acquire a Fire-Up powerup, it affects already-placed bombs. At any rate, it was an explicit decision I made that I was felt was important to note.
A soft block is no protection against a Pass-Through bomb (spiked)
Let's finally talk about the powerups themselves. What do they look like? I have a very simple
PowerUp component:
class PowerUp : Component
{
PowerUpType Type;
int SoundId; // The sound to play when this is picked up
}
PowerUpType is just an enum of the different kinds of powerups.
PowerUpSystem, which operates over
PowerUp components and controls picking them up, just has a large switch statement that manipulates the
PlayerInfo component of the entity that picked it up. Oh the horror!
I did consider having different message handlers attached to each powerup prefab which contained the custom logic for that particular powerup. That is the most extensible and flexible system. We wouldn't even need a
PowerUp component or
PowerUpSystem. We'd simply define a "a player is colliding with me" message which would be fired and picked up by the custom powerup-specific message handler. This really seemed like over-architecting to me though, so I went with a simpler quicker-to-implement choice.
Here's a little snippet of the switch statement where we assign the player capabilities based on the powerup:
case PowerUpType.BombDown:
player.PermittedSimultaneousBombs = Math.Max(1, player.PermittedSimultaneousBombs - 1);
break;
case PowerUpType.DangerousBomb:
player.BombState.PropagationDirection = PropagationDirection.All;
player.BombState.IsHardPassThrough = true;
break;
Prefabs
My ECS allows you to construct entity templates, or prefabs. These assign a name to a template (e.g. "BombUpPowerUp"), and associate with it a bunch of Components and their values. We can tell our EntityManager to instantiate a "BombUpPowerUp", and it will create an Entity with all the right Components.
Visual representation of various prefabs
I think it would be useful to list some of the prefabs I use for the Bomberman clone. I won't go into details on the values used in each; I'll simply list which Components each type of entity uses, with some comments where useful. You can look at the source code for more details. These are just examples of prefabs - e.g. in the actual game there are multiple types of Brick (SoftBrick, HardBrick) with different values in their components.
"Player"
Placement
Aspect
Physics
InputMap // controls what inputs control the player (Spacebar, game pad button, etc...)
InputHandlers // and how the player reacts to those inputs (DropBomb, MoveUp)
ExplosionImpact
MessageHandler
PlayerInfo
"Brick"
Placement
Aspect
Physics
ExplosionImpact
MessageHandler // to which we attach behavior to spawn powerups when a brick is destroyed
"Bomb"
Placement
Aspect
Physics
ExplosionImpact
MessageHandler
ScriptContainer // we attach a script that makes the bomb "wobble"
Bomb
"Explosion"
Placement
Aspect
Explosion
FrameAnimation // this one lets us animation the explosion image
"PowerUp"
Placement
Aspect
ExplosionImpact
ScriptContainer
PowerUp
Interesting Points
An ECS also gives you great flexibility at creating new types of entities. It makes it easy to say "hey, what if I combine this with that?". This can be good for brainstorming new kinds of mechanics. What if you could take control of a bomb? (Add
InputMap to a bomb entity). What if explosions could cause other players to slow down? (Add
PowerUp to an explosion entity). What if explosions were solid? (Add
Physics to an explosion entity). What if a player could defect an explosion back towards someone? (A little bit of logic to add, but still pretty trivial).
You'll find that it is very easy to experiment and add new code without breaking other things. The dependencies between components are (hopefully) clear and minimal. Each pieces of code operates on the absolute minimum it needs to know.
Of course, I also faced some problems in this little project.
I decided to use the
Farseer Physics library to handle collision detection between the player and other objects. The game is grid-based, but the player can move on a much more granular level. So that was an easy way to get that behavior without having to do much work. However, a lot of the gameplay is grid-based (bombs can only be dropped at integer locations, for instance). So I also have my own very simple grid collision detection (which lets you query: "what entities are in this grid square?"). Sometimes these two methods came into conflict. This problem isn't anything specific to ECS though. In fact, ECS ecnourages my usage of Farseer Physics to be entirely limited to my
CollisionSystem (which operates over
Physics components). So it would be very easy to swap out the physics library with another and not have it affect any other code. The Physics component itself has no dependency on Farseer.
Another problem I faced is that there is a tendency to make certain problems fit into the ECS way of thinking. One example is the state needed for the overall game: the time remaining, the size of the board, and other global state. I ended up creating a
GameState component and an accompanying
GameStateSystem. GameStateSystem is responsible for displaying the time remaining, and determining who won the game. It seems a bit awkward to cram it into the ECS framework - it only ever makes sense for there to be one
GameState object. It does have some benefits though, as it makes it easier to implement a save game mechanic. My Components are required to support serialization. So I can serialize all entities to a stream, and then deserialize them and end up exactly back where I was.
One decision that I often faced was: "Do I make a new Component type for this, or just attach a script for custom behavior?" Sometimes it is a fairly arbitrary decision as to whether a piece of logic merits its own Component and System or not. A Component and System can feel a bit heavyweight, so it is definitely essential to have the ability to attach custom behavior to entities. This can make it harder to grok the whole system though.
I currently have 3 ways of attaching custom behavior to an entity: input handlers, message handlers and scripts. Scripts are executed every update cycle. Input and message handlers are invoked in response to input actions or sending messages. I was trying out a new input handler methodology for this project. It worked well (but it might make sense to combine it with message handling). I was using the keyboard to control the player. When it came time to implement gamepad support, it took all of five minutes. I was inspired by
this article.
A powerup entity (generic container) is defined by its components: Placement, Aspect, ExplosionImpact, ScriptContainer and PowerUp. ScriptContainer allows attaching scripts for custom behavior. In this case, a wiggle script is responsible for wiggling the powerup around.
Scripts often need to store state. For instance, a script that makes a powerup wiggle, or a bomb entity wobble (by changing the
Size property in its
Aspect component) needs to know the minimum and maximum sizes, and what point we are in the wobble progression. I could make scripts full-fledged classes with state, and instantiate a new one each each entity that needs it. However, this causes problems with serialization (each script would need to know how to serialize itself). So in my current implementation, scripts are simply callback functions. The state they need is stored in a generic property bag in the
Scripts component (the
Scripts component simply stores a list of ids that are mapped to a specific callback function). This makes the C# script code a little cumbersome, as each get and set of a variable is a method call on the property bag. At some point, I plan to support a very simple custom scripting language with syntactic sugar to hide the ugliness. But I haven't done that yet.
Conclusion
Theory is nice, but I hope this article helped with showing a practical implementation of some mechanics with ECS.
The sample project attached is implemented in XNA 4.0. In addition to the mechanics described in this article, it shows some other things which might be interesting:
- How I handle animating the components like explosions
- The input mapping system I briefly described above
- How I handle querying objects at a particular grid location
- How little things like the bomb wobble or land mine rise/fall is done
I didn't have time to implement AI players in the sample, but there are 3 human-controllable characters. Two of them use the keyboard: (arrow keys, space bar and enter) and (WASD, Q and E). The third uses the gamepad, if you have that. It should be possible to implement a mouse-controlled player without too much work.
When time runs out, death blocks appear...
The sample features 12 full-functioning powerups (some of the more powerful ones appear too frequently though), random soft blocks, and the "death blocks" that start appearing when time is running out. Of course, a lot of polish is missing: the graphics are ugly, there is no death animation or player walking animation. But the main focus is on the gameplay mechanics.
Article Update Log
24 May 2013: Initial draft
Informative article. Very easy-to-digest article that really puts the ECS into good use. Thanks for sharing this!
A note: I know you provided the source code, but it would be nice if you could put up a small example of methods and logic of a system as well. Just briefly, as you have with components above.