Jump to content

  • Log In with Google      Sign In   
  • Create Account


Like
22Likes
Dislike

Case Study: Bomberman Mechanics in an Entity-Component-System

By Philip Fortier | Published Aug 15 2013 01:26 PM in Game Programming
Peer Reviewed by (jbadams, Endurion, Dave Hunt)

ecs xna game mechanics

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


Attached Image: Impacts123.jpg
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


Attached Image: ChainReaction.jpg
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!


Attached Image: InfiniteRange.jpg
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.


Attached Image: SquareBomb.jpg
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)


Attached Image: PowerUps.jpg
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.


Attached Image: PassThrough.jpg
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.


Attached Image: Killed.jpg
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.


Attached Image: Prefabs.jpg
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.


Attached Image: Diagram.jpg
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.


Attached Image: DeathBlocks.jpg
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





License


The Microsoft Public License (Ms-PL)




Comments

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.

Interesting and informative article. It seems to have little to do with real bomberman though.

 

Bomberman is a product of an 8bit/16bit era and ran on systems that had very little ram and weren't programmed in higher level languages. As such there were no "powerup components". Powerups were just tiles placed in the map. Making them components and or having them as active game objects would have taken too much ram and too much processing. As tiles they are effectively static. The player tries to take a step in the direction of the powerup. The code looks at the tile, sees it's a powerup tile, replaces the tile with a ground tile and sets a flag in the player they he now has that powerup (or one more bomb).

 

As another example bombs don't expand. They blow up at their defined size instantly. (Google SNES Bomberman video to see).

The article is a great introduction into ECS but it's also arguably an introduction into over engineering. Sorry, that sounds like a judgement and arguably anyway your game works is fine.  It's just the limits of old systems forced or suggested different solutions. Those solutions have their own advantages and disadvantages.

For example, using the tile based (non GameObject) solution you can easily make a level that is filled with bombs because the bombs themselves are tiles. They can only be placed on tile boundaries. When you place one the ground tile is replaced with bomb tile. That means having 1 or 1000 bombs takes the same amount of processing. They only become gameobjects when they are moving and usually only a few are moving at any one time. Possibly max 2 per player. In other words the bomb is tile, if the player kicks a bomb tile (and has the kick powerup) the tile is replaced with a ground tile, a gameobject is spawned, the bomb moves down the hall until it hits the end or the player stops it, the closest ground tile is changed to back to a bomb tile and the gameobject is destroyed.

 

The bombs can appear to "tick" using tile animation. Rather than a gameobject per bomb all you need is an array of bomb tile locations and a time they were placed. A single function walks the array, any bomb who's time is up explodes the tile. So much less overhead. In fact because bombs are added to the array/queue as they are placed the code only has to check the oldest bomb. So in general only one bomb needs to be checked each frame to see if it's time to explode (or 2 or 3 for those rare occasions where 2 or more players put bombs down on exactly the same frame)

 

Even explosions are tiles. The moment a bomb explodes the code goes and replaces ground tiles down the aisles with explosion tiles. That means all the aisles on the entire board can be explosion tiles and again there is no processing. (tile animation effectively free on those old systems and can be replicated to be free on modern GPUs. Similarly it makes collisions easy. Each player just checks the max 2 tiles they are straddling. If they are explosion tiles they player dies.

 

In gameobject terms, the original bomberman likely only had 1 gameobject per player and 1 gameobject per moving bomb. Everything else was tiles with a few arrays to remember which tiles to change later (like changing explosion tiles back into ground tiles when the explosion is done)

 

Having bombs as tiles also has other advantages. Collisions are simple. The player checks if the tile he's on is an explosion tile, if so he dies, then he checks in the direction he's moving. If that's explosion he dies, if it's a powerup he changes it to a ground tile, if it's ground he moves, if it's anything thing else he can't go that way. So, bombs become barriers without any extra code.

I wouldn't even be surprised if kicked bombs sliding down hallways weren't placing tiles in the hallway as they slide down it. Either tiles with a bomb image offset or some tiles that looked like ground but were actually "bomb is sliding here" tiles. That way if another player tried to enter an aisle at the exact time a bomb is sliding by his tile checking code will prevent him from walking into the tile and so you wouldn't need any special "is there a gameobject version of a bomb here" collision code. Similarly the tile/tiles under each player might also be special "play is standing here" tiles so similarly a bomb sliding down a hallway will know to how to stop just by checking the tile in front of them instead of having to check gameobject positions.

 

All of these solutions make this type of game possible on a 1mhz 6502 or 65816 machine but they also have benefits. Collisions are easy, explosions, bombs, powerups, take nearly zero processing time.

Interesting and informative article. It seems to have little to do with real bomberman though.

 

Well, I wasn't making any attempt to "stay true" to the original bomberman code (if anything, the opposite). I just chose Bomberman because it has an interesting set of gameplay mechanics that most people are familiar with.

 

 

All of these solutions make this type of game possible on a 1mhz 6502 or 65816 machine but they also have benefits. Collisions are easy, explosions, bombs, powerups, take nearly zero processing time.

 

 

I'm sure you're right that performance would be better - but that kind of speed is completely unnecessary, and you'd end up with bug-prone code that is difficult to modify or extend (for a small game like Bomberman, it may not be a big deal). Never a good trade-off. And you'd have to do yucky things like the following:

 

 

 Everything else was tiles with a few arrays to remember which tiles to change later (like changing explosion tiles back into ground tiles when the explosion is done)

 

 

Modern hardware allows us the luxury of superior coding practices. There's no way you'd be able to efficiently create a typical modern game with the type of code that the original Bomberman would have been limited to.

The bombs can appear to "tick" using tile animation. Rather than a gameobject per bomb all you need is an array of bomb tile locations and a time they were placed. A single function walks the array, any bomb who's time is up explodes the tile. So much less overhead. In fact because bombs are added to the array/queue as they are placed the code only has to check the oldest bomb. So in general only one bomb needs to be checked each frame to see if it's time to explode (or 2 or 3 for those rare occasions where 2 or more players put bombs down on exactly the same frame)

 

How is that less overhead? Did you actually look at the article's code? That's pretty much exactly how it works. There's an array of Bomb components that the BombSystem iterates over and decreases their timer. It's extremely simple. (And yes, you do need to check all bombs).


Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS