Create a multiplayer web game in the .io games genre

Released in 2015, Agar.io was the progenitor of a new genre of games .io , which has grown in popularity since then. I have experienced the rise in popularity of .io games myself: over the past three years, I have created and sold two games in this genre.

In case you've never heard of these games before, these are free multiplayer web games that are easy to play (no account required). Usually, they face many opposing players in one arena. Other famous games .io genre: Slither.io and Diep.io .

In this post, we will figure out how to create an .io game from scratch.... Just knowing Javascript will be enough for this: you need to understand things like ES6 syntax, keyword thisand Promises. Even if you don't know Javascript perfectly, you can still figure out most of the post.

.Io game example

We will link to the .io example game to help guide you through the tutorial . Try to play it!

The game is pretty simple: you control a ship in an arena where there are other players. Your ship automatically fires projectiles and you try to hit other players while dodging their projectiles.

1. Overview / project structure

I recommend downloading the source code of the sample game so you can follow along.

The example uses the following:

Express is the most popular Node.js web framework for managing the game's web server.
socket.io is a websocket library for exchanging data between browser and server.
Webpack is a module manager. You can read about why using Webpack here .

This is what the project directory structure looks like:

public/
    assets/
        ...
src/
    client/
        css/
            ...
        html/
            index.html
        index.js
        ...
    server/
        server.js
        ...
    shared/
        constants.js

public /

Everything in the folder public/will be statically transmitted by the server. This public/assets/contains the images used by our project.

src /

All source code is in the folder src/. Names client/and server/speak for themselves, and shared/contains the constants file is imported and the client and the server.

2. Builds / project parameters

As mentioned above, we use the Webpack module manager to build the project . Let's take a look at our Webpack config:

webpack.common.js:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: {
    game: './src/client/index.js',
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/client/html/index.html',
    }),
  ],
};

The most important lines here are:

src/client/index.jsIs the entry point of the Javascript (JS) client. Webpack will start from here and recursively search for other imported files.
The output JS of our Webpack build will be located in the directory dist/. I will refer to this file as our JS package .
We are using Babel , and in particular the @ babel / preset-env config to transpiling our JS code for older browsers.
We are using a plugin to extract all the CSS referenced by the JS files and combine them in one place. I will refer to it as our CSS package .

You may have noticed strange package filenames '[name].[contenthash].ext'. They contain substitutions for Webpack file names : [name]will be replaced with the name of the entry point (in our case, this game), and [contenthash]will be replaced with a hash of the file content. We do this to optimize the project for hashing - we can tell browsers to cache our JS packages indefinitely, because if a package changes, then its filename changes (changes contenthash). The finished result will be the name of the view file game.dbeee76e91a97d0c7207.js.

The file webpack.common.jsis the base configuration file that we import in the development and finished project configurations. For example, here's a development configuration:

webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
});

For efficiency, we use during development webpack.dev.js, and switch to webpack.prod.jsto optimize package sizes when deploying to production.

Local setting

I recommend installing the project on a local machine so you can follow the steps listed in this post. The setup is simple: first, Node and NPM must be installed on the system . Next, you need to run

$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install

and you're ready to go! To start the development server, just run

$ npm run develop

and go to localhost: 3000 in your web browser . The development server will automatically rebuild the JS and CSS packages as the code changes - just refresh the page to see all the changes!

3. Client entry points

Let's get down to the actual game code. First, we need a page index.html; when visiting a site, the browser will load it first. Our page will be pretty simple:

index.html




  An example .io game
 


 
 
 



This code example has been slightly simplified for clarity, and I'll do the same with many of the other examples in the post. The complete code can always be viewed on Github .

We have:

The HTML5 Canvas ( ) element that we will use to render the game.
to add our CSS package.
to add our Javascript package.
Main menu with username and PLAY button ( ).

After loading the home page in the browser begins to be executed Javascript-code, starting with the JS file entry point: src/client/index.js.

index.js

import { connect, play } from './networking';
import { startRendering, stopRendering } from './render';
import { startCapturingInput, stopCapturingInput } from './input';
import { downloadAssets } from './assets';
import { initState } from './state';
import { setLeaderboardHidden } from './leaderboard';

import './css/main.css';

const playMenu = document.getElementById('play-menu');
const playButton = document.getElementById('play-button');
const usernameInput = document.getElementById('username-input');

