We were working with a client on building a video game that had multiplayer integrated.
The game is more of a tabletop turn based game (think monopoly, life). We had to make sure turns and actions were synchronized across the board.
Furthermore we had the restraint that only one of the players is essentially the one enables to perform the actions. This means we had to account for scenarios where the client would be disconnected and we had to pass the power of decision to another user.
Choosing the stack
Having had some experience with game jams and other multiplayer games we decided to go with a typescript server leveraging Socket.IO
This would let use easily create a client / server using the same typescript types for exchanging data information.
Session Context
Introduction
First we wanted to setup our context. This means we could then interact with any game session or client data throughout any Socket.IO event.
export class SessionContext {
activeSessions: Record<string, GameSession>
clientSockets: string[]
constructor () {
this.activeSessions = {};
this.clientSockets = [];
}
}
Here we can see our context is consisted of activeSessions
and clientSockets
clientSockets
is just an array of strings of the ids of each clients connection. This is used later on on handling disconnection.
activeSessions
are were we have a list of game session. We use here the type Record
as this lets us access information on a faster scale with just the following:
const mySession = sessionContext.activeSessions[sessionId]
Our class then had a few extra functions that don't need explanation such as (addActiveSession, getSessionByClientSocket, etc.)
Handling Disconnections
It was important for us to handle disconnections as if a client in power gets disconnected we want to give that power to someone else.
This is where the clientSockets
comes in as it lets us do a difference with the list of clients handled by Socket.IO. This is needed as the disconnect event does not tell us exactly who disconnected.
We can then have something like this:
reconcileDisconnect(ioClients: string[], io: SocketIO.Server) {
// Compare the list of the IO Server clients vs the one we stored
const disconnectedClient = this.clientSockets.filter(x => !ioClients.includes(x));
// If we have more than one disconnected user then we have discrepancy
if (disconnectedClient.length > 0) {
// Loop through all the disconnected clients
for (let i = 0; i < disconnectedClient.length; i++) {
// Init local variables for easy access
const tmpClient = disconnectedClient[i]
const sessionOfDisconnected = this.getSessionByClientSocket(tmpClient)
// If disconnected client was in a session
if (sessionOfDisconnected) {
const tmpSession = this.activeSessions[sessionOfDisconnected]
console.log("GM : " + tmpSession.master);
// Remove him from the GameSession Client list
delete tmpSession.clients[tmpClient]
// Remove him from the global pool of connections
this.clientSockets.splice(this.clientSockets.indexOf(tmpClient), 1)
// Check if there are no more clients in the GameSession
if (Object.keys(tmpSession.clients).length <= 0) {
// Delete GameSession if there are no more users
delete this.activeSessions[sessionOfDisconnected]
} else {
// Check if he was has power
if (tmpSession.hasPower === tmpClient) {
// Log a message if we switched master
if (tmpSession.switchMaster()) {
log(`Power of the Session (${tmpSession.hash}) has been changed to Client (${tmpSession.hasPower})`);
let newPowerClient = tmpSession.getClientFromSocketId(tmpSession.hasPower);
tmpSession.broadcast({ type: "newGameMaster", query: newPowerClient.hash }, io, true);
}
}
}
}
}
}
}
And we now have a new person in power or our session deleted if the person was the last user in the session.
Conclusion
We have a few other functions for getting and setting information but the main point of this article was to show how to handle identity of disconnections and how to access data efficiently using context.
Pixium Digital - Shaping your project with technology and innovation
https://pixiumdigital.com
https://github.com/pixiumdigital