Hi, I'm Adam. Email me about: CTOs, Mobile/iOS, Project Management, and Development

Designing Bomberman with an Entity System: Which Components?

Today I found a new article on implementing a Bomberman clone using an Entity System (in C#) – but I feel it doesn’t quite work.

It’s a well-written article, with some great illustrations. But the authors “Components” struck me as over-complicated and too behaviour-focussed. If you’re designing a game based on an ES, this isn’t a great way to do it – it feels too much towards old-style OOP gameobjects.

This is an opportunity to talk about good and bad directions in Component design. I really like the author’s idea of using Bomberman as a reference – it’s simple, it’s a well-known game, but it’s got a lot of depth. Read the original article to refresh your memory on Bomberman and how it works. Then come back here, and we’ll do a worked example on choosing components for a game. I’ll completely ignore the programming issues, and only look at the design issues.

Choosing your Components

Design rules for Components (rules of thumb):

  1. Components are data, not code
    • …game-state, not functionality
  2. Components are as small as possible
    • …atoms: the smallest set of data possible, for a particular aspect of the entity
  3. Components never share data
    • …in Database terms: everything is Normalized (Google it)
    • …if data should be in two components, but you can’t decide which: create a third component for it

Looking at Bomberman

List the behaviours, list the data

This is an iterative process. I start with two lists written at the same time. Writing items in one list often makes you think of something that’s needed in the other list, so you keep jumping back and forth.

Here’s a first draft (probably incomplete, but it’s a good start):

Behaviours: Bomberman

  1. move a player up/down/left/right
  2. drop a bomb
  3. detonate a radio-controlled bomb
  4. kick a bomb
  5. jump one space (some versions)
  6. teleport a player (environmental)
  7. kill a player (environmental)
  8. upgrade a player’s FUTURE bombs (don’t affect the ones already on floor)
  9. dissolve a block
  10. spread/block the explosion according to several possible rules (depends upon multiple factors)

Data: Bomberman

  1. Position of each player in pixels (for visual representation)
  2. Cell that each player is standing in (for the game logic)
  3. bombs in play
  4. fuse-time left on each bomb
  5. explosion logic for each bomb (tiles covered)
  6. explosion logic for each bomb (tiles affected)
  7. explosion logic for each bomb (tiles that block / shadow the coverage)
  8. score for each player (number of kills, number of deaths)
  9. positions of all solid blocks
  10. positions of all soft blocks
  11. positions of all powerups
  12. positions of all special features of environment (e.g. teleporters)

Convert Data into Components

(remember: stick to the rules for designing a component)

My preferred way to do this is convert ALL the data into primitive data types (i.e.: int, float, string, boolean, enum, list, map).

For a simple game, you should never need “list” and “map”

The ones I tend to use most often: enum, float, int, boolean, string

This list often contains duplicates; that’s fine, we’ll come back to it in a sec.

All data, converted into primitives

  1. Position of each player in pixels (for visual representation)
    • position.xpixels (int)
    • position.ypixels (int)
  2. Cell that each player is standing in (for the game logic)
    • position.xcell (int)
    • position.ycell (int)
  3. bombs in play
    • position.xcell for each
    • position.ycell for each
  4. fuse-time left on each bomb
    • fuse.secondsLeft (float)
  5. explosion logic for each bomb (tiles covered)
  6. explosion logic for each bomb (tiles affected)
  7. explosion logic for each bomb (tiles that block / shadow the coverage)
    • this is where it gets tricky: the data you store is heavily dependent on how you intend to write the “behaviour” code later on. Let’s skip it for now
  8. score for each player (number of kills, number of deaths)
    • score.kills (int)
    • score.deaths (int)
  9. positions of all solid blocks
    • position.xcell
    • position.ycell
    • we could assume that “default” squares are solid, or empty. Let’s assume “solid”, so that anything off the edge of the playing area is inaccessible by default
  10. positions of all soft blocks
    • position.xcell
    • position.ycell
    • solid.isSolid (boolean)
    • material.hitpointsRemaining (int)
  11. positions of all powerups
    • position.xcell
    • position.ycell
    • powerup.type (enum)
  12. positions of all special features of environment (e.g. teleporters)
    • position.xcell
    • position.ycell
    • solid.collisionEffect (enum)

Components, after some wrangling

Now … spend some time looking at that list and thinking about it. Think about duplicates: all those “position.xcell” items – ARE THEY REALLY THE SAME THING?

(if they’re not, if some of them are “sort of the same, but actually different – and might change in future”, then you need to have two types of Position component. This already happened right at the start when I realised that the “on screen” position of the sprites is similar-but-different from the cells in which each bomb sits. One is finer granularity than the other. A very simple implementation of Bomberman could have them the same component – but it’s obvious that you’d want to “upgrade” this after only a few builds, and add pixel-perfect positions, because it looks so much better)

  1. CellPosition
    • x (int)
    • y (int)
  2. ScreenPosition
    • x (int)
    • y (int)
  3. TimedEffect
    • timeRemaining (float)
    • effectType (enum: EXPLODE1, EXPLODE2, VANISH)
  4. Explosion
    • distance (int)
    • affectsSolids (boolean)
    • affectsSofts (boolean)
    • affectsPlayers (boolean)
    • penetratesSolids (boolean)
    • penetratesSofts (boolean)
    • penetratesPlayers (boolean)
    • damage (int)
  5. PlayerBombs
    • …everything from Explosion?
  6. Score
    • kills (int)
    • deaths (int)
  7. Destroyable
    • hitPoints (int)
  8. Collideable
    • blocksPlayers (boolean)
    • blocksFlameDamage (int)
    • addsFeature (enum: FLAME_LENGTH, BOMB_AMOUNT)
    • triggersEffect (enum: DEATH, SELF_BOMBS, TELEPORT)

That was my first attempt. It is poor: by the time I got to the end, I’d changed the way I was thinking about concepts compared to the beginning. The list above is self-contradictory, it has to be re-done

By the end, I’d realised a couple of things. Firstly, the “data” of explosions can be used to model most of the flame Behaviours, so long as we treat each “flame” square like a depth of water that “flows” to either side, getting weaker by one point each time it moves an extra square.

Secondly, I noticed that my “Explosion” component was getting very large – remember: Components should be as small as possible.

Thirdly, the enums inside my Collideable were very non-homogenous. i.e. they seemed to be serving a lot of purposes at once. This is a warning sign with ES design: “heterogeneous data” is what your Components are for: heterogeneous enums suggest you need to split your Component

So, we have two big changes to make on a second draft. We’re going to re-define everything to do with explosions in terms of “water that flows”, and we’re going to split two of the components into smaller pieces (one because it’s “too big”, the other because it’s “too heterogeneous”).

Components, Attempt 2

  1. Explosion
    • power (int) — decreases by 1 each square it spreads. At 0, it runs out.

I got this far, and realised: “power” suggests “damage” — but what I really mean here is “spreading power”. I thought for a moment: could I ever want to use this spreading effect for something other than damage?

Yes! What about a powerup that “explodes” in a radius? Cool. It would be EXACTLY THE SAME thing, but it proves the name I’d chosen at first was a bad one.

So, I hastily renamed both the Component and that attribute:

  1. CellPosition
    • x (int)
    • y (int)
  2. ScreenPosition
    • x (int)
    • y (int)
  3. TimedEffect
    • timeRemaining (float)
    • effectType (enum: SPREAD, VANISH) — simplified now: spreading is neatly contained inside Spreadable below.
  4. Spreadable
    • depth (int) — decreases by 1 each square it spreads. At 0, it runs out.
    • spreadPattern (enum: FLOW_AROUND_OBSTACLES, FLOW_CARTESIAN, FLOW_IGNORE_OBSTACLES ) — the Behaviour will interpret these
  5. Score
    • kills (int)
    • deaths (int)
  6. Destroyable
    • hitPoints (int)
  7. Collideable
    • height (int) — if height > Spreadable.depth, we flow AROUND instead of OVER. Also, we can give players a “height” they can move over. This also lets us implement “jumping” in terms of increasing (for a single step) the height you can move over (makes it easy for us to have “jumpable” and “non-jumpable” squares).
  8. Damager
    • inflictDamage (int)
  9. BombLayer — (something that lays bombs … not a rendering “layer”)
    • spread (int)
    • depth (int)
    • damage (int)
  10. Teleporter
    • cellsDeltaX (int) — +1 = right one square, -1 = left one square
    • cellsDeltaY (int) — +1 = down one square, -1 = up one square
  11. PowerupPlayer
    • addsFeature (enum: FLAME_LENGTH, BOMB_AMOUNT)
    • amount (int) — +2 has double the effect, +3 has triple, etc

Exercises for the reader…

There’s a bunch of holes in what I’ve written above; this post is meant to be indicative of what I’d be doing, rather than a complete game-design document. At this point, I think you have enough to fill in the blanks easily – if not, post in the commments and I’ll add some more info and guidance.

In particular, I look at this and ask the following questions:

  1. Without changing anything, can you implement powerups that disappear if not collected quickly enough? (easy)
  2. …what about: powerups that disappear when the player walks on them? (less easy)
  3. How do make a powerup that’s very powerful, but has a risk of doing something nasty to the player – e.g. teleporting them when they collect it?
  4. Or a powerup that has a chance to kill the player outright?

Consider the possibilities of your new Components

I like to do this as a sanity-check that my Components are as flexible as they should be. But … it’s also inspirational to look at your system and see what emergent features it’s already created.

We look at our components and take odd pairings we didn’t intentionally design, and ask what would happen if we put them together.

e.g.: “TimedEffect { timeRemaining = 2, effectType = SPREAD }” + “TimedEffect { timeRemaining = 4, effectType = VANISH }” + “Spreadable { depth = 3, spreadPattern = FLOW_CARTESIAN }” + “PowerupPlayer { addsFeature = FLAME_LENGTH, amount = 2 }”

  • Armageddon!
  • Drop one of these when the level reaches a timeout (e.g. 5 minutes), and it will spread through the level, powering-up every player it hits, and fading away as it passes, like a wave rippling outwards
    • This behaviour/feature is entirely automatic, requires zero additional code!
  • I set the VANISH to take two more ticks than SPREAD, on the assumption that my “spread” behaviour will only spread into tiles where the effect doesn’t already exist. That’s a sensible implementation.
    • …this way, the spread is always outwards, because the inner ring of “about to fade” blocks prevent the spread going backwards into the middle again
    • If you tweaked that Behaviour (maybe add an extra flag to Spreadable, e.g. “(boolean) spreadsIntoTilesAlreadyPresent”), you could make this into a reverberating wave, that bounces back and forth forever across the level.
    • You could make a whole level around this: have a couple of waves that bounce back and forth giving powerups, and have no other powerups in the level. Players get powered up rapidly by the waves washing over them…

Did this post help you?

You can say “thank you” by giving me your email address, and letting me contact you next time I make a game of my own:

[smlsubform emailtxt=”” showname=” mailinglist=”pif-entity-systems”]

70 thoughts on “Designing Bomberman with an Entity System: Which Components?”

Comments are closed.