Promise.all([
  connect(),
  downloadAssets(),
]).then(() => {
  playMenu.classList.remove('hidden');
  usernameInput.focus();
  playButton.onclick = () => {
    // Play!
    play(usernameInput.value);
    playMenu.classList.add('hidden');
    initState();
    startCapturingInput();
    startRendering();
    setLeaderboardHidden(false);
  };
});

It may sound complicated, but there really isn't much going on here:

Importing several other JS files.
CSS import (so Webpack knows to include them in our CSS bundle).
Launch connect()to establish a connection to the server and launch downloadAssets()to download the images needed to render the game.
After completing step 3 , the main menu ( playMenu) is displayed .
Configuring the handler for pressing the "PLAY" button. When the button is clicked, the code initializes the game and tells the server that we are ready to play.

The main meat of our client-server logic is in those files that were imported by the file index.js. Now we will look at them all in order.

4. Customer data exchange

In this game, we use the well-known socket.io library to communicate with the server . Socket.io has built-in support for WebSockets , which are good for two-way communication: we can send messages to the server and the server can send messages to us over the same connection.

We will have one file src/client/networking.jsthat will handle all communications with the server:

networking.js

import io from 'socket.io-client';
import { processGameUpdate } from './state';

const Constants = require('../shared/constants');

const socket = io(`ws://${window.location.host}`);
const connectedPromise = new Promise(resolve => {
  socket.on('connect', () => {
    console.log('Connected to server!');
    resolve();
  });
});

export const connect = onGameOver => (
  connectedPromise.then(() => {
    // Register callbacks
    socket.on(Constants.MSG_TYPES.GAME_UPDATE, processGameUpdate);
    socket.on(Constants.MSG_TYPES.GAME_OVER, onGameOver);
  })
);

export const play = username => {
  socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
};

export const updateDirection = dir => {
  socket.emit(Constants.MSG_TYPES.INPUT, dir);
};

This code is also slightly abbreviated for clarity.

There are three main things going on in this file:

We are trying to connect to the server. connectedPromiseonly allowed when we have established a connection.
If the connection is successful, we register a callback-function ( processGameUpdate()and onGameOver()) for the messages that we receive from the server.
We export play()and updateDirection()so that other files can use them.

5. Rendering the client

It's time to display a picture on the screen!

... but before we can do that, we need to download all the images (resources) that are needed for this. Let's write a resource manager:

assets.js

const ASSET_NAMES = ['ship.svg', 'bullet.svg'];

const assets = {};
const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset));

function downloadAsset(assetName) {
  return new Promise(resolve => {
    const asset = new Image();
    asset.onload = () => {
      console.log(`Downloaded ${assetName}`);
      assets[assetName] = asset;
      resolve();
    };
    asset.src = `/assets/${assetName}`;
  });
}

export const downloadAssets = () => downloadPromise;
export const getAsset = assetName => assets[assetName];

Resource management is not that hard to implement! The main point is to store an object assetsthat will bind the key of the filename to the value of the object Image. When the resource is loaded, we save it to an object assetsfor quick retrieval in the future. When the download of each individual resource is allowed (that is, all resources are downloaded ), we allow downloadPromise.

After downloading the resources, you can start rendering. As stated earlier, we use HTML5 Canvas ( ) to draw on a web page . Our game is pretty simple, so we only need to draw the following:

Background
Player ship
Other players in the game
Shells

Here are the important snippets src/client/render.jsthat draw exactly the four points listed above:

render.js

import { getAsset } from './assets';
import { getCurrentState } from './state';

const Constants = require('../shared/constants');
const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;

// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');

// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

function render() {
  const { me, others, bullets } = getCurrentState();
  if (!me) {
    return;
  }

  // Draw background
  renderBackground(me.x, me.y);

  // Draw all bullets
  bullets.forEach(renderBullet.bind(null, me));

  // Draw all players
  renderPlayer(me, me);
  others.forEach(renderPlayer.bind(null, me));
}

// ... Helper functions here excluded

let renderInterval = null;
export function startRendering() {
  renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
  clearInterval(renderInterval);
}

This code is also abbreviated for clarity.

render()Is the main function of this file. startRendering()and stopRendering()control the activation of the 60 FPS render loop.

The specific implementations of the individual render helper functions (for example renderBullet()) aren't that important, but here's one simple example:

render.js

function renderBullet(me, bullet) {
  const { x, y } = bullet;
  context.drawImage(
    getAsset('bullet.svg'),
    canvas.width / 2 + x - me.x - BULLET_RADIUS,
    canvas.height / 2 + y - me.y - BULLET_RADIUS,
    BULLET_RADIUS * 2,
    BULLET_RADIUS * 2,
  );
}

Note that we are using the method we getAsset()saw earlier in asset.js!

If you're interested in exploring other rendering helper functions, read the rest of src / client / render.js .

6. Client input

It's time to make the game playable ! The control scheme will be very simple: to change the direction of movement, you can use the mouse (on a computer) or touch the screen (on a mobile device). To accomplish this, we will register Event Listeners for the Mouse and Touch events.
All this will be done by src/client/input.js:

input.js

import { updateDirection } from './networking';

function onMouseInput(e) {
  handleInput(e.clientX, e.clientY);
}

function onTouchInput(e) {
  const touch = e.touches[0];
  handleInput(touch.clientX, touch.clientY);
}

function handleInput(x, y) {
  const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y);
  updateDirection(dir);
}

export function startCapturingInput() {
  window.addEventListener('mousemove', onMouseInput);
  window.addEventListener('touchmove', onTouchInput);
}

export function stopCapturingInput() {
  window.removeEventListener('mousemove', onMouseInput);
  window.removeEventListener('touchmove', onTouchInput);
}

onMouseInput()and onTouchInput()are Event Listeners that call updateDirection()(from networking.js) when an input event occurs (for example, when the mouse moves). updateDirection()handles the exchange of messages with the server, which processes the input event and updates the game state accordingly.

7. Client status

This section is the most difficult in the first part of the post. Don't be discouraged if you don't understand it from the first reading! You can even skip it and come back to it later.

The last piece of the puzzle to complete the client / server code is state . Remember the code snippet from the Rendering Client section?

render.js

import { getCurrentState } from './state';

function render() {
  const { me, others, bullets } = getCurrentState();

  // Do the rendering
  // ...
}

getCurrentState()should be able to provide us with the current state of the game in the client at any given time based on updates received from the server. Here's an example of a game update that the server can send:

{
  "t": 1555960373725,
  "me": {
    "x": 2213.8050880413657,
    "y": 1469.370893425012,
    "direction": 1.3082443894581433,
    "id": "AhzgAtklgo2FJvwWAADO",
    "hp": 100
  },
  "others": [],
  "bullets": [
    {
      "id": "RUJfJ8Y18n",
      "x": 2354.029197099604,
      "y": 1431.6848318262666
    },
    {
      "id": "ctg5rht5s",
      "x": 2260.546457727445,
      "y": 1456.8088728920968
    }
  ],
  "leaderboard": [
    {
      "username": "Player",
      "score": 3
    }
  ]
}

Each game update contains five identical fields:

t : Server timestamp indicating when this update was generated.
me : information about the player receiving this update.
others : An array of information about other players participating in the same game.
bullets : An array of information about the bullets in the game.
leaderboard : current leaderboard data. We will not take them into account in this post.

7.1 Naive client state

A naive implementation getCurrentState()can only directly return the data of the most recently received game update.

naive-state.js

let lastGameUpdate = null;

// Handle a newly received game update.
export function processGameUpdate(update) {
  lastGameUpdate = update;
}

export function getCurrentState() {
  return lastGameUpdate;
}

Nice and understandable! But if only it were that simple. One of the reasons this implementation is problematic is that it limits the render frame rate to the server clock rate .

Frame Rate : The number of frames (i.e. calls render()) per second, or FPS. Games usually aim to achieve at least 60 FPS.

Tick ​​Rate : The rate at which the server sends game updates to clients. It is often lower than the frame rate . In our game, the server runs at a frequency of 30 cycles per second.

If we just render the last update of the game, then the FPS in fact can never exceed 30, because we never receive more than 30 updates from the server per second . Even if we call render()60 times per second, half of those calls will just redraw the same thing, essentially doing nothing. Another problem with a naive implementation is that it is subject to delays . With an ideal Internet speed, the client will receive a game update exactly every 33 ms (30 per second):

 

