Writing

Bomberman Clone

Published 7 years ago Ā· Updated 2 years ago

Introduction and Scope

We got an assignment in the course ā€˜Web- and Multimedia Technologiesā€™, in which we learn the fundamental usage of web technologies. We had to implement a given project idea or suggest our own. I pitched my idea of building an easy multiplayer, web-based clone of Bomberman.

I figured the following scope for the game:

  • Multiplayer (socket.io, Node.js)
  • Session Management (Node.js)
  • Boosts (more šŸ’£s!)
  • Username Login
  • High-score saved by username
  • Desktop / Keyboard navigation only

Whatā€™s may interesting for game development:

  • The server holds the complete game state
  • The client serves as a simple output (e.g.Ā no predictions)
  • Collision detection is square based
  • Moving figures is step based

Architecture

Communication

I thought the easiest way to sync the game state to multiple clients is to put the server 100% in charge of everything. Meaning that the client holds no logic according to the game. Itā€™s basically like a monitor outputting the game state and being able to send keyboard inputs. All the rest is handled by the server.

The server has two events to communicate in the direction of the client: initalize_world and update_world, where initalize_world just means 1st update_world with some initialization data on top and update_world is where the game state is being delivered to the client.

The client is capable of sending the following commands to the server:

  • player_add User joined the game with a username
  • player_leave Client was disconnected or user decided to leave the game
  • player_move User wants to move in a certain direction
  • player_plant User wants to plant a bomb

The player_move event gets the parameter of the direction. All other events donā€™t need parameters.

The latency event you see in the illustration shows the back-and-forth call to measure the latency.

Directory Structure

The assets folder holds image, style and audio files. The components on both ends is a general term for a module this could be either one function or a class that has a single responsibility. The entities are representing parts of the game like a figure or a tile. The index.js and index.html are entry points for each application. The lib folder is for third-party libraries.

Data Structure and Object Orientation

I wanted to make sure that every entity can handle their own behavior e.g.Ā a bomb could count down and create an explosion afterward on itself (capsuled) instead of having a logic layer above which would manage all timers and all explosions and so on. Besides to that, I created the separation of entities and components on the server.

Server-Side

Using JavaScript classes I created the following server-side components:

  • world A container for the game logic (incl.Ā collision detection, moving a player, ā€¦) and used as a data store for all entities
  • world-generator This component generates a tileset and the boosts
  • player-statistics A simple store to save the player statistics

The world component is the data store for all entities so itā€™s holding the current game state aka the world.

  • player An entity that represents a joined player (also as a figure)
  • tile A tile with a type of grass, stone, treeā€¦
  • bomb A entity representing the logic of a bomb
  • boost A entity representing the logic of a boost
  • explosion A entity representing the logic of an explosion

Making sure the client only receives the data it needs to render, every entity has a serialize() function which returns the data visible to the client.

Example from the bomb entity (source code):

serialize() {
  return {
    x: this.x,
    y: this.y,
    detonated: this.detonated,
    timer: this.timer
  }
}

Generating the whole game state by calling getData() in the world component looks like this:

getData() {
  return {
    players: Object.keys(this.players).map((key, i) => {
      this.players[key].stats = this.playerStats.get(key);
      return this.players[key].serialize()
    }),
    tiles: this.tiles.map(t => t.serialize()),
    bombs: this.bombs.map(b => b.serialize()),
    explosions: this.explosions.map(e => e.serialize()),
    boosts: this.boosts.map(b => b.serialize())
  }
}

A possible game state could look like this:

{
  "players": [{
    "id": "a894f3ce-c70e-49a4-ac0f-b56a1537eed4",
    "username": "dwadaw",
    "stats": {
      "dies": 0,
      "kills": 0
    },
    "dead": false,
    "x": 96,
    "y": 400,
    "color": "#f4b3bf",
    "type": 3,
    "boosts": []
  }],
  "tiles": [{
    "x": 400,
    "y": 80,
    "type": 5
  }, {
    "x": 416,
    "y": 80,
    "type": 6
  }, {
    "x": 400,
    "y": 96,
    "type": 7
  }, ā€¦],
  "bombs": [],
  "explosions": [],
  "boosts": [{
    "x": 288,
    "y": 80,
    "type": 12
  }, {
    "x": 368,
    "y": 112,
    "type": 12
  }, {
    "x": 96,
    "y": 160,
    "type": 12
  }, ā€¦]
}

Client-Side

Given this game state, I used the same structure on the client. Having JavaScript classes representing a visual texture on the canvas (texture, bomb, boost, explosion, figure, tile). Every entity is extending texture, giving them attributes like a position and a size.

And on the other hand helpers for playing audio or holding the client state persistent (sprite, audio-player, client, uuid).

  • world The world component hold the logic how to render to the canvas
  • texture Parent class for the bomb, boost, explosion, figure, and tile. Holding visual representation information (e.g.Ā position)
  • bomb Representation of a bomb
  • boost Representation of a boost
  • explosion Representation of an explosion
  • figure Representation of a players figure
  • tile Representation of a single tile
  • sprite Helper to preload the texture sprite
  • audio-player Audio preloader and player
  • client Client class which handles its persistence to local storage

The update function of the clientsā€™ world component creates new instances of all entities when they were changed (source code to change detection here). The draw() function then takes all entities and calls their own draw() function. The entities get initialized with the worlds canvas context so they can draw itself to the canvas.

Both Sides

Then there is the shared folder which gives both sides access to constants e.g.Ā for tile types and boosts types. (source code)

Conclusion

What did work out?

  • The ā€˜only update when neededā€™ approach worked out better than I thought. The data flows were pretty obvious and only some little tweaks were needed to have some change detection (e.g.Ā play the boost sound when picked up).
  • By the same reason that the server is handling the whole game logic, there is no redundant game logic spread across the server and the client.

What to do differently next time?

  • Donā€™t underestimate latency issues. Thatā€™s what Iā€™ve learned. The whole game is only enjoyable when having a proper latency to the server (WiFi is already a bit limited). The player only moves after the server accepted the request and sent back the new game state. Having a latency of 100ms makes it already feel laggy.
  • The change detection could be done on a more detailed level. For example instead of checking if anything on an object changed and creating a new one. Itā€™s possible to use the same object on both sides and only update the visual representation.
  • Use a matrix based data structure for the game if you use step based movement for your players. I used objects in an array with x, y attributes. After attending the first session of ā€˜Algorithms and Data Structuresā€™ itā€™s pretty obvious how much more efficient a matrix based grid layout for tiles and other entities can become. The ability to quickly identify which consequences follow is important in every moment the player interacts with the world (e.g.Ā collision detection).
  • Use an easy frontend framework even for small applications. I used only Vanilla JS to map events and showing the leaderboard. After some small changes, it wouldā€™ve become handy to manage DOM changes with a framework designed to do that. Examples: ReactJS, AngularJS, VueJS. On the limitation of time, I decided to not add a frontend economy overhead to the project.