Building a Real-Time Multiplayer Game with Express and Socket.io

Building a Real-Time Multiplayer Game with Express and Socket.io

Table of contents

No heading

No headings in the article.

To explore the frontend code and get a closer look at the project's user interface and design, visit Amartya Singh blog. The frontend code is available there, offering a comprehensive understanding of the project's visual elements and interactions. Stay tuned, as the project will be going live soon*, bringing the frontend and backend together for an exciting and fully functional experience.*

Introduction:

In this blog, we will explore a code snippet that showcases the process of building a real-time multiplayer game using the Express framework and Socket.io library. We will delve into the server-side code in server.js and the client-side code in script.js. By examining these snippets, we will gain insights into the key functionalities and components involved in creating a dynamic multiplayer game.

About the game:

In this game, there is a room where 5 people can join the room and when they hit on the start button the code randomly selects a user and give them the role MAFIA to one and CREWMATE to rest by showing the images:-

Server-Side Implementation:

Setting Up the Server:

The server.js file begins by importing the required dependencies: Express, Socket.io, uuid, and body-parser. Express is a web application framework for Node.js, while Socket.io enables real-time communication between clients and servers. The uuid library generates unique identifiers, and body-parser aids in handling URL-encoded data.

npm i express socket.io ejs uuid body-parser

Initializing the Application:

The code initializes the Express application by creating an instance of the app and server objects using the http module. It also configures the view engine to use EJS (Embedded JavaScript) and serves static files from the "/public" directory. Additionally, body-parser is set up to handle URL-encoded data.

const express = require("express");
const app = express();
const server = require("http").Server(app);
const io = require("socket.io")(server);
const { v4: uuidV4 } = require("uuid");
const bodyParser = require('body-parser')

app.set("view engine", "ejs");
app.use("/public", express.static(__dirname + "/public"));
// app.use(express.json())
app.use(bodyParser.urlencoded({ extended: true }))

let ROOM_ID = "";

Defining Routes:

The code defines several routes to handle different requests. The "/start-game" route generates a unique room ID using uuidV4 and stores it in the ROOM_ID variable. It also retrieves the user's room limit from the request body and redirects the user to the generated room.

app.get("/start-game", async (req, res) => {
  ROOM_ID = uuidV4();
  console.log("Room_ID:-"+ROOM_ID)
  userSetroomLimit = req.body.userRoomLimit
  res.redirect(`/${ROOM_ID}`);
});

The "/" route renders the "main" view, while the "/join-room" route handles a POST request to join a specific room. It renders the "joined" view, passing the inputRoomId from the request body as a parameter.

app.get("/", async (req, res) => {
  res.render("main");//This is the main screen of game
});
app.post("/join-room",async (req, res) => {
  res.render("joined", { roomId: req.body.inputRoomId });
});

The "/:room" route is a dynamic route that renders the "joined" view, passing the room ID as a parameter.

app.get("/:room", async (req, res) => {
  res.render("joined", { roomId: req.params.room });
});

Managing Rooms and Users:

The code defines a room limit, which is set to 5(can be changed). It initializes an empty object called rooms to store information about different rooms and their users.

const roomLimit =5;
const rooms = {};

When a new user connects to the server through Socket.io, the "connection" event is triggered. The user is added to a specified room using the "join" method. If the room doesn't exist, a new Set is created to store the users of that room.

If the room has reached the user limit, the server emits the "roomFull" event to the client, indicating that the room cannot accommodate more users.

To handle the assignment of user roles, the code listens for the "user-role" event. When the number of users in the room reaches the room limit, a random client is selected from the array of clients. The server then sends a message to each client, changing the displayed image based on whether it matches the randomly selected client.


