Building HTML5 Multiplayer Game with Node.js

In a previous tutorial, I showed how to build a Socket.IO chat and connect few people around the globe with the magic of WebSockets. In this article I want to make this project a little bit more fun and turn it into a multiplayer game: "rock, paper, scissors". So let's grab the previous code and introduce few modifications. Firstly, on a client side, we'll add 3 buttons: for rock, paper and scissors.
  1. <form id="say-form">
  2.   <div><input><button type="submit">Say</button></div>
  3. </form>
  4. <div>
  5.   <button id="rock">Rock</button>
  6.   <button id="paper">Paper</button>
  7.   <button id="scissors">Scissors</button>
  8. </div>
And then add an event listener that sends the 'turn' event to our socket.io server.
  1. ['rock', 'paper', 'scissors'].forEach(function(figure) {
  2.   var btn = document.getElementById(figure);
  3.   btn.addEventListener('click', function() {
  4.     io.emit('turn', figure);
  5.   });
  6. });
  7. <javascript>
  8.  
  9. And this code will enough to have our minimalistic Rock, Paper, Scissors client.
  10.  
  11. Now we need to update the server code too to support the game. Firstly, we need to match our players
  12. against each other. We will not implement lobby (even though, it is a good exercise to do on your own). Insted we'll match players in pairs as they come. If there's a player waiting, the next joined player will start a match against him, otherwise the newly joined user will be patiently waiting for his turn.
  13.  
  14. <javascript>
  15. let waiting = null;
  16.  
  17. io.on('connection', (sock) => {
  18.   sock.emit('msg', 'You are connected');
  19.   sock.on('msg', (msg) => io.emit('msg', msg));
  20.  
  21.   if (waiting === null) {
  22.     waiting = sock;
  23.   } else {
  24.     startGame(waiting, sock);
  25.     waiting = null;
  26.   }
  27.  
  28. });
  29.  
  30. function startGame(p1, p2) {
  31.   // Game Logic goes here
  32. }
As you see, building a simple matching system is a matter of just 10 lines of code. There is a space for improvement though. Think, how would you handle disconnect events for this scenario? Now, the "meat" of the game. The game logic that we will write in startGame() function. The game allows users to "blindly" select their moves, and once both moves are selected, it will find who is a winner. Let's leave the win condition for now and simply print who selected what after round ends.
  1. function startGame(p1, p2) {
  2.   const roomName = 'RPS' + roomId++;
  3.  
  4.   let p1Turn = null;
  5.   let p2Turn = null;
  6.  
  7.   [p1, p2].forEach((p) => p.join(roomName));
  8.   io.to(roomName).emit('msg', 'Game Started!');
  9.  
  10.   p1.on('turn', (e) => {
  11.     console.log(e);
  12.     p1Turn = e;
  13.     checkRoundEnd();
  14.   });
  15.  
  16.   p2.on('turn', (e) => {
  17.     console.log(e);
  18.     p2Turn = e;
  19.     checkRoundEnd();
  20.   });
  21.  
  22.   function checkRoundEnd() {
  23.     if (p1Turn !== null && p2Turn !== null) {
  24.       io.to(roomName).emit('msg', 'Round Ended! P1 - '
  25.         + p1Turn + ' P2 - ' + p2Turn);
  26.      
  27.       io.to(roomName).emit('msg', 'Next round!');
  28.  
  29.       p1Turn = p2Turn = null;
  30.     }
  31.   }
  32. }