Unfortunately, nothing is perfect. The following picture would be more realistic:


A naive implementation is practically the worst case when it comes to latency. If a game update is received with a 50ms latency, then the client is stalled for an extra 50ms because it is still rendering the game state from the previous update. You can imagine how inconvenient this is for the player: arbitrary braking will make the game feel jerky and unstable.

7.2 Improved client state

We will make some improvements to the naive implementation. First, we are using a render latency of 100ms. This means that the "current" state of the client will always be 100ms behind the state of the game on the server. For example, if the server time is 150 , then the client will render the state the server was in at 50 :

 

This gives us a 100ms buffer to survive the unpredictable timing of receiving game updates:

 

The price to pay for this is a constant input lag of 100ms. This is a minor sacrifice for smooth gameplay - most players (especially casual ones) won't even notice this lag. It is much easier for people to adjust to a constant 100ms latency than to play with an unpredictable latency.

We can use another technique called client-side prediction that does a good job of reducing perceived latency, but it will not be covered in this post.

Another improvement we are using is linear interpolation . Due to the rendering lag, we usually run at least one update ahead of the current time in the client. When called getCurrentState(), we can perform linear interpolation between game updates just before and after the current time in the client:

 

This solves the problem with the frame rate: now we can render unique frames at any rate we need!

7.3 Implementing an enhanced client state

The example implementation in src/client/state.jsuses both render latency and linear interpolation, but this is not for long. Let's split the code into two parts. Here's the first one:

state.js, part 1

const RENDER_DELAY = 100;

const gameUpdates = [];
let gameStart = 0;
let firstServerTimestamp = 0;

export function initState() {
  gameStart = 0;
  firstServerTimestamp = 0;
}

export function processGameUpdate(update) {
  if (!firstServerTimestamp) {
    firstServerTimestamp = update.t;
    gameStart = Date.now();
  }
  gameUpdates.push(update);

  // Keep only one game update before the current server time
  const base = getBaseUpdate();
  if (base > 0) {
    gameUpdates.splice(0, base);
  }
}

function currentServerTime() {
  return firstServerTimestamp + (Date.now() - gameStart) - RENDER_DELAY;
}

// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
  const serverTime = currentServerTime();
  for (let i = gameUpdates.length - 1; i >= 0; i--) {
    if (gameUpdates[i].t       return i;
    }
  }
  return -1;
}

The first step is to figure out what it does currentServerTime(). As we saw earlier, a server timestamp is included with every game update. We want to use the render latency to render the image 100ms behind the server, but we'll never know the current server time because we can't know how long it took for any of the updates to get to us. The Internet is unpredictable and its speed can vary greatly!

To work around this problem, we can use a reasonable approximation: we'll pretend that the first update arrived instantly . If this were true, then we would know the server time at that particular moment! We store the server timestamp in firstServerTimestampand store our local(client) timestamp at the same moment in gameStart.

Oh, wait a minute. Shouldn't there be server time = client time? Why do we distinguish between "server timestamp" and "client timestamp"? This is a great question! It turns out they are not the same thing. Date.now()will return different timestamps in the client and server and it depends on factors local to these machines. Never assume that time stamps will be the same on all machines.