io.on("connection", (socket) => {


  console.log("A user connected");

  socket.on("joinRoom", (roomName ) => {
    socket.join(roomName);


    // Create a new room if it doesn't exist
    if (!rooms[roomName]) {
      rooms[roomName] = new Set();
    }

    const room = rooms[roomName];
    const usersCount = room.size;

    // Check if the room has reached the user limit
    if (usersCount >= roomLimit) {
      socket.emit("roomFull");
      return;
    }

    // Add the user to the room
    room.add(socket.id);







    // Emit the updated list of users in the room to all clients in the room
    const users = Array.from(rooms[roomName]);
    const data = {
      "usersArray": users,
      "userId": socket.id
    }
    io.to(roomName).emit("usersInRoom", data);

    socket.on("user-role", (data) => {

      // Convert the set of clients to an array
      const clientsArray = Array.from(users);

      if (users.length == roomLimit) {
        // Randomly choose a client from the array
        const randomClient = clientsArray[Math.floor(Math.random() * clientsArray.length)];

        for (let index = 0; index < clientsArray.length; index++) {
          const element = clientsArray[index];

          io.to(element == randomClient ? randomClient : element).emit('changeDivImage', {
            msg: element == randomClient ? "../public/images/candidate2.png" : "../public/images/finalCivilian.png",
            "userId": element == randomClient ? randomClient : element
          });
        }

      }
    })


  });

Handling Disconnections:

When a user disconnects from the server, the "disconnect" event is triggered. The code searches for the disconnected user in all the rooms and removes them from the corresponding room's set of users. The updated list of users in the room is then emitted to all clients in that room.

socket.on("disconnect", () => {
    console.log("A user disconnected");

    // Remove the user from the room
    for (const roomName in rooms) {
      console.log("bd Usernames" + rooms[roomName])
      if (rooms[roomName].has(socket.id)) {

        console.log(socket.id)

        rooms[roomName].delete(socket.id);

        // Emit the updated list of users in the room to all clients in the room
        const users = Array.from(rooms[roomName]);
        io.emit("userLeft", users);

        break;
      }
    }
  });

The server Listens at port 3000,

server.listen(3000, (req, res) => {
  console.log("Server Connected");
});

Client-Side Implementation:

The script.js file handles the client-side logic for the multiplayer game. It establishes a connection with the server using Socket.io and determines the index based on the window's width to select the appropriate room container.

const socket = io("/");
let index;
// this logic is performed because there is two different UIs for Desktop and mobile and this will choose wether rendered ui is of mobile or desktop
window.innerWidth >= 1024 ? index = 1 : index = 0;
const roomContainer = document.getElementsByClassName("members")[index];

The "renderDivs" function renders div elements for all users in the room, clearing the container before adding new divs. It receives an array of users and iterates through them, creating a div for each user and appending it to the room container.

// Function to render divs for all users in the room
function renderDivs(users) {
  let count = 0;
  roomContainer.innerHTML = ""; // Clear the container
  users.forEach((user) => {
    count++;
    const div = document.createElement("div");
    div.className = "gamersId";
    div.id = user;
    console.log("rendered");
    roomContainer.appendChild(div);

    console.log("UsersInRoom:-" + count);
  });
}

The client emits the "joinRoom" event, indicating that the user wants to join the specified room.

// Join the room
socket.emit("joinRoom", ROOM_ID);

Upon receiving the "usersInRoom" event from the server, the client renders the divs for all users in the room.

The client listens for the "user-role" event when the "start-game" button is clicked.

// Receive the list of users in the room when a new user joins
socket.on("usersInRoom", (users) => {
  console.log("userId:-" + users["userId"]);
  console.log("users:-" + users["usersArray"]);
  renderDivs(users["usersArray"]);
});
const startButton = document.getElementsByClassName('start-game')[index]
startButton.addEventListener('click', () => {
  socket.emit('user-role', ROOM_ID)

})

Once the server emits the "changeDivImage" event, the client updates the displayed image based on the received message.

socket.on('changeDivImage', (data) => {
  alt="role div"
  document.body.innerHTML=`<img src=${data["msg"]} height='100vh' width='100vw' alt=${alt}></img>`;
});

The client also listens for the "roomFull" event, indicating that the room has reached its user limit, and displays a corresponding message. When a user leaves the room, the client removes the corresponding div from the room container.

socket.on("roomFull", () => {
  document.textContent = "Room Is full";
});
socket.on("userLeft", (user) => {
  const divToRemove = roomContainer.querySelector(`#${user}`);

  if (divToRemove) {
    divToRemove.remove();
  }
});

Conclusion:

In this blog, we explored a code snippet that demonstrated the process of building a real-time multiplayer game using Express and Socket.io. The server-side code provided insights into handling user connections, managing rooms and users, and assigning user roles. The client-side code showcased the rendering of divs for users, joining a room, and updating the game's display based on server events. By understanding these concepts, developers can leverage Express and Socket.io to create engaging and interactive multiplayer games.