Separate game logic on the server in Node.js​

How to make code easier to work with? DON’T WRITE IT IN ONE FILE!

And that is exactly what we are going to do here. It will be the last part of the Server-Side WebSocket series.

If you missed previous ones, you can go back to preparing the local environment or writing base for our server.

In this post, we will create a new script responsible for the game part of the server.

Game Script 👾

Let’s start by creating a new script called game.js.

In this script, we will make an interpreter for the server and client messages.

This way, it will be easier to work further on the code separate from the server connection.

There are many ways you can export the content of the JS file. I prefer to wrap it into an object.

module.exports = Game = {
   // Game functions
}

The next step will be to add some functions to our game script.

We will need some for player connection, disconnection, and everything in between.

module.exports = Game = {
  nameMap: {}, // dict with player keys - index by uuid to access player key
  connectedPlayers: {}, // dict with connected players - use player key to access connection

  // Initialize game logic and variables.
  initGame: function (wss) {
    
  },

  // Deinitialize game logic and variables.
  deinitGame: function () {

  },

  // Called when new client join the server.
  playerConnected: function (connection, uuidPlayer, request) {

  },

  // Called when client disconnects from the server.
  playerDisconnected: function (uuidPlayer) {

  },

  // Called when client send a message to the server.
  interpretMessage: function (connection, uuidPlayer, message) {
    
  }
}

Awesome! With that code ready, now it is time to add it to the server code!

// Print info about Node.JS version
console.log(new Date() + ' | Using Node ' + process.version + ' version to run server');


// Setting up server.
const http = require('http');
const server = http.createServer();
const port = 3000;

// Needed for parsing URLs.
const url = require('url');

// Setting WebSockets
const WebSocket = require('ws');
const wss = new WebSocket.Server({ noServer: true, clientTracking: true });

// Needed to generate player ids
const uuidv4 = require('uuid/v4');

// Game logic is in a separate file.
const game = require('./game');

// Websocket connection handler.
wss.on('connection', function connection(ws, request) {
  console.log(new Date() + ' | A new client is connected.');

  // Assign player Id to connected client.
  var uuidPlayer = uuidv4();

  // Let game register the player.
  game.playerConnected(ws, uuidPlayer, request);

  // Handle all messages from users.
  ws.on('message', function(msgStr) {
    console.log('Message: '+msgStr);

    // Pass message to be interpreted by the game.
    game.interpretMessage(ws, uuidPlayer, msgStr);
  });

  // What to do when client disconnect?
  ws.on('close', function(connection) {
    console.log(new Date() + ' | Closing connection for a client.');

    // Pass that that information to the game.
    game.playerDisconnected(uuidPlayer);
  });
});

// Attach broadcaster, to all clients. No broadcast by default.
wss.broadcast = function(data) {
  console.log(new Date() + ' | Broadcasting: ' + data);
  console.log(new Date() + ' | Reaching ' + Object.keys(game.connectedPlayers).length + ' clients.');

  // Sending data to all connected clients.
  for (var playerKey in game.connectedPlayers) {
    var client = game.connectedPlayers[playerKey];
    if(client.readyState === WebSocket.OPEN)
    {
      // Sending data to the client.
      client.send(data);
    }
  }
};

// Attach connection termination.
wss.terminateConnections = function () {
    console.log(new Date() + ' | Terminating connections.');

    // Send warning to the connected clients.
    wss.broadcast('Server is terminating connection.');

    // Terminating connected players.
    for (var playerKey in game.connectedPlayers) {
      var client = game.connectedPlayers[playerKey];
      if(client.readyState === WebSocket.OPEN)
      {
        // Terminating client connection.
        client.terminate();
      }
    }

    // Stop running game server.
    game.deinitGame();
}

// HTTP Server ==> WebSocket upgrade handling:
server.on('upgrade', function upgrade(request, socket, head) {

    console.log(new Date() + ' | Upgrading http connection to wss: url = '+request.url);

    // Parsing url from the request.
    var parsedUrl = url.parse(request.url, true, true);
    const pathname = parsedUrl.pathname

    console.log(new Date() + ' | Pathname = '+pathname);

    // If path is valid connect to the websocket.
    if (pathname === '/') {
      wss.handleUpgrade(request, socket, head, function done(ws) {
        wss.emit('connection', ws, request);
      });
    } else {
      socket.destroy();
    }
});