Now we know what it does currentServerTime(): it returns the server timestamp of the current render time . In other words, it is the current server time ( firstServerTimestamp

Now let's take a look at how we handle game updates. When received from the server, the update is called processGameUpdate()and we save the new update to the array gameUpdates. Then, to check memory usage, we delete all old updates before the base update , because we no longer need them.

What is a "core update"? This is the first update we find moving backward from the current server time . Remember this diagram?

 

The game update is directly to the left of "Client Render Time" and is a basic update.

What is the baseline update used for? Why can we drop updates to baseline? To figure this out, let's finally look at the implementation getCurrentState():

state.js, part 2

export function getCurrentState() {
  if (!firstServerTimestamp) {
    return {};
  }

  const base = getBaseUpdate();
  const serverTime = currentServerTime();

  // If base is the most recent update we have, use its state.
  // Else, interpolate between its state and the state of (base + 1).
  if (base < 0) {
    return gameUpdates[gameUpdates.length - 1];
  } else if (base === gameUpdates.length - 1) {
    return gameUpdates[base];
  } else {
    const baseUpdate = gameUpdates[base];
    const next = gameUpdates[base + 1];
    const r = (serverTime - baseUpdate.t) / (next.t - baseUpdate.t);
    return {
      me: interpolateObject(baseUpdate.me, next.me, r),
      others: interpolateObjectArray(baseUpdate.others, next.others, r),
      bullets: interpolateObjectArray(baseUpdate.bullets, next.bullets, r),
    };
  }
}

We handle three cases:

base < 0means that there are no updates up to the current render time (see implementation above getBaseUpdate()). This can happen immediately at the start of the game due to rendering delays. In this case, we will use the most recent update received.
baseIs the most recent update we have. This can happen due to network latency or poor Internet connection. In this case, we are also using the most recent update we have.
We have an update both before and after the current render time, so we can interpolate !

All that's left in state.jsis the implementation of linear interpolation, which is simple (but boring) math. If you want to learn it yourself, then open it state.json Github .

Part 2. Backend Server

In this part, we'll take a look at the Node.js backend that runs our example .io game .

1. Server entry point

We'll be using a popular Node.js web framework called Express to manage the web server . It will be configured by our server entry point file src/server/server.js:

server.js, part 1

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackConfig = require('../../webpack.dev.js');

// Setup an Express server
const app = express();
app.use(express.static('public'));

if (process.env.NODE_ENV === 'development') {
  // Setup Webpack for development
  const compiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(compiler));
} else {
  // Static serve the dist/ folder in production
  app.use(express.static('dist'));
}

// Listen on port
const port = process.env.PORT || 3000;
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

Remember we discussed Webpack in Part 1? This is where we will be using our Webpack config. We will apply them in two ways:

Use webpack-dev-middleware to automatically rebuild our development packages, or
Statically pass the folder dist/where Webpack will write our files after the production build.

Another important task server.jsis to set up a socket.io server that just connects to the Express server:

server.js, part 2

const socketio = require('socket.io');
const Constants = require('../shared/constants');

// Setup Express
// ...
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

// Setup socket.io
const io = socketio(server);

// Listen for socket.io connections
io.on('connection', socket => {
  console.log('Player connected!', socket.id);

  socket.on(Constants.MSG_TYPES.JOIN_GAME, joinGame);
  socket.on(Constants.MSG_TYPES.INPUT, handleInput);
  socket.on('disconnect', onDisconnect);
});

After successfully establishing a socket.io connection to the server, we set up event handlers for the new socket. Event handlers process messages received from clients by delegating to a singleton object game:

server.js, part 3

const Game = require('./game');

// ...

// Setup the Game
const game = new Game();

function joinGame(username) {
  game.addPlayer(this, username);
}

function handleInput(dir) {
  game.handleInput(this, dir);
}

function onDisconnect() {
  game.removePlayer(this);
}

We are creating a game in the .io genre, so we only need one copy Game("Game") - all players play in the same arena! In the next section, we'll see how this class works Game.

2. Game server

The class Gamecontains the most important server side logic. It has two main tasks: player management and game simulation .

Let's start with the first task, player management.

game.js, part 1

const Constants = require('../shared/constants');
const Player = require('./player');

class Game {
  constructor() {
    this.sockets = {};
    this.players = {};
    this.bullets = [];
    this.lastUpdateTime = Date.now();
    this.shouldSendUpdate = false;
    setInterval(this.update.bind(this), 1000 / 60);
  }

  addPlayer(socket, username) {
    this.sockets[socket.id] = socket;

    // Generate a position to start this player at.
    const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    this.players[socket.id] = new Player(socket.id, username, x, y);
  }

  removePlayer(socket) {
    delete this.sockets[socket.id];
    delete this.players[socket.id];
  }

  handleInput(socket, dir) {
    if (this.players[socket.id]) {
      this.players[socket.id].setDirection(dir);
    }
  }

  // ...
}

In this game, we will identify players by idtheir socket.io field (if you are confused, go back to server.js). Socket.io itself assigns each socket a unique one id, so we don't need to worry about that. I will refer to it as the Player ID .

With that in mind, let's examine the instance variables in the class Game:

socketsIs an object that binds the player ID to the socket associated with the player. It allows us to access sockets for a constant time by their player ID.
players Is an object that binds the player ID to the code> Player object

