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:
Whatās may interesting for game development:
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:
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.
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.
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.
Using JavaScript classes I created the following server-side components:
The world component is the data store for all entities so itās holding the current game state aka the world.
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
}, ā¦]
}
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).
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.
Then there is the shared folder which gives both sides access to constants e.g.Ā for tile types and boosts types. (source code)