A scalable networking framework to build realtime multiplayer games with simultaneously running game rooms
This starter kit allows you to add multiplayer functionality (that follows the Client/Server strategy) to your game. It provides a communication framework so that your players can communicate with a central server, in realtime, for the entire duration of the gameplay.
It also allows you to implement a 'game rooms' feature using Node JS worker threads, allowing you to spin up multiple instances of the game, each with a separate group of players.
-
Multiplayer space invaders - GitHub project | Tutorial | Demo
-
Multiplayer Flappy birds - GitHub Project | Video tutorial
(...make a PR to add yours!)
-
Clone this repo
git clone https://github.com/Srushtika/multiplayer-scalable-game-networking-starter-kit.git
-
Change directory into the project folder
cd multiplayer-scalable-game-networking-starter-kit.git
-
Install the dependencies
npm install
-
Create a free account with Ably Realtime to get your Ably API KEY. Add a new file in called
.env
and add the following. (Remember to replace the placeholder with your own API Key. You can get your Ably API key from your Ably dashboard):ABLY_API_KEY=<YOUR-ABLY-API-KEY> PORT=5000
-
Run the server
node server.js
-
To see the realtime communication in action, open the app in two separate browser windows side-by-side: https://localhost:5000
-
Create a game room in one window and join the room as a 'non-host' player by using the unique code in the other window.
-
Start the game when you are ready and use the arrow keys to publish dummy player input. You will see the change happening at the same time in both the windows.
Voila! Your multiplayer game networking framework is up and running, now replace the game logic with yours and make it your own.
Feel free to share your multiplayer game with me on Twitter, I'll be happy to give it a shoutout!
This file has the main server thread. It performs three functions:
- Serve the HTML and JS for front-end clients using Express.
- Authenticate front-end clients with the Ably Realtime service using Token Auth strategy.
- Create and manage Node JS worker threads when a host player requests to create a new game room.
This file represents a Node JS worker thread and a new instance of this file will run for every game room created. When the game is finished, the worker thread is killed. When a new player joins or leaves the worker thread communicates with the parent thread (main thread aka server.js) and lets it know the number of players (among other things).
These files represent the home page where a player can choose to host a new game or join a game using the room's unique code.
As the names suggest, these are the HTML files for the host player and non-host player respectively. Being the host of the game allows for additional controls like starting or stopping the game, hence the different views.
a. Staging area for host player
b. Game area for host player
c. Staging area for non-host player
d. Game area for non-host player
Again as the names suggest, these pages are shown when a non-host player tries to join a game using a unique code, either when the game has already started or that game code doesn't exist, respectively.
This file contains the main front-end logic for both host and non-host type players. It uses ES6 modules to import two other files:
This file contains all the methods that render the game UI on the webpage as per the latest realtime updates.
This file contains all the utility methods used in the game.
This file has all the styles that are applied on top of Bootstrap styles.
Before we look at the architecture and design of the app, we need to understand a few concepts based on which this app is built
The Client-Server game building strategy allows for high scalability when compared to the Peer-to-Peer strategy. In this design, the game server can be considered as the single source of truth and is responsible to maintain the latest game state at all times. All the players send their state to the game server, which in turn collates them together and sends it back at the same time to all the players.
The client-side script will use this state received from the game server to render the game objects on players' screens accordingly, ensuring all the players are fully in-sync.
The WebSockets protocol, unlike HTTP, is a stateful communications protocol that works over TCP. The communication initially starts off as an HTTP handshake, but if both the communicating parties agree to continue over WebSockets then the connection is elevated; giving rise to a full-duplex, persistent connection. This means the connection remains open for the duration that the application is in use. This gives the server a way to initiate any communication and send data to pre-subscribed clients, so they don’t have to keep sending requests inquiring about the availability of new data. Which is exactly what we need in our game!
This project uses Ably Realtime to implement WebSocket based realtime messaging between the game server and the players. Ably, by default, deals with scalability, protocol interoperability, reliable message ordering, guaranteed message delivery historical message retention and authentication, so we don't have to. This communication follows the Pub/Sub messaging pattern.
Pub/Sub messaging allows various front-end or back-end clients to publish some data and/or subscribe to some data. For any active subscriptions, these clients will receive asynchronous event callbacks when a new message is published.
In any realtime app, there's a lot of moving data involved. Channels help us group this data logically and let us implement subscriptions per channel. This allowing us to implement the custom callback logic for different scenarios. In the diagram above, each color would represent a channel.
Presence is an Ably feature using which you can track the connection status of various clients on a channel. In essence, you can see who has just come online and who has left using each client's unique clientId
This kit uses Node JS worker threads to create new game rooms so various groups of people can simultaneously play the game.
To create and use Node JS worker threads, from the main thread, you'll need to require the worker_threads
library:
const {
Worker,
isMainThread,
parentPort,
workerData,
threadId,
MessageChannel,
} = require('worker_threads');
and instance a new worker and pass it two parameters:
a) path to the worker file
b) data as a JSON object
const worker = new Worker('./game-server.js', {
workerData: {
hostNickname: hostNickname,
hostRoomCode: hostRoomCode,
hostClientId: hostClientId,
},
});
In the worker file, you'll need to require the same library worker_threads
. You'll have access to the workerData
object directly. For example, in the worker thread, you can access the host nickname with workerData.hostNickname
The worker thread can publish data to the main thread as follows:
parentPort.postMessage({
roomCode: roomCode,
totalPlayers: totalPlayers,
isGameOn: isGameOn,
isGameOver: isGameOver,
});
In this kit, the worker thread communicates with the main thread on three occasions:
- When a new player joins the game room.
- When an existing player leaves the game room.
- When the game is over and the worker thread is going to be killed.
This information is used by the main server thread to maintain a list of active worker threads, along with the number of players in each.
In order to use this kit, you will need an Ably API key. If you are not already signed up, you can sign up now for a free Ably account. Once you have an Ably account:
- Log into your app dashboard
- Under Your apps, click on Manage app for any app you wish to use for this tutorial, or create a new one with the Create New App button
- Click on the API Keys tab
- Copy the secret API Key value from your Root key.
The server-side scripts connect to Ably using Basic Authentication, i.e. by using the API Key directly as shown below:
const realtime = Ably.Realtime({
key: ABLY_API_KEY,
echoMessages: false,
});
Note: Setting the echoMessages
false prevents the server from receiving its own messages.
The main server thread uses Express to listen to HTTP requests. It has an /auth
endpoint that is used by the client-side scripts to authenticate with Ably using tokens. This is a recommended strategy as placing your secret API Key in a front-end script exposes it to potential misuse. The client-side scripts connect to Ably using Token Authentication as shown below:
const realtime = new Ably.Realtime({
authUrl: '/auth',
});
-
main-game-thread
- Used by the main server thread to listen for host player entries and leaves via presence, to be able to create new Node JS worker threads for new game rooms. -
<unique room code>:primary
- Main game state channel for a particular game room. It'll be used by players to enter or leave presence on the game room and by the worker thread to stream game state updates. -
<unique room code>:player-ch-<unique client id>
- Unique channel for every player, which is used to publish their state (or input) to the worker thread for that unique game room. The worker thread is subscribed to one such channel per player.
You can also add any other channels that you may need in your game.
Note: Due to the fact that the above channel names exist in a unique channel namespace identified by the unique room code (separated from channel names with a :), you can guarantee that one game room's data never creeps into the other.
The game's worker thread (server) stores all the players in an associative array as a key value pair with the following structure:
globalPlayersState[
"<unique-player-clientId-1>" : {
id: "<unique-player-clientId-1>",
nickname: player.data.nickname,
isAlive: true,
isConnected: true,
score: 0,
left: '<LEFT-POSITION>',
top: '<TOP-POSITION>',
color: '<PLAYER-COLOR>',
},
"<unique-player-clientId-2>" : {
id: "<unique-player-clientId-2>",
nickname: player.data.nickname,
isAlive: false,
isConnected: true,
score: 0,
left: '<LEFT-POSITION>',
top: '<TOP-POSITION>',
color: '<PLAYER-COLOR>',
},
]
The game's worker thread (server) also keeps a track of all the channels meant for each player's input, so it can attach and detach and from them as needed. This is also stored using an associative array with the following structure:
playerChannels[
'<unique-player-clientId-1>': {'<channel-instance-object>'},
'<unique-player-clientId-2>': {'<channel-instance-object>'}
]
The global game state of all the players is published by the game's worker thread (server) at a high frequency to all the players. The payload of this game state has the following structure:
gameRoomChannel.publish('game-state', {
globalPlayersState: globalPlayersState,
totalPlayers: totalPlayers,
isGameOn: isGameOn,
isGameOver: isGameOver,
});
The client-side script also stores the game state of each player locally and updates it as per the latest data from the server. This has exactly the same structure as in the server:
localGameState[
"<unique-player-clientId-1>" : {
id: "<unique-player-clientId-1>",
nickname: player.data.nickname,
isAlive: true,
isConnected: true,
score: 0,
left: '<LEFT-POSITION>',
top: '<TOP-POSITION>',
color: '<PLAYER-COLOR>',
},
"<unique-player-clientId-2>" : {
id: "<unique-player-clientId-2>",
nickname: player.data.nickname,
isAlive: false,
isConnected: true,
score: 0,
left: '<LEFT-POSITION>',
top: '<TOP-POSITION>',
color: '<PLAYER-COLOR>',
},
]
In the example game, HTML divs represent player bodies, so that is stored in another associative array, the attributes of which are updated as per the latest data in the localGameState
:
playerDivs[
"<unique-player-clientId-1>" : {'<HTML div element object>'},
"<unique-player-clientId-2>" : {'<HTML div element object>'}
]
-
All of Ably's messaging limits, broken down by package can be found in a support article.
-
We are currently performing load and performance tests on this framework and will update this guide with that info when it's available. If this is important to you, please leave a message to me directly on Twitter