15 min read

This post will teach you how to build a simple 3D multiplayer game using THREE.js and socket.io. This guide is intended to help you create a very simple yet perfectly functional multiplayer FPS ( First Person Shooter). Its name is “Dodgem” and will feature a simple arena, random destructible obstacles and your opponent. I’ve already done this, and you can check it out on github.

The playable version of this project can be found here.

Explanation

First of all, we need a brief description of how the project is built. Speaking technically, we have a random number of clients (our actual players), and a server, which randomly select an opponent for each new player. Every player who joins the game is put in a “pending” status, until he/she is randomly selected for a new match. Before the match starts, the player will be able to wander around the arena, and when the server communicates the beginning of the fight, both clients will receive informations on how to create the obstacles in the world.

Each player is represented like a blue sphere with two guns on the sides (this has been done for simplicity’s sake, we all know a humanoid figure would be more interesting). Every obstacle is destructible, if you shoot them, and you life is represented as a red bar on top the screen. Once one of the players dies, both will be prompted to join another match or simply continue to move, jump and shoot around.

Let’s code!

We can now start our project. We’ll use a easy-to-use “Game Engine” I wrote, since it provides a lot of useful things for our project. Its name is “Wage”, it’s fully open source (you can check the github repository here) and it’s available to install via npm. So, first things first, prompt this to your shell:

npm install -g wage

This will install wage as global package on your machine. You will be now able to create a new project wherever you want, using the “wage” command. Keep in mind that this Game Engine is still an in-dev version, so please use it very carefully, and submit any issue you want to the repository if needed.

Now, run:

wage create dodgem

This will create a folder named “dodgem” in your current directory, with everything you need to start the project. We’re now ready to start: I won’t explain every single line, just the basic informations required to start and the skeleton of the app (The entire source code is available on github, and you’re free to clone the repo on your machine). Only the server code is fully exaplained. Now, we can create our server.

Server

First of all, create a “server” folder beside the “app” folder. Add a “package.json” inside of it, with this content:

{
       "name":"dodgem",
       "version":"1.0.0",
       "description":"",
       "main":"",
       "author":"Your name goes here, thanks to Marco Stagni",
       "license":"MIT",
       "dependencies":{
           "socket.io":"*",
           "underscore":"*"
       }
   }

This file will tell npm that our server uses socket.io and underscore as modules (no matter what version they are), and running

npm install

inside the “server” folder will install the dependencies inside a “node_modules” folder. Speaking about the modules, socket.io is obviously used as main communication system between server and clients, while underscore is used because it provides a LOT of useful tools when you’re working with data sets. If you don’t know what socket.io and underscore are, just click on the links for a deep explanation. I won’t explain how socket.io works, because I assume that you’re already aware of its functionalities.

We’ll now create the server.js file (you must create it inside the server folder):

//server.js
   // server requirements

   var util = require("util"),
       io = require("socket.io"),
       _ = require("underscore"),
       Player = require("./Player.js").Player;

   // server variables
   var socket, players, pendings, matches;


   // init method
   functioninit() {
       players = [];
       pendings = [];
       matches = {};
       // socket.io setup
       socket = io.listen(8000);
       // setting socket io transport
       socket.set("transports", ["websocket"]);
       // setting socket io log level
       socket.set("log lever", 2);
       setEventHandlers();
   }

   var setEventHandlers = function() {
       // Socket.IO
       socket.sockets.on("connection", onSocketConnection);
   };

Util module is only used for logging purposes, and you don’t need to install it via npm, since it’s a system module. The Player variable refers to the Player model, which will be explained later. The other variables (socket, players, pendings an matches) will be used to store informations about pending players, matches and socket.io instance.

init and setEventHandlers

This method is used to instanciate socket.io and set a few options, such as the transport method (we’re using only websocket, but socket.io provides also a lot of transports more than websocket) and log level. The socket.io server is set to listen on port 8000, but you can choose whatever port you desire. This method also instanciate players, pendings and matches variables, and calls the “setEventHandlers” method, which will attach a “connection” event listener to the socket.io instance. The init method is called at the end of the server code. We can now add a few lines after the “setEventHandlers” method.

socket.io listeners