bulletsIs an array of objects Bulletin no particular order.
lastUpdateTimeIs the timestamp of when the game was last updated. We will see how it is used shortly.
shouldSendUpdateIs an auxiliary variable. We will also see its use shortly.
Methods addPlayer(), removePlayer()and there is handleInput()no need to explain, they are used in server.js. If you need to refresh your memory, go back a little higher.

The last line constructor()starts the game update cycle (with a frequency of 60 updates / s):

game.js, part 2

const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // Calculate time elapsed
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) / 1000;
    this.lastUpdateTime = now;

    // Update each bullet
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        // Destroy this bullet
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

    // Update each player
    Object.keys(this.sockets).forEach(playerID => {
      const player = this.players[playerID];
      const newBullet = player.update(dt);
      if (newBullet) {
        this.bullets.push(newBullet);
      }
    });

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // Check if any players are dead
    Object.keys(this.sockets).forEach(playerID => {
      const socket = this.sockets[playerID];
      const player = this.players[playerID];
      if (player.hp         socket.emit(Constants.MSG_TYPES.GAME_OVER);
        this.removePlayer(socket);
      }
    });

    // Send a game update to each player every other time
    if (this.shouldSendUpdate) {
      const leaderboard = this.getLeaderboard();
      Object.keys(this.sockets).forEach(playerID => {
        const socket = this.sockets[playerID];
        const player = this.players[playerID];
        socket.emit(
          Constants.MSG_TYPES.GAME_UPDATE,
          this.createUpdate(player, leaderboard),
        );
      });
      this.shouldSendUpdate = false;
    } else {
      this.shouldSendUpdate = true;
    }
  }

  // ...
}

The method update()contains probably the most important piece of server-side logic. Let's list everything it does in order:

Calculates how much time dthas passed since the last one update().
Refreshes each projectile and destroys them if necessary. We will see the implementation of this functionality later. For now, it is enough for us to know what it bullet.update() returns trueif the projectile is to be destroyed (it went out of the arena).
Updates each player and, if necessary, create a projectile. We will see this implementation later - it player.update() can return an objectBullet .
Checks for collisions between projectiles and players with a help applyCollisions()that returns an array of projectiles that hit players. For each projectile returned, we increase the score of the player who fired it (with player.onDealtDamage()) and then remove the projectile from the array bullets.
Notifies and destroys all killed players.
Sends a game update to all players every second time it is summoned update(). The helper variable mentioned above helps us keep track of it shouldSendUpdate. Since it update()is called 60 times / s, we send game updates 30 times / s. Thus, the server clock rate is 30 clock / s (we talked about the clock rate in the first part).

Why send game updates only one time ? To save the channel. 30 game updates per second is a lot!

Why not just call update()30 times per second then? To improve the simulation of the game. The more often it is called update(), the more accurate the simulation of the game will be. But do not get too carried away by the number of calls update(), because this is a computationally expensive task - 60 per second is quite enough.

The rest of the class Gameconsists of helper methods used in update():

game.js, part 3

class Game {
  // ...

  getLeaderboard() {
    return Object.values(this.players)
      .sort((p1, p2) => p2.score - p1.score)
      .slice(0, 5)
      .map(p => ({ username: p.username, score: Math.round(p.score) }));
  }

  createUpdate(player, leaderboard) {
    const nearbyPlayers = Object.values(this.players).filter(
      p => p !== player && p.distanceTo(player)     );
    const nearbyBullets = this.bullets.filter(
      b => b.distanceTo(player)     );

    return {
      t: Date.now(),
      me: player.serializeForUpdate(),
      others: nearbyPlayers.map(p => p.serializeForUpdate()),
      bullets: nearbyBullets.map(b => b.serializeForUpdate()),
      leaderboard,
    };
  }
}

getLeaderboard()pretty simple - it sorts the players by points, takes the top five, and returns a username and score for each.

createUpdate()used update()to create game updates that are passed on to players. Its main task is to call methods serializeForUpdate()implemented for classes Playerand Bullet. Note that it only transfers data to each player about nearby players and projectiles - there is no need to transfer information about game objects that are far from the player!

3. Game objects on the server

In our game, the projectiles and the players are actually very similar: they are abstract circular moving game objects. To take advantage of this similarity between players and projectiles, let's start by implementing a base class Object:

object.js

class Object {
  constructor(id, x, y, dir, speed) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.direction = dir;
    this.speed = speed;
  }

  update(dt) {
    this.x += dt * this.speed * Math.sin(this.direction);
    this.y -= dt * this.speed * Math.cos(this.direction);
  }

  distanceTo(object) {
    const dx = this.x - object.x;
    const dy = this.y - object.y;
    return Math.sqrt(dx * dx + dy * dy);
  }

  setDirection(dir) {
    this.direction = dir;
  }

  serializeForUpdate() {
    return {
      id: this.id,
      x: this.x,
      y: this.y,
    };
  }
}

Nothing complicated happens here. This class will be a good starting point for extensions. Let's see how the class Bulletuses Object:

bullet.js

const shortid = require('shortid');
const ObjectClass = require('./object');
const Constants = require('../shared/constants');

class Bullet extends ObjectClass {
  constructor(parentID, x, y, dir) {
    super(shortid(), x, y, dir, Constants.BULLET_SPEED);
    this.parentID = parentID;
  }

  // Returns true if the bullet should be destroyed
  update(dt) {
    super.update(dt);
    return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
  }
}

The implementation is Bulletvery short! We have added to Objectonly the following extensions:

Using the shortid package to randomly generate a idprojectile.
Adding a field parentIDso that you can track the player who created this projectile.
Adding a return value to update(), which is equal trueto if the projectile is outside the arena (remember we talked about this in the last section?).

Let's move on to Player:

player.js

const ObjectClass = require('./object');
const Bullet = require('./bullet');
const Constants = require('../shared/constants');

class Player extends ObjectClass {
  constructor(id, username, x, y) {
    super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED);
    this.username = username;
    this.hp = Constants.PLAYER_MAX_HP;
    this.fireCooldown = 0;
    this.score = 0;
  }

  // Returns a newly created bullet, or null.
  update(dt) {
    super.update(dt);

    // Update score
    this.score += dt * Constants.SCORE_PER_SECOND;

    // Make sure the player stays in bounds
    this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
    this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));

    // Fire a bullet, if needed
    this.fireCooldown -= dt;
    if (this.fireCooldown       this.fireCooldown += Constants.PLAYER_FIRE_COOLDOWN;
      return new Bullet(this.id, this.x, this.y, this.direction);
    }
    return null;
  }

  takeBulletDamage() {
    this.hp -= Constants.BULLET_DAMAGE;
  }

  onDealtDamage() {
    this.score += Constants.SCORE_BULLET_HIT;
  }

  serializeForUpdate() {
    return {
      ...(super.serializeForUpdate()),
      direction: this.direction,
      hp: this.hp,
    };
  }
}

Players are more complex than projectiles, so a few more fields must be stored in this class. Its method update()does a lot of the work, in particular, it returns the newly created projectile if there isn't one left fireCooldown(remember we talked about that in the previous section?). It also extends the method serializeForUpdate()because we need to include additional fields for the player in the game update.

Having a base class Objectis an important step in avoiding code repetition . For example, without a class, Objectevery game object would have to have the same implementation distanceTo(), and it would be a nightmare to copy and paste all of these implementations across multiple files. This becomes especially important for large projects when the number of extension Objectclasses grows.

4. Recognition of collisions

The only thing left for us is to recognize when the projectiles hit the players! Remember this piece of code from a method update()in a class Game:

game.js

const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // ...

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // ...
  }
}

We need to implement a method applyCollisions()that returns all the shells hit by the players. Fortunately, this is not that hard to do because

All colliding objects are circles, and this is the simplest figure to implement collision detection.
We already have a method distanceTo()that we implemented in the class in the previous section Object.

This simple collision detection is based on the fact that two circles collide if the distance between their centers is less than the sum of their radii . Here is a case when the distance between the centers of two circles is exactly equal to the sum of their radii:

There are a couple of other aspects that need to be carefully considered here:

The projectile must not hit the player who created it. This can be achieved by comparing bullet.parentIDwith player.id.
The projectile should only hit once in the extreme case of a simultaneous collision with several players. We will solve this problem with the help of an operator break: as soon as a player who collided with a projectile is found, we stop searching and move on to the next projectile.

cost to ship a car
Intercity Lines’ Standard for Auto Transport
shipping a car enclosed auto transport
How to Tell if an Auto Transport Company is a Broker_