This code does a lot of things: firsly it keeps track of both player turns (p1Turn and p2Turn). Then it checks if both turns are 'filled' and if so, notifies players about round end. In this code we introduced a feature of Socket.IO called 'rooms'. A room is simply a group of sockets that share a name. It is then easy to address all the sockets in the same 'room'. You will often use rooms to organize matches between players, or to separate one discussion subject from the other in your online chat. As you see, turning a chat into a game was very easy. With this basic features you can start thinking about more feature-rich online games. Who knows, maybe next World of Warcraft will be played in browsers. Here's the code of the finished application. index.html:
  1. <!doctype html>
  2. <html lang="en">
  3.   <meta charset="utf-8">
  4.   <title>Rock Paper Scissors</title>
  5. </head>
  6.  
  7. <ul id="chat"></ul>
  8.  
  9. <form id="say-form">
  10.   <div><input><button type="submit">Say</button></div>
  11. </form>
  12. <div>
  13.   <button id="rock">Rock</button>
  14.   <button id="paper">Paper</button>
  15.   <button id="scissors">Scissors</button>
  16. </div>
  17.  
  18.  
  19. <script src="/socket.io/socket.io.js"></script>
  20.   var sock = io();
  21.   sock.on('msg', onChatMessage);
  22.  
  23.   function onChatMessage(msg) {
  24.     console.log('Chat message!');
  25.     var ul = document.querySelector('#chat');
  26.     var item = document.createElement('li');
  27.     item.innerHTML = msg;
  28.     ul.appendChild(item);
  29.   }
  30.  
  31.   function sendChatMessage(e) {
  32.     e.preventDefault();
  33.  
  34.     var inp = document.querySelector('#say-form input');
  35.     var msg = inp.value;
  36.     inp.value = '';
  37.  
  38.     sock.emit('msg', msg);
  39.   }
  40.  
  41.   (function init() {
  42.     var form = document.querySelector('#say-form');
  43.     form.addEventListener('submit', sendChatMessage);
  44.  
  45.     ['rock', 'paper', 'scissors'].forEach(function(figure) {
  46.       var btn = document.getElementById(figure);
  47.       btn.addEventListener('click', function() {
  48.         sock.emit('turn', figure);
  49.       });
  50.     });
  51.   })();
  52. </body>
  53. </html>
main.js (server):
  1. const express = require('express');
  2. const http = require('http');
  3.  
  4. const app = express();
  5. const server = http.createServer(app);
  6. const io = require('socket.io')(server);
  7.  
  8. let waiting = null;
  9.  
  10. io.on('connection', (sock) => {
  11.   sock.emit('msg', 'You are connected');
  12.   sock.on('msg', (msg) => io.emit('msg', msg));
  13.  
  14.   if (waiting === null) {
  15.     sock.emit('msg', 'You Are Waiting');
  16.     waiting = sock;
  17.   } else {
  18.     startGame(waiting, sock);
  19.     waiting = null;
  20.   }
  21.  
  22. });
  23.  
  24. let roomId = 1;
  25.  
  26. function startGame(p1, p2) {
  27.   const roomName = 'RPS' + roomId++;
  28.  
  29.   let p1Turn = null;
  30.   let p2Turn = null;
  31.  
  32.   [p1, p2].forEach((p) => p.join(roomName));
  33.   io.to(roomName).emit('msg', 'Game Started!');
  34.  
  35.   p1.on('turn', (e) => {
  36.     console.log(e);
  37.     p1Turn = e;
  38.     checkRoundEnd();
  39.   });
  40.  
  41.   p2.on('turn', (e) => {
  42.     console.log(e);
  43.     p2Turn = e;
  44.     checkRoundEnd();
  45.   });
  46.  
  47.   function checkRoundEnd() {
  48.     if (p1Turn !== null && p2Turn !== null) {
  49.       io.to(roomName).emit('msg', 'Round Ended! P1 - '
  50.         + p1Turn + ' P2 - ' + p2Turn);
  51.  
  52.       io.to(roomName).emit('msg', 'Next round!');
  53.  
  54.       p1Turn = p2Turn = null;
  55.     }
  56.   }
  57. }
  58.  
  59. app.use(express.static(`${__dirname}/public`));
  60.  
  61. server.listen(3000, () => console.log('Ready on 0.0.0.0:3000'));

Add new comment