// New socket connection
   functiononSocketConnection(client) {
     util.log("New player has connected: "+client.id);

       // Listen for client disconnected
       client.on("disconnect", onClientDisconnect);

       // Listen for new player message
       client.on("new player", onNewPlayer);

       // Listen for move player message
       client.on("move player", onMovePlayer);

       // Listen for shooting player
       client.on("shooting player", onShootingPlayer);

       // Listen for died player
       client.on("Idied", onDeadPlayer);

       // Listen for another match message
       client.on("anothermatch", onAnotherMatchRequested);
   };

This function is the “connection” event of socket.io, and what it does is setting every event listener we need for our server. The events listed are: “disconnect” (when our client closes his page, or reloads it), “new player” (called when a client connects to the server), “move player” (called every time the player moves around the arena), “shooting player” (the player is shooting), “Idied” (the player who sent this message has died) and finally “anothermatch” (our player is requesting another match to the server).

The most important part of the listeners is the one which listen for new players.

// New player has joined
   functiononNewPlayer(data) {
       // Create a new player
       var newPlayer = newPlayer(data.x, data.y, data.z, this);
       newPlayer.id = this.id;

       console.log("creating new player");
       // Add new player to the players array
       players.push(newPlayer);

       // searching for a pending player
       var id = _.sample(pendings);
       if (!id) {
           // we didn't find a player
           console.log("added " + this.id + " to pending");
           pendings.push(newPlayer.id);
           // sending a pending event to player
           newPlayer.getSocket().emit("pending", {status: "pending", message: "waiting for a new player."});
       } else {
           // creating match
           pendings = _.without(pendings, id);
           matches[id] = newPlayer.id;
           matches[newPlayer.id] = id;
           console.log(matches);
           // generating world for this match
           var numObstacles = _.random(10, 30);
           var height = _.random(70, 100);
           var MAX_X = 490
           var MINUS_MAX_X = -490
           var MAX_Z = 990
           var MINUS_MAX_Z = -990
           var positions = [];
           for (var i=0; i<numObstacles; i++) {
               positions.push({
                   x: _.random(MINUS_MAX_X, MAX_X),
                   z: _.random(MINUS_MAX_Z, MAX_Z)
               });
           }
           console.log(numObstacles, height, positions);
           // sending both player info that they're connected
           newPlayer.getSocket().emit("matchstarted", {status: "matchstarted", message: "Player found!", numObstacles: numObstacles, height: height, positions: positions});
           playerById(id).getSocket().emit("matchstarted", {status: "matchstarted", message: "Player found!", numObstacles: numObstacles, height: height, positions: positions});
       }
   };

What it does, is creating a new player using the Player module imported at the beginning, storing the socket associated with the client. The new player is now stored inside the players list. The most important thing now is the research of a new match: the server will randomly pick a player from the pendings list, and if it’s able to find one, it will create a match. If the server doesn’t find a suitable player for the match, the new player is put inside the pendings list. Once the match is created, the server creates the informations needed by clients in order to create a common world. The informations are the sent to both clients. As you can see, I’m using a playerById method, which essentially is a function which search inside the players list for a player with the id equal to the one given.

// Find player by ID
   functionplayerById(id) {
       var i;
       for (i = 0; i < players.length; i++) {
           if (players[i].id == id)
               return players[i];
       };
      
       returnfalse;
   };

The other functions used as socket listeners are:

onMovePlayer

This function is called when the “move player” event is received. This will find the player associated with the socket id, find its opponent using the “matches” oject, then emit a socket event to the opponent, proving the right informations about the player movement. Using pseudo code, the onMovePlayer function is something like this:

onMovePlayer: function(data) {

       movingPlayer = findPlayerById(this.id)
       opponent = matches[movingPlayer.id]

       if !opponent
           console.log"error
       else
           opponent.socket.send(data.movement)
   }

onShootingPlayer

This function is called when the “shooting player” event is received. This will find the player associated with the socket id, find its opponent using the “matches” oject, then emit a socket event to the opponent, proving the right informations about the shooting player (such as starting point of the bullet and bullet direction). Using pseudo code, the onShootingPlayer function is something like this:

onShootingPlayer: function(data) {

       shootingPlayer = findPlayerById(this.id)
       opponent = matches[shootingPlayer.id]

       if !opponent
           console.log"error
       else
           opponent.socket.send(data.startingpoint, data.direction)
   }

onDeadPlayer, onAnotherMatchRequested

This function is called every time a player dies. When this happen, the dead user is removed from matches, players and pendings references, and he’s prompted to join a new match (his/her opponent is informed that he/she won the match). If this happen, the procedure is nearly the same as when he connects for the first time: another player is randomly picked from pendings list, and another match is created from scratch.

onClientDisconnect

Last but not least, onClientDisconnect is the function called when a user disconnects from server: this can happen when the user reloads the page, or when he/she closes the client. The corresponding opponent is informed of the situation, and put back to pending status.

We now must see how the “Player” model is implemented, since it’s used to create new players, and to retrieve informations about their connection, movements or behaviour.

Player

var Player = function(startx, starty, startz, socket) {
       var x = startx,
           y = starty,
           z = startz,
           socket = socket,
           rotx,
           roty,
           rotz,
           id;

       // getters

       var getX = function() {
           return x;
       }

       var getY = function() {
           return y;
       }

       var getZ = function() {
            return z;
       }

       var getSocket = function() {
           return socket;
       }

       var getRotX = function() {
           return rotx;
       }

       var getRotY = function() {
           return roty;
       }

       var getRotZ = function() {
           return rotz;
       }

       // setters

       var setX = function(value) {
           x = value;
       }

       var setY = function(value) {
           y = value;
       }

       var setZ = function(value) {
           z = value;
       }
      
       var setSocket = function(socket) {
           socket = socket;
       }

       var setRotX = function(value) {
           rotx = value;
       }

       var setRotY = function(value) {
          roty = value;
       }

       var setRotZ = function(value) {
           rotz = value;
       }

       return {
           getX: getX,
           getY: getY,
           getZ: getZ,
           getRotX: getRotX,
           getRotY: getRotY,
           getRotZ: getRotZ,
           getSocket: getSocket,
           setX: setX,
           setY: setY,
           setZ: setZ,
           setRotX: setRotX,
           setRotY: setRotY,
           setRotZ: setRotZ,
           setSocket: setSocket,
           id: id
       }
   };

   exports.Player = Player;

The Player model is pretty straightforward: you just have getter and setters for every parameter of the Player object, but not all of them are used inside this project.

So, this was the server code. This is not obviously the complete source code, but I explained all of the characteristics it has. For the complete code, you can check the github repository here.

Client

The client is pretty easy to understand. Once you create the project using Wage, you will find a file, named “main.js”: this is the starting point of your game, and can contain almost every single aspect of the game logic. The very first time you create something with Wage, you will find a file like this:

include("app/scripts/cube/mybox")

   Class("MyGame", {

       MyGame: function() {
           App.call(this);
       },

       onCreate: function() {
           var geometry = newTHREE.CubeGeometry(20, 20, 20);
           var material = newTHREE.MeshBasicMaterial({
               color: 0x00ff00,
               wireframe : true
           });

           var cube = newMesh(geometry, material, {script : "mybox", dir : "cube"});

           console.log("Inside onCreate method");

           document.addEventListener( 'mousemove', app.onDocumentMouseMove, false );
           document.addEventListener( 'touchstart', app.onDocumentTouchStart, false );
           document.addEventListener( 'touchmove', app.onDocumentTouchMove, false );
           document.addEventListener( 'mousewheel', app.onDocumentMouseWheel, false);

           //example for camera movement
           app.camera.addScript("cameraScript", "camera");
       }

   })._extends("App");

I know you need a brief description of what is going on inside the main.js file. Wage is using a library I built, wich provides a easy to use implementation of inheritance: this will allow you to create, extend and implement classes in an easy and readable way. The library is named “classy”, and you can find every information you need on github. A Wage application needs an implementation of the “App” class, as you can see from the example above. The constructor is the function MyGame, and it simply calls the super class “App”. The most important method you have to check is “onCreate”, because it’s the method that you have to use in order to star adding elements to your scene. First, you need a description of Wage is, and what is capable to do for you.

Wage is a “Game Engine”. It automatically creates a THREE.js Scene for you, and gives you a huge set of features that allow you to easily control your game. The most important things are a complete control of meshes (both animated and not), lights, sounds, shaders, particle effects and physics. Every single mesh and light is “scriptable”, since you’re able to modify their behaviour by “attaching” a custom script to the object itself (if you know how Unity3D works, then you know what I’m talking about. Maybe you can have a look a this, to better understand.). The scene created by Wage is added to index.html, which is the layout loaded by your app. Of course, index.html behave like a normal html page, so you can import everything you want, such as stylesheets or external javascript libraries. In this case, you have to import socket.io library inside index.html, like this:

<head>
       ...
       <script type="text/javascript" src="http:/YOURIP:8000/socket.io/socket.io.js"></script>
       ...
   </head>

I will now provide a description of what the client does, describing each feature in pseudo code.

Class("Dodgem", {

       Dodgem: function() {
           super()
       },

       onCreate: function() {

           this.socket = socketio.connect("IPADDRESS");
           // setting listeners
           this.socket.on("shoot", shootListener);
           this.socket.on("move", moveListener);
           this.socket.on(message3, listener3);

           // creating platform
           this.platform = newPlatform();
       },

       shootListener: function(data) {
           // someone is shooting
           createBulletAgainstMe(data)
       },

       moveListener: function(data) {
           // our enemy is moving around
           moveEnemyAround(data)
       }
   });

   Game.update = function() {

       handleBulletsCollisions()
       updateBullets()
       updateHealth()

       if health == 0
           Game.Die()
   }

As you can see, the onCreate method takes care of creating the socket.io instance, adding event listeners for every message incoming from the game server. The events we need to handle are: “shooting” (our opponent is shooting at us), “move” (our opponent is moving, we need to update its position), “pending” (we’ve been added to the pending list, we’re waiting for new player), “matchstarted” (our match is started), “goneplayer” (our opponent is no longer online), “win” (guess what this event means..). Every event has its own listener.

onMatchStarted

onMatchStarted: function(data) {
       alert(data.message)
       app.platform.createObstacles(data)
       app.opponent = newPlayer()
   }

This function creates the arena’s obstacles, and creates our opponent. A message is shown to user, telling that the match is started.

onShooting

onShooting: function(data) {
       createEnemyBullet()
   }

This function only creates enemy bullets with informations coming from the server. The created bullet is then handled by the Game.update method, to check collisions with obstacles, enemy and our entity.

onMove

onMove: function(data) {
       app.opponent.move(data.movement)
   }

This function handles the movements of our opponent. Every time he moves, we need to update his position on our screen. Player movements are updated at the highest rate possible.

onGonePlayer

 onGonePlayer: function(data) {
       alert(data.message)
   }

This function only shows a message, telling the user that his opponents has just left the arena.

onPending

onPending: function(data) {
       alert(data.message)
   }

This function is called when we join the game server for the first time and the server is not able to find a suitable player for our match. We’re able to move around the arena, randomly shooting.

Conclusions

Ok, that’s pretty much everything you need to know on how to create a simple multiplayer game. I hope this guide gave you the informations you need to start creating you very first multiplayer game: the purpose of this guide wasn’t to give you every line of code needed, but instead provide a useful guide line on how to create something funny and nicely playable.

I didn’t cover the graphic aspect of the game, because it’s completely available on the github repository, and it’s easily understandable. However, covering the graphic aspect of the game is not the main purpose of this tutorial. This is a guide that will let you understand what my simple multiplayer game does.

The playable version of this project can be found here.

About the author

Marco Stagni is a Italian frontend and mobile developer, with a Bachelor’s Degree in Computer Engineering. He’s completely in love with JavaScript, and he’s trying to push his knowledge of the language in every possible direction. After a few years as frontend and Android developer, working both with Italian Startups and Web Agencies, he’s now deepening his knowledge about Game Programming. His efforts are currently aimed to the completion of his biggest project: a Javascript Game Engine, built on top of THREE.js and Physijs (the project is still in alpha version, but already downloadable via http://npmjs.org/package/wage. You can follow him also on twitter @marcoponds or on github at http://github.com/marco-ponds. Marco is a big NBA fan.

LEAVE A REPLY

Please enter your comment!
Please enter your name here