// On establishing port listener.
server.listen(port, function() {
    console.log(new Date() + ' | Server is listening on port ' + port);

    // Start running game server.
    game.initGame(wss);
});

// Detecting interrupt signal.
process.on('SIGINT', function() {
  console.log();
  console.log(new Date() + ' | Caught interrupt signal.');

  // Terminating connections.
  wss.terminateConnections();

  // Closing server.
  wss.close(function () {
    console.log(new Date() + ' | Server is closed.');

    // Exiting process.
    process.exit();
  });
});

Thanks to that split, our code will be much easier to work with!

Server.js will be responsible for handling connections and the game.js will be responsible for handling game logic on the server.

With this integration, let’s get back to the game script!

Handling connection 🔌

Now, the server is passing some general information about what a player is doing. Is he connected? Did he drop the connection? Or is he sending a new message to the server? The game has to react to all of these messages.

Let’s start by registering a new player and adding him to the pool of connected clients. We can reuse some of our previous code here.

module.exports = Game = {
  ...

  // Called when new client join the server.
  playerConnected: function (connection, uuidPlayer, request) {

      // Registering player with the session.
      let sessionMsg = {};
      sessionMsg.type = "register";
      sessionMsg.method = "register";

      // Gathering player connection key.
      let playerKey = request.headers['sec-websocket-key'];

      sessionMsg.sessionId = playerKey;
      sessionMsg.uuidPlayer = uuidPlayer;

      // Sending confirm message to the connected client.
      connection.send(JSON.stringify(sessionMsg));

      // Saving player variables in dicts.
      this.nameMap[uuidPlayer] = playerKey;
      this.connectedPlayers[playerKey] = connection;
  },

  ...
}

We also have to handle disconnection from the server.

module.exports = Game = {
  ...

  // Called when client disconnects from the server.
  playerDisconnected: function (uuidPlayer) {

    // Removing player connection info.
    let playerKey = this.nameMap[uuidPlayer];
    delete this.connectedPlayers[playerKey];
    delete this.nameMap[uuidPlayer];
  },

  ...
}

With that out of our way, it’s time to handle everything that will be in between!

Interpreting messages! 😱

For ease of use, let’s create a simple message format that we will utilize for communication between the game and the server.

{
  "method":"method name",
  "data":"message data as JSON"
}

And with that template, let’s make some simple test message.

{
  "method": "echo",
  "data": "{\"message\":\"Hello!\"}"
}

Great! Now we have to build a simple system to recognize this type of message!

You can notice that this message has echo type. For this type of message, we can make our server send the same message back to the client.

module.exports = Game = {
  ...

  // Called when the client sends a message to the server.
  interpretMessage: function (connection, uuidPlayer, message) {

    // Trying to parse the message.
    try {
      let clientMessage = JSON.parse(message);

      switch (clientMessage.method) {
        case 'echo':
          console.log(new Date() + ' | Server echo message: ' + clientMessage.data);

          // Sends back the same message.
          connection.send(message);
          break;
        default:
          console.log(new Date() + ' | Server can\'t recognize message method: ' + clientMessage.method);

          // Unknown type of message.
      }
      
    }
    // Catching invalid messages.
    catch (e) {
        console.log(new Date() + ' | Server received invalid message: ' + message + '\nThrowing error: ' + e);
    }

  }
}

This is just an example of a message interpretation. When you work on your server, you can extend this code with more types or even smarter recognition.

Let’s test it! 👨‍💻

From the previous post, you know that we can use websocat to connect and send messages to the server.

We will do exactly that now!

Communication between client and server

YES! It’s alive! 🙌

Do you have some questions about it? Let me know in the comment section below!

If you want to get notified of future content, sign up for the newsletter!

As always, the whole project is available at my public repository. 🔗

And I hope to see you next time! 🤓

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments