29 min read
on Jul 31, 2019
Comprehensive walkthrough on building a real-time multiplayer game.

Building a fast and scalable multiplayer game is no easy task. However, we’re excited that you’ve landed on this tutorial, and our hope is by the end of it, you’ll have a good grasp on building and deploying a fully functioning multiplayer game.

Our demo multiplayer game is Ninja Platformer, a collaborative puzzle game that encourages you to work with your friends to collect the keys in clever ways. It’s a perfect example as it combines a number of core multiplayer features, all bundled into one game.

Check out the demo below to see what you’ll be building. For the multiplayer experience, open it in two incognito browser windows, or invite a friend to play with you! Also, check out the GitHub repo to for the entire source code.

try-the-demo Demo
The playable demo of Ninja Platformer is available here. Open it up in two browsers side-by-side (each is a unique player!)
multiplayer-game-tutorial-github-repo Source Code
Complete source code is available in our GitHub repository.

Multiplayer Game Overview

Ninja Platformer is written in less than a 1000 lines of code! This is an incredible feat for the amount of functionality this game has. The game uses PubNub’s Data Stream Network to manage network traffic between players, and Functions to manage game state between devices.

It’s written in JavaScript and the levels are generated via JSON files, which provide the position of the platforms and game objects. Using Phaser's Arcade Physics Library, each character and object has its own physics body with its own set of physics properties. Overall, it’s an excellent showcase of both Phaser and PubNub’s capabilities.

With that, let’s get started! In the table of contents below, you’ll see our major topics. We’ll start with the basics and walk you through the bare-bone basics of setting up an online game from scratch using simple assets and scripts.

Table of Contents

Setup Your Machine

In order to prepare your machine for HTML5 game development, you are going to need a few tools on your system. Luckily, you may already have some if you have done development in the past. This tutorial is aimed at developers at all levels since it uses the minimum amount of tools necessary for development.

What you will need to make an HTML5 game:

  • Text Editor
  • Access to terminal / console on your computer
  • A local web server

Text Editor

You will use a text editor to code your game. There are tons of text editors out there. Choose whichever one you like best:


You are going to need access to the terminal / command prompt in order to launch your local web server.

  • On Windows, it’s called <b>Command Prompt</b> and you can access it by going to Start > Run > cmd
  • On MacOS, it’s called <b>Terminal</b> app and you can find it in the Applications folder or by typing hitting the Command + Space for Spotlight then type in Terminal.
  • On Linux, most distributions have the terminal icon in the dock. In Ubuntu the application is called Terminal.

Launch a Local Server

In order for the Phaser game engine to run in your browser, you are going to need a local web server. The reason loading the files without a webserver will not work is because browsers block files from loading from different domains. Create a folder on the desktop and call it Ninja-Multiplayer-Platformer.

If you have Node

Execute this line to install the http-server package:

npm -g install http-server

Then navigate to the directory where you are going to create your files.

cd Desktop
cd Ninja-Multiplayer-Platformer

A Better Alternative

You can download a tool called Browser Sync which will automatically reload the browser every time you modify a file.

npm -g install browser-sync

This will launch the server and reload the browser when you modify any Javascript file.

cd Desktop
cd Ninja-Multiplayer-Platformer
browser-sync start --server --files ="**/*.js"


Mac OS or Linux (or Python installed on your system)

Go to your terminal application and run this:

cd Desktop
cd Ninja-Multiplayer-Platformer
python -m SimpleHTTPServer

If you don’t have Node or Python, checkout this link for more details on how to setup your system.

Setting Up PubNub

Pub/Sub In a Nutshell

PubNub utilizes a Publish/Subscribe model for real-time data streaming and device signaling which lets you establish and maintain persistent socket connections to any device and push data to global audiences in less than ¼ of a second.

You can publish messages to any given channel, and subscribing clients receive only messages associated with that channel. The message payload can be any JSON data including numbers, strings, arrays, and objects.

Pub/Sub Use Cases and Scenarios

  • Chat: Sending and receiving messages, typing indicators, buddy lists, security
  • Locations and Connected Car: Dispatching taxi cabs, live tracking
  • Smart Sensors: Receiving data from a sensor for data visualizations and triggering device action
  • Healthcare: Monitoring heart rate from a patient’s wearable device
  • Multiplayer Gaming
  • Interactive Media: Audience-participating voting system

Setting up Your PubNub Account

To build an application that leverages the PubNub Data Stream, you need to sign up for your account to obtain API keys.

Once you have successfully signed up you will be taken to the admin page.

You can edit the app name by clicking the App name (“My First PubNub App”) and on the next screen by clicking the pencil icon next to the App name.

You should have your Publish Key and Subscribe Key under your app. (You do not need the Secret Key for now. You will only need this when you are using Access Manager APIs).

You can add additional apps by clicking the APPS tab on the top menu bar, then clicking New App.

You will get a new set of Publish and Subscribe Keys each time you create a new app.

Initialize Phaser

Download the project assets here.

Now in order to view your website, you have to launch your local web server. The instructions on how to do that are above. However when you cd to your project folder directory, you can run the python -m SimpleHTTPServer command in terminal. Then go to your web browser, and type in the IP and port that is listed in terminal. In some cases you can type in localhost:8000 or Then navigate to the index.html file in your web browser and click on the link. You should now see a blank screen.

To begin, open up your main.js file. In the following code below that you should copy and paste, we are going to load the other Javascript files and initialize the phaser window.

// Load External Javascript files
const loadHeroScript = document.createElement('script');
loadHeroScript.src = './js/heroScript.js';
const loadLoadingState = document.createElement('script');
loadLoadingState.src = './js/loadingState.js';
const loadPlaystate = document.createElement('script');
loadPlaystate.src = './js/playState.js';
// =============================================================================
// Load the various phaser states and start game
// =============================================================================
window.addEventListener('load', () => {
  const game = new window.Phaser.Game(960, 600, window.Phaser.AUTO, 'game');
  game.state.disableVisibilityChange = true; // This allows two windows to be open at the same time and allow both windows to run the update function
  game.state.add('play', window.PlayState);
  game.state.add('loading', window.LoadingState);
  //window.createMyPubNub(0); // Connect to the PubNub network and run level code 0
  //window.StartLoading = function () {
    game.state.start('loading'); // Run the loading function once you successfully connect to the PubNub network

Now if you refresh your HTML window, you should see that the phaser window has been created. It should be completely black since there are no assets loaded in the scene yet. The window.Phaser.AUTO parameter is to specify whether we want a 2D canvas or a WebGL canvas. In this case, we will use WebGL by default but will fall back to 2D if it’s not supported.


Now lets create the loading state to load the assets into the scene. Open up your loadingState.js file and copy and paste the following code:

'use strict';
// =============================================================================
// Loading state
// =============================================================================
window.LoadingState = { // Create an object with all of the loading information inside of it
  init() {
    // keep crispy-looking pixels
    this.game.renderer.renderSession.roundPixels = true; // Make the phaser sprites look smoother
  preload() {
    this.game.stage.disableVisibilityChange = true;
    // Load JSON levels
    this.game.load.json('level:0', 'data/level00.json');
    this.game.load.json('level:1', 'data/level01.json');
    this.game.load.json('level:2', 'data/level02.json');
    this.game.load.image('font:numbers', 'images/numbers.png');
    this.game.load.image('icon:coin', 'images/coin_icon.png');
    this.game.load.image('background', 'images/bg.png');
    this.game.load.image('invisible-wall', 'images/invisible_wall.png');
    this.game.load.image('ground', 'images/ground.png');
    this.game.load.image('grass:8x1', 'images/grass_8x1.png');
    this.game.load.image('grass:6x1', 'images/grass_6x1.png');
    this.game.load.image('grass:4x1', 'images/grass_4x1.png');
    this.game.load.image('grass:2x1', 'images/grass_2x1.png');
    this.game.load.image('grass:1x1', 'images/grass_1x1.png');
    this.game.load.image('key', 'images/key.png');
    this.game.load.spritesheet('decoration', 'images/decor.png', 42, 42);
    this.game.load.spritesheet('herodude', 'images/hero.png', 36, 42);
    this.game.load.spritesheet('hero', 'images/gameSmall.png', 36, 42);
    this.game.load.spritesheet('coin', 'images/coin_animated.png', 22, 22);
    this.game.load.spritesheet('door', 'images/door.png', 42, 66);
    this.game.load.spritesheet('icon:key', 'images/key_icon.png', 34, 30);
    this.game.load.audio('sfx:jump', 'audio/jump.wav');
    this.game.load.audio('sfx:coin', 'audio/coin.wav');
    this.game.load.audio('sfx:key', 'audio/key.wav');
    this.game.load.audio('sfx:stomp', 'audio/stomp.wav');
    this.game.load.audio('sfx:door', 'audio/door.wav');
    this.game.load.audio('bgm', ['audio/bgm.mp3', 'audio/bgm.ogg']);
  create() {
    this.game.state.start('play', true, false, { level: window.globalCurrentLevel }); // Start Game

Now lets look at what this code is doing. We created a object called window.LoadingState with all of the loading state information inside of it. In the init() function, we made the sprite objects in the game look smoother by using the Phaser API this.game.renderer.renderSession.roundPixels = true;

In the preload function, we load the JSON level information from the data folder. This data information will be used to generate the levels. Then every asset that we will use in the game needs to be preloaded into cache. We also load the various spritesheets and even preload the audio (we won’t be using audio in this tutorial, however you can easily add it by uncommenting out code).

Lastly we run the create() function that starts the game and loads whatever the window.globalCurrentLevel is. Lets go set that variable in main.js.

Display Assets on the Screen

Navigate to main.js and at the top of the document add the code:

window.syncOtherPlayerFrameDelay = 0; //30 frames allows for 500ms of network jitter, to prevent late frames
window.currentChannelName; // Global variable for the current channel that your player character is on
window.currentFireChannelName; // Global variable that checks the current stage you are on to send the correct information to the PubNub Block
window.globalCurrentLevel = 0; // Global variable for the current level (index starts at 0)
window.UniqueID = window.PubNub.generateUUID(); // Generate a unique id for the player. Generated by the PubNub Network
window.globalLevelState = null; // Sets the globalLevelState to null if you aren't connected to the network. Once connected, the level will generate to the info that was on the block.
window.globalWasHeroMoving = true;
// console.log('UniqueID', UniqueID); // Print out your clientsr Unique ID
window.text1 = 'Level 1 Occupancy: 0'; // Global text objects for occupancy count
window.text2 = 'Level 2 Occupancy: 0';
window.text3 = 'Level 3 Occupancy: 0';
let textResponse1;
let textResponse2;
let textResponse3;
window.updateOccupancyCounter = false; // Occupancy Counter variable to check if the timer has already been called in that scene
window.keyMessages = [];

This created global variables necessary for the next part of the tutorial. Now go to playState.js and instantiate the PlayState game state and set up some variables:

const keyStates = {};
let keyCollected = false;
window.frameCounter = 0;
window.PlayState = {

Now inside of the window.PlayState object, add the first part of the code:

init(data) {
  this.keys = this.game.input.keyboard.addKeys({
    left: window.Phaser.KeyCode.LEFT,
    right: window.Phaser.KeyCode.RIGHT,
    up: window.Phaser.KeyCode.UP
  this.coinPickupCount = 0;
  keyCollected = false;
  this.level = (data.level || 0);
create() {
  window.globalGameState = this;
  // fade in  (from black)
  // create sound entities
  this.sfx = {
    jump: this.game.add.audio('sfx:jump'),
    coin: this.game.add.audio('sfx:coin'),
    key: this.game.add.audio('sfx:key'),
    stomp: this.game.add.audio('sfx:stomp'),
    door: this.game.add.audio('sfx:door')
  // create level entities and decoration
  this.game.add.image(0, 0, 'background');
  window.textObject1 = this.game.add.text(700, 5, window.text1, { font: 'Bold 200px Arial', fill: '#000000', fontSize: '20px' });
  window.textObject2 = this.game.add.text(700, 35, window.text2, { font: 'Bold 200px Arial', fill: '#000000', fontSize: '20px' });
  window.textObject3 = this.game.add.text(700, 65, window.text3, { font: 'Bold 200px Arial', fill: '#000000', fontSize: '20px' });
  if (window.globalLevelState === null) {
    window.globalLevelState = {
      time: 0,
      coinCache: this.game.cache.getJSON(`level:${this.level}`)
  // this._loadLevel(window.globalLevelState.value);
  // create UI score boards

Now lets look at each part of the code. In the init(data) function, we set this.keys to be the command to detect which key on the keyboard has been pressed. Since this is the initialize function in the play state, certain variables must be set for later use.

In the create() {} function, we set some more variables and also make the screen fade in upon loading the web page by using the Phaser API call this.camera.flash('#000000');

Then we setup our sound effect variables in the this.sfx object. Currently in this tutorial, all of the sound effects are commented out, however you can easily go uncomment them later.

We then set the background image by calling the command this.game.add.image(0, 0, 'background');

Next we set the text objects that will be used to detect presence events in each room. We have an if statement that checks to see if the window.globalLevelState is equal to null. If it is equal, we set the coinCache equal to level:0 since we want the scene to load the first level if it doesn’t receive any information from the PubNub Block.

We then call the _loadLevel() function and also the _createHud() function.

If we run this code as is, we will get errors since we are calling functions that we haven’t created yet. Let’s add some more code inside of window.playState = {} and below the create(){} function:

_loadLevel(data) {
  // console.log(data)
  // create all the groups/layers that we need
  this.bgDecoration = this.game.add.group();
  this.platforms = this.game.add.group();
  this.coins = this.game.add.group();
  // spawn hero and enemies
  this._spawnCharacters({ hero: data.hero, spiders: data.spiders });
  // spawn level decoration
  data.decoration.forEach(function (deco) {
      this.game.add.image(deco.x, deco.y, 'decoration', deco.frame));
  }, this);
  // spawn platforms
  data.platforms.forEach(this._spawnPlatform, this);
  // spawn important objects
  data.coins.forEach(this._spawnCoin, this);
  this._spawnKey(data.key.x, data.key.y);
  this._spawnDoor(data.door.x, data.door.y);
  // enable gravity
  const GRAVITY = 1200;
  this.game.physics.arcade.gravity.y = GRAVITY;
_spawnPlatform(platform) {
  const sprite = this.platforms.create(platform.x, platform.y, platform.image);
  // physics for platform sprites
  sprite.body.allowGravity = false;
  sprite.body.immovable = true;
_spawnCoin(coin) {
  const sprite = this.coins.create(coin.x, coin.y, 'coin');
  sprite.anchor.set(0.5, 0.5);
  // physics (so we can detect overlap with the hero)
  sprite.body.allowGravity = false;
  // animations
  sprite.animations.add('rotate', [0, 1, 2, 1], 6, true); // 6fps, looped
_addOtherCharacter(uuid) {
  // console.log('Added another character to game');
  if (window.globalOtherHeros.has(uuid)) { return; }
  // console.log('_addOtherCharacter', uuid);
  this.hero2 = new window.Hero(this.game, 10, 10);
  this.hero2.lastKeyFrame = 0;
  window.globalOtherHeros.set(uuid, this.hero2);
_removeOtherCharacter(uuid) {
  if (!window.globalOtherHeros.has(uuid)) { return; }
_spawnCharacters(data) {
  this.hero = new window.Hero(this.game, 10, 10);
  const playerText = this.game.add.text(this.hero.position.x - 10, this.hero.position.y - 550, 'me', { fill: '#000000', fontSize: '15px' });
  // console.log(playerText.position.x, playerText.position.y);
  window.globalMyHero = this.hero;
  window.globalOtherHeros = this.otherHeros = new Map();
  // globalMyHero.alpha = 1; //compensating for lag
  // window.sendKeyMessage({}); // UNCOMMENT LATER
_spawnKey(x, y) {
  this.key = this.bgDecoration.create(x, y, 'key');
  this.key.anchor.set(0.5, 0.5);
  // enable physics to detect collisions, so the hero can pick the key up
  this.key.body.allowGravity = false;
  // add a small 'up & down' animation via a tween
  this.key.y -= 3;
    .to({ y: this.key.y + 6 }, 800, window.Phaser.Easing.Sinusoidal.InOut)
_spawnDoor(x, y) {
  this.door = this.bgDecoration.create(x, y, 'door');
  this.door.anchor.setTo(0.5, 1);
  this.door.body.allowGravity = false;
_createHud() {
  const NUMBERS_STR = '0123456789X ';
  this.coinFont = this.game.add.retroFont('font:numbers', 20, 26, NUMBERS_STR, 6);
  this.keyIcon = this.game.make.image(0, 19, 'icon:key');
  this.keyIcon.anchor.set(0, 0.5);
  const coinIcon = this.game.make.image(this.keyIcon.width + 7, 0, 'icon:coin');
  const coinScoreImg = this.game.make.image(coinIcon.x + coinIcon.width, coinIcon.height / 2, this.coinFont);
  coinScoreImg.anchor.set(0, 0.5);
  this.hud = this.game.add.group();
  this.hud.position.set(10, 10);

Let’s look at each section of the above code to see what it’s doing.

_loadLevel(data) creates asset groups that we are need later on in the code. Then it spawns all of the level decorations (the mushrooms, grass etc) from the JSON file information that we stored into cache earlier in the code. Next we spawn the animated coins, the key and the door into the level along with setting the gravity constant and turning on gravity.

_spawnPlatform(platform) spawns each platform object and turns them into a sprite. Then they are set to not be affected by gravity and also set to be immovable so other sprite objects can’t impact their position.

_spawnCoin(coin) creates each coin asset and places them on the screen, and adds their animations.

_addOtherCharacter(uuid) adds a hero that is not your own to the screen when someone else connects to the same PubNub channel.

_spawnCharacters(data) spawns the hero asset into the game. The hero information is defined in heroScript.js that we are going to create in a bit. We also set playerText to appear above your player so that way when we make the game multiplayer, you can tell who’s apart.

_spawnKey(x,y) creates the key that unlocks the door. A tween is applied to give the key the animation effect.

_spawnDoor(x,y) places the door in a set position.

_createHud() creates the overlay at the top left of the screen that checks to see if you have collected the key for that level and also how many coins you have collected.

Keep in mind you will still get errors in the console since we haven’t yet wrote the heroScript.js code. Lets do that now.

Hero Script

Open up your heroScript.js document and copy and paste the follow code then save:

'use strict';
// =============================================================================
// Create Player (Hero)
// =============================================================================
window.Hero = class Hero extends window.Phaser.Sprite {
  constructor(game) {
    window.Phaser.Sprite.call(this, game, 10, 523, 'hero');
    // anchor
    this.anchor.set(0.5, 0.5);
    // physics properties
    this.body.collideWorldBounds = true;
    // animations
    this.animations.add('stop', [0]);
    this.animations.add('run', [1, 2], 8, true); // 8fps looped
    this.animations.add('jump', [3]);
    this.animations.add('fall', [4]);
    // starting animation
  move(direction) {
    // guard
    if (this.isFrozen) { return; }
    const SPEED = 200;
    this.body.velocity.x = direction * SPEED;
    // update image flipping & animations
    if (this.body.velocity.x < 0) {
      this.scale.x = -1;
    } else if (this.body.velocity.x > 0) {
      this.scale.x = 1;
  jump() {
    // Hero jumping code
    const JUMP_SPEED = 600;
    const canJump = this.body.touching.down && this.alive && !this.isFrozen;
    // console.log({
    //   canJump: canJump,
    //   'this.body.touching.down': this.body.touching.down,
    //   'this.alive': this.alive,
    //   'this.isFrozen': this.isFrozen
    // });
    if (canJump || this.isBoosting) {
      this.body.velocity.y = -JUMP_SPEED;
      this.isBoosting = true;
    return canJump;
  update() {
    // update sprite animation, if it needs changing
    const animationName = this._getAnimationName();
    if (this.animations.name !== animationName) {
  freeze() { // When player goes through door do animation and remove player
    this.body.enable = false;
    this.isFrozen = true;
  // returns the animation name that should be playing depending on
  // current circumstances
  _getAnimationName() {
    let name = 'stop'; // default animation
    if (this.isFrozen) {
      name = 'stop';
    } else if (this.body.velocity.y < 0) {
      name = 'jump';
    } else if (this.body.velocity.y >= 0 && !this.body.touching.down) {
      name = 'fall';
    } else if (this.body.velocity.x !== 0 && this.body.touching.down) {
      name = 'run';
    return name;

heroScript.js sets up the player animations and handles the player movement. In the move(direction) function, we determine what direction the player should be facing depending upon the velocity of the player.

In jump() we set the properties to determine if the player can jump or not. In update() we update the sprite animation only if it needs to be changed. In freeze() we play the animate of the player going through the door. In _getAnimationName() we set the various animation names depending upon the hero body’s velocity.

Now if you copy and pasted this code correctly, save your document then refresh your browser window. You should see something like this:


Player Movement

Now we are going to add the code for the player movement to work. Below the create() {} function in playState.js, paste the following code:

update() {
  // update scoreboards
  this.coinFont.text = `x${this.coinPickupCount}`;
  this.keyIcon.frame = keyCollected ? 1 : 0;
shutdown() {
  // this.bgm.stop();
_canHeroEnterDoor(hero) {
  return keyCollected && hero.body.touching.down;

The update() function adds one to the frame count every frame. This is used to sync the player movements across all devices without the need to send PubNub publishes every frame. It also calls the _handleInput() and _handleCollisions() function every frame.

The shutdown() function is used to stop the background music from playing if you so wish to enable it (for this tutorial sound effects are commented out).

The _canHeroEnterDoor(hero) function checks to see if the key was collect and the hero is touching the platform object in order to enter through the door.

Now lets add the _handleCollisions() function to the game. Add this code right under the last code you just copy and pasted:

_handleCollisions() {
   for (let i = 0; i < 2; i++) { // prevent collisions for pushing thru
     this.game.physics.arcade.collide(this.hero, this.platforms);
     for (const uuid of window.globalOtherHeros.keys()) {
       const otherplayer = window.globalOtherHeros.get(uuid);
       this.game.physics.arcade.collide(otherplayer, this.platforms, null, null, this);
       this.game.physics.arcade.overlap(otherplayer, this.coins, this._onHeroVsCoin, null, this);
       this.game.physics.arcade.overlap(otherplayer, this.key, this._onHeroVsKey, null, this);
       this.game.physics.arcade.overlap(otherplayer, this.door, this._onOtherHeroVsDoor, this._canHeroEnterDoor, this);
     // hero vs coins (pick up)
     this.game.physics.arcade.overlap(this.hero, this.coins, this._onHeroVsCoin, null, this);
     // hero vs key (pick up)
     this.game.physics.arcade.overlap(this.hero, this.key, this._onHeroVsKey, null, this);
     // hero vs door (end level)
     this.game.physics.arcade.overlap(this.hero, this.door, this._onHeroVsDoor, this._canHeroEnterDoor, this);
     // ignore if there is no key or the player is on air

The _handleCollisions() function handles all of the scenes object collision events. For instance if the hero collides with a key, it will run the this._onHeroVsKey function.

Now lets add the _handleInput() function. Code is commented out here until we add the PubNub portion. Then we will go back and uncomment the code to send key event messages to the PubNub network. Add this code under the _handleCollisions() function:

_handleInput() {
  // handleKeyMessages(); // UNCOMMENT LATER
  //  logCurrentState(this.game);
  if (this.hero) { // Added this so we can control spawning of heros
    if (this.keys.left.isDown) {
      if (!keyStates.leftIsDown) {
        // console.log('left pushed');
        // window.sendKeyMessage({ left: 'down' });  // UNCOMMENT LATER
      keyStates.leftIsDown = true;
    } else {
      if (keyStates.leftIsDown) {
        // console.log('left un-pushed');
        // window.sendKeyMessage({ left: 'up' }); // UNCOMMENT LATER
      keyStates.leftIsDown = false;
    if (this.keys.right.isDown) {
      if (!keyStates.rightIsDown) {
        // console.log('right pushed');
        // window.sendKeyMessage({ right: 'down' }); // UNCOMMENT LATER
      keyStates.rightIsDown = true;
    } else {
      if (keyStates.rightIsDown) {
        // console.log('right un-pushed');
        // window.sendKeyMessage({ right: 'up' }); // UNCOMMENT LATER
      keyStates.rightIsDown = false;
    if (this.hero.body.touching.down) {
      if (this.keys.up.isDown) {
        if (!keyStates.upIsDown) {
          // window.sendKeyMessage({ up: 'down' }); // UNCOMMENT LATER
        keyStates.upIsDown = true;
      } else {
        if (keyStates.upIsDown) {
          // console.log('up un-pushed');
          // window.sendKeyMessage({ up: 'up' }); // UNCOMMENT LATER
        keyStates.upIsDown = false;
    if (this.keys.left.isDown) { // move hero left
    } else if (this.keys.right.isDown) { // move hero right
    } else { // stop
    // handle jump
    const JUMP_HOLD = 10;// 200; // ms
    if (this.keys.up.downDuration(JUMP_HOLD)) {
      // let didJump = this.hero.jump();
      // if (didJump) { this.sfx.jump.play();}
    for (const uuid of window.globalOtherHeros.keys()) {
      const otherplayer = window.globalOtherHeros.get(uuid);
      if (Date.now() + JUMP_HOLD <= otherplayer.jumpStart) {
        // otherplayer.jump();
      if (otherplayer.goingLeft) { // move hero left
      } else if (otherplayer.goingRight) { // move hero right
      } else { // stop
  if (window.globalWasHeroMoving && this.hero.body.velocity.x === 0 && this.hero.body.velocity.y === 0 && this.hero.body.touching.down) {
    // window.sendKeyMessage({ stopped: 'not moving' }); // UNCOMMENT LATER
    window.globalWasHeroMoving = false;
  } else if (window.globalWasHeroMoving || this.hero.body.velocity.x !== 0 || this.hero.body.velocity.y !== 0 || !this.hero.body.touching.down) {
    window.globalWasHeroMoving = true;

The _handleInput() function runs every frame and checks to see if your hero object exists on the screen. If it does, it checks every frame to see if any of the keys have been pressed down. If it has been pressed down, send a message that the button pressed is up. This is a basic boolean if statement.

The next part of the code actually moves the character by calling this.hero.move(-1)this.hero.move(1) or this.hero.move(0). That calls the move function in heroScript.js.

The next portion of the code is going to be used later on in the tutorial, but is there to check to see which UUID matches with what player object on the screen. If there is a match, it moves the players based off what messages it’s receiving from the PubNub callback which we are going to code later on in this tutorial.

In the next portion, of code we check to see if the player has stopped moving entirely. If they have we send out a message to everyone declaring that they are standing still.

When you save all of your documents and refresh your web browser, you should see this screen and should be able to move your character around using the left, right and up arrows. Try it out:

multiplayer gaming player movement tutorial

Add Object Collisions

Now lets add the functions that allow the players on the screen to interact with the objects on screen. Each function is a function that is called when that specific collision event occurs. Most of the logic here is simply animations. The _goToNextLevel() function loads the next level and if the level is equal to the last level, it will restart you at the first level. Copy and past this code below the _handleInput() function:

_onHeroVsKey(hero, key) {
  // this.sfx.key.play();
  this.door.frame = 1;
  keyCollected = true;
  window.sendKeyMessage({ keyCollected });
_onHeroVsCoin(hero, coin) {
  // this.sfx.coin.play();
  logCurrentStateCoin(this.game, coin);
_onHeroVsDoor(hero, door) {
    // 'open' the door by changing its graphic and playing a sfx
  door.frame = 1;
    // this.sfx.door.play();
    // play 'enter door' animation and change to the next level when it ends
    .to({ x: this.door.x, alpha: 0 }, 0, null, true)
    .onComplete.addOnce(this._goToNextLevel, this);
_onOtherHeroVsDoor(hero, door) {
  // 'open' the door by changing its graphic and playing a sfx
  door.frame = 1;
  // this.sfx.door.play();
  // play 'enter door' animation and change to the next level when it ends
    .to({ x: this.door.x, alpha: 0 }, 500, null, true);
_goToNextLevel() {
  this.camera.onFadeComplete.addOnce(function () {
    window.updateOccupancyCounter = false;
    if (this.level === 2) {
    } else {
      window.createMyPubNub(this.level + 1);
  }, this);

If you refresh the window, you will get an error since we haven’t defined logCurrentStateCoin yet. Go to the top of playState.js and add the following code right below the window.frameCounter variable:

function logCurrentStateCoin(game, coin) {
  // Log Current Game State of Collected Coins
  for (const value of window.globalLevelState.coinCache.coins) {
    if (coin.x === value.x) {
      window.globalLevelState.coinCache.coins.splice(window.globalLevelState.coinCache.coins.indexOf(value), 1);
      // console.log(value)
  // console.log(window.globalLevelState.coinCache.coins)

When you copy and paste that code, you will still get an error since window.fireCoins() is a function that has not yet been defined. Go ahead and comment out that line of code by using the // tags in front of the statement.

Now refresh your window and you should be able to move your player around and collect the coins without trouble. However, if you try to collect the key, you will get an error since we have not yet defined the sendKeyMessage() function.

add object collision item collection game

Handle Messages

Now lets add the handleKeyMessages() function to the game so we can start implementing the multiplayer components. This function handles all of the messages that get received by the client. Essentially what it’s doing is syncing all the clients up to each other so the movements are accurately displayed on the screen. Copy and paste this code in playState.js right below the logCurrentStateCoin(game, coin) function:

function handleKeyMessages() {
  const earlyMessages = [];
  const lateMessages = [];
  window.keyMessages.forEach((messageEvent) => {
    if (window.globalOtherHeros) { // If player exists
      if (messageEvent.channel === window.currentChannelName) { // If the messages channel is equal to your current channel
        if (!window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message isn't equal to your uuid
          window.globalGameState._addOtherCharacter(messageEvent.message.uuid); // Add another player to the game that is not yourself
          const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid);
          otherplayer.position.set(messageEvent.message.position.x, messageEvent.message.position.y); // set the position of each player according to x y
          otherplayer.initialRemoteFrame = messageEvent.message.frameCounter;
          otherplayer.initialLocalFrame = window.frameCounter;
          window.sendKeyMessage({}); // Send publish to all clients about user information
        if (messageEvent.message.position && window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message contains the position of the player and the player has a uuid that matches with one in the level
          const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid);
          const frameDelta = messageEvent.message.frameCounter - otherplayer.lastKeyFrame;
          const initDelta = otherplayer.initialRemoteFrame - otherplayer.initialLocalFrame;
          const frameDelay = (messageEvent.message.frameCounter - window.frameCounter) - initDelta + window.syncOtherPlayerFrameDelay;
            lastKeyFrame: otherplayer.lastKeyFrame,
            frameCounter: messageEvent.message.frameCounter,
            rf_lf: otherplayer.initialRemoteFrame - otherplayer.initialLocalFrame,
          if (frameDelay > 0) {
            if (!messageEvent.hasOwnProperty('frameDelay')) {
              messageEvent.frameDelay = frameDelay;
              otherplayer.totalRecvedFrameDelay += frameDelay;
            //console.log('avgFrameDelay', otherplayer.totalRecvedFrameDelay / otherplayer.totalRecvedFrames);
            //console.log('initDelta', initDelta, 'early', frameDelay);
            //console.log('early', frameDelay);
          } else if (messageEvent.message.keyMessage.stopped === 'not moving') {
            console.log('initDelta', initDelta, 'stopping player');
            otherplayer.body.position.set(messageEvent.message.position.x, messageEvent.message.position.y);
            otherplayer.body.velocity.set(0, 0);
            otherplayer.goingLeft = false;
            otherplayer.goingRight = false;
            if (otherplayer.totalRecvedFrames > 0) {
              const avgFrameDelay = otherplayer.totalRecvedFrameDelay / otherplayer.totalRecvedFrames;
              const floorFrameDelay = Math.floor(avgFrameDelay);
            //console.log('otherplayer.initialRemoteFrame before', otherplayer.initialRemoteFrame);
              otherplayer.initialRemoteFrame += floorFrameDelay - 7;
            //console.log('otherplayer.initialRemoteFrame after', otherplayer.initialRemoteFrame);
              console.log('avg frame delay', avgFrameDelay, 'adjusting delta', floorFrameDelay);
            otherplayer.totalRecvedFrameDelay = 0;
            otherplayer.totalRecvedFrames = 0;
          } else if (frameDelay < 0) {
            otherplayer.totalRecvedFrameDelay += frameDelay;
            console.log('initDelta', initDelta, 'late', frameDelay);
          } else {
          //console.log('initDelta', initDelta, 'ontime', frameDelay);
          otherplayer.lastKeyFrame = messageEvent.message.frameCounter;
          if (messageEvent.message.keyMessage.up === 'down') { // If message equals arrow up, make the player jump with the correct UUID
            otherplayer.jumpStart = Date.now();
          } else if (messageEvent.message.keyMessage.up === 'up') {
            otherplayer.jumpStart = 0;
          if (messageEvent.message.keyMessage.left === 'down') { // If message equals arrow left, make the player move left with the correct UUID
            otherplayer.goingLeft = true;
          } else if (messageEvent.message.keyMessage.left === 'up') {
            otherplayer.goingLeft = false;
          if (messageEvent.message.keyMessage.right === 'down') { // If message equals arrow down, make the player move right with the correct UUID
            otherplayer.goingRight = true;
          } else if (messageEvent.message.keyMessage.right === 'up') {
            otherplayer.goingRight = false;
  if (lateMessages.length > 0) {
  //console.log({ lateMessages, earlyMessages });
  window.keyMessages.length = 0;
  earlyMessages.forEach((em) => {

This function handles all messages coming from other clients that are connected to the game. The function won’t do anything right now until we add the multiplayer components to the game.

However let’s take a quick look at what this function is doing. We start out by taking the message data and checking to see if the message is equal to the current channel you are subscribed too and if you aren’t the one sending the message. If you receive a message from someone who is not in the game, create a new player and set their position. We then send a message to update all clients about their new player position.

If you receive a message from someone who is not in the game, create a new player and set their position. We then send a message to update all clients about their new player position.

The handleKeyMessages() function also checks frame count to make sure all clients are in sync. Also we check the messageEvent.message.keyMessage for the input events of all other users and will update their players state on all clients.

Adding PubNub

Now we are going to add the PubNub portion of the code into the game to allow other players to join the game. In your main.js file, add this section of code after the variables you set up and above the javascript files you loaded into the scene:

window.createMyPubNub = function (currentLevel) {
  // console.log('createMyPubNub', currentLevel);
  window.globalCurrentLevel = currentLevel; // Get the current level and set it to the global level
  window.currentFireChannelName = 'realtimephaserFire2';
  window.currentChannelName = `realtimephaser${currentLevel}`; // Create the channel name + the current level. This way each level is on its own channel.
  let checkIfJoined = false; // If player has joined the channel
  // Setup your PubNub Keys
  window.pubnub = new window.PubNub({
    subscribeKey: 'ADD-YOUR-PUBNUB-SUBKEY-HERE',
    uuid: window.UniqueID,
  // Subscribe to the two PubNub Channels
    channels: [window.currentChannelName, window.currentFireChannelName],
    withPresence: true,
  // If person leaves or refreshes the window, run the unsubscribe function
  window.addEventListener('beforeunload', () => {
    navigator.sendBeacon(`https://pubsub.pubnub.com/v2/presence/sub_key/mySubKey/channel/ch1/leave?uuid=${window.UniqueID}`); // pub
  // Unsubscribe people from PubNub network
  window.globalUnsubscribe = function () {
    try {
      // console.log('unsubscribing', window.currentChannelName);
        channels: [window.currentChannelName, window.currentFireChannelName],
        withPresence: true
    } catch (err) {
      // console.log("Failed to UnSub");

This function sets up some variables and channel names that PubNub is going to use for network communication. We set window.currentChannelName to equal whatever current level the user is on. We then setup the PubNub keys and subscribe to the channels specified. Then we add a listener that when the browser is unloaded, it sends a beacon that a user has left the channel so the presence event updates for all other clients. We also have a globalUnsubscribe function that removes the listener for the client and subscribes them from the channel.

Then we add a listener that when the browser is unloaded, it sends a beacon that a user has left the channel so the presence event updates for all other clients. We also have a globalUnsubscribe function that removes the listener for the client and subscribes them from the channel.

Setup Your PubNub Dashboard

Take a look at your publish and subscribe key. You will need to add your own Publish and Subscribe keys in order for the game to work. Now if you haven’t already, create a PubNub account.

Once you’re in the Admin Dashboard, name your application whatever you wish, and click the Create New App button. Once you create the application, click on the application to few the key information. You should see that you have two keys, a Publish Key, and a Subscribe Key. Click on the demo keyset, and it should load up a page that shows your keys in addition to Application Add-Ons. In the Application Add-Ons section, turn ON Presence and check Generate Leave on TCP FIN or RST and Global Here Now. Also turn ON PubNub Functions. Make sure to have Access Manager turned off or else the sample code won’t work since you need to include a secret key.

Click on the demo keyset, and it should load up a page that shows your keys in addition to Application Add-Ons. In the Application Add-Ons section, turn ON Presence and check Generate Leave on TCP FIN or RST and Global Here Now. Also turn ON PubNub Functions. Make sure to have Access Manager turned off or else the sample code won’t work since you need to include a secret key.

The code you have written so far still won’t work since we haven’t added the callback listener that will listen for all messages sent through the PubNub Network on your channel while the client is connected.

Let’s add the following code where the comment says ADD LISTENER HERE:

// Create PubNub Listener for message events
window.listener = {
  status() {
    // Send fire event to connect to the block
    const requestIntMsg = { requestInt: true, currentLevel: window.globalCurrentLevel, uuid: window.UniqueID };
      message: requestIntMsg,
      channel: window.currentFireChannelName,
      sendByPost: false
  message(messageEvent) {
    if (messageEvent.message.uuid === window.UniqueID) {
      return; // this blocks drawing a new character set by the server, to lower latency
    if (messageEvent.channel === window.currentFireChannelName) {
      window.globalLastTime = messageEvent.timetoken; // Set the timestamp for when you send fire messages to the block
      if (messageEvent.message.int === true && messageEvent.message.sendToRightPlayer === window.UniqueID) { // If you get a message and it matches with your UUID
        window.globalLevelState = messageEvent.message.value; // Set the globalLevelState to the information set on the block
        window.StartLoading(); // Call the game state start function in onLoad
    if (window.globalOtherHeros) { // If player exists
      if (messageEvent.channel === window.currentChannelName) { // If the messages channel is equal to your current channel
        if (!window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message isn't equal to your uuid
          window.globalGameState._addOtherCharacter(messageEvent.message.uuid); // Add another player to the game that is not yourself
          window.sendKeyMessage({}); // Send publish to all clients about user information
          const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid);
          otherplayer.position.set(messageEvent.message.position.x, messageEvent.message.position.y); // set the position of each player according to x y
          otherplayer.initialRemoteFrame = messageEvent.message.frameCounter;
          otherplayer.initialLocalFrame = window.frameCounter;
          otherplayer.totalRecvedFrameDelay = 0;
          otherplayer.totalRecvedFrames = 0;
        if (messageEvent.message.position && window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message contains the position of the player and the player has a uuid that matches with one in the level
  presence(presenceEvent) { // PubNub on presence message / event
    let occupancyCounter;
    function checkFlag() {  // Function that reruns until response
      if (window.globalOtherHeros && checkIfJoined === true) { // If the globalother heros exists and if the player joined equals true
        clearInterval(occupancyCounter); // Destroy the timer for that scene
        window.updateOccupancyCounter = true; // Update the variable that stops the timer from running
        // Run PubNub HereNow function that controls the occupancy
            includeUUIDs: true,
            includeState: true
          (status, response) => {
            // If I get a valid response from the channel change the text objects to the correct occupancy count
            if (typeof (response.channels.realtimephaser0) !== 'undefined') {
              textResponse1 = response.channels.realtimephaser0.occupancy.toString();
            } else {
              textResponse1 = '0';
            if (typeof (response.channels.realtimephaser1) !== 'undefined') {
              textResponse2 = response.channels.realtimephaser1.occupancy.toString();
            } else {
              textResponse2 = '0';
            if (typeof (response.channels.realtimephaser2) !== 'undefined') {
              textResponse3 = response.channels.realtimephaser2.occupancy.toString();
            } else {
              textResponse3 = '0';
            window.text1 = `Level 1 Occupancy: ${textResponse1}`;
            window.text2 = `Level 2 Occupancy: ${textResponse2}`;
            window.text3 = `Level 3 Occupancy: ${textResponse3}`;
    if (window.updateOccupancyCounter === false) {
      occupancyCounter = setInterval(checkFlag, 200); // Start timer to run the checkflag function above
    if (presenceEvent.action === 'join') { // If we receive a presence event that says a player joined the channel from the PubNub servers
      checkIfJoined = true;
      // text = presenceEvent.totalOccupancy.toString()
      if (presenceEvent.uuid !== window.UniqueID) {
        window.sendKeyMessage({}); // Send message of players location on screen
    } else if (presenceEvent.action === 'leave' || presenceEvent.action === 'timeout') {
      try {
        window.globalGameState._removeOtherCharacter(presenceEvent.uuid); // Remove character on leave events if the individual exists
      } catch (err) {
        // console.log(err)

Now let’s go through this code to look at what it’s doing. The listener is listening for events every frame but will only run on the initial connection status to PubNub, or when a message is sent on the channel or when a presence change occurs.

In the status(status) callback, it’s sending a fire message to the block to request level information from the KV store.

In the message(messageEvent) callback function, we check to see if the message channel name is equal to the current fire channel name. If it is, call the start loading function to load the game. Then after that if statement, we check to see if the message channel is equal to the window.currentChannelName. If it is equal and it’s not a message from yourself, add another player to the game and set its position in the correct location based on the message data.

In the presence(presenceEvent) callback function, we have a function that runs if someone joins, leaves or timeouts of the channel, or if window.updateOccupancyCounter is equal to false. We then run PubNub’s hereNow API function that checks to see how many people are in the channel and outputs the current occupancy along with the UUID’s in the channel. We are only checking the amount of players in the channel when a presence event is called which is optimized compared to calling the function every frame.

Now right below that function we just wrote but above the load external javascript files code, copy and paste these last two functions. One will send messages out to all clients connected to the channel. The message will contain player UUID information, position and frame count. The second function will send a message to the block telling it the current cache state of the user.

window.sendKeyMessage = (keyMessage) => {
  try {
    if (window.globalMyHero) {
        message: {
          uuid: window.UniqueID,
          position: window.globalMyHero.body.position,
          frameCounter: window.frameCounter
        channel: window.currentChannelName,
        sendByPost: false, // true to send via posts
      // console.log("send message!")
  } catch (err) {
window.fireCoins = () => {
  const message = {
    uuid: window.UniqueID,
    coinCache: window.globalLevelState.coinCache,
    currentLevel: window.globalCurrentLevel,
    time: window.globalLastTime
  // console.log('fireCoins', message);
      channel: window.currentFireChannelName,
      sendByPost: false, // true to send via posts

Uncomment code

Now in order to make the function work, we have to go uncomment some code we left commented out before. Go to the bottom of main.js where the event listener loads the scene. Uncomment the following:

window.createMyPubNub(0); // Connect to the PubNub network and run level code 0
window.StartLoading = function () {
    game.state.start('loading'); // Run the loading function once you successfully connect to the PubNub network

Now go to playState.js uncomment window.fireCoins(); in the logCurrentStateCoin() function. Now go down to the _handleInput() function and uncomment:

window.sendKeyMessage({ left: 'down' });
window.sendKeyMessage({ left: 'up' });
window.sendKeyMessage({ right: 'down' });
window.sendKeyMessage({ right: 'up' });
window.sendKeyMessage({ up: 'down' });
window.sendKeyMessage({ up: 'up' });
window.sendKeyMessage({ stopped: 'not moving' });

Then in the _spawnCharacters() function uncomment:


Now save your files and refresh your window. If you open up two separate windows of the game, you should be able to move your character on one window and see the character move on the other window with very low latency. This is all powered by PubNub’s real-time Data Stream Network and API.

PubNub Functions to Manage Game State

You will notice that if you collect a coin in one window, then open up another window, all the coins will show up for the new player that joined but there will be coins missing on the other players screen. We can prevent this from happening by using a PubNub Function. Make sure to go uncomment window.fireCoins() that you commented earlier in the logCurrentStateCoin function.

Then go to https://pubnub.com and login to your dashboard. Once loaded, click on your application then on the left hand bar, click the Functions box. Click the Create Module button and name it whatever you wish. Then create a new Function and call it whatever you wish. Make sure you select Before Publish or Fire and the channel name is realtimephaserFire2.

Now copy and paste the following code into the portal:

export default (request) => { 
    const pubnub = require('pubnub');
    const db = require('kvstore');
    const xhr = require('xhr');
    const keyName = "gamestate2_" + request.message.currentLevel;
    if(request.message.int || request.message.fromServer) {
        return request.ok(); // Return a promise when you're done 
      db.get(keyName).then((value) => {
                "channel": "realtimephaserFire2",
                "message": {
                    value: value,
                    int: true,
                    sendToRightPlayer: request.message.uuid
            }).then((publishResponse) => {
        return request.ok(); // Return a promise when you're done
    console.log("spitout", request.message)
    pubnub.time().then((timetoken) => {
        db.get(keyName).then((value) => {
            if(value === null || value.time < request.message.time || true) {
                value = {time: timetoken, coinCache: request.message.coinCache};
                db.set(keyName, value, 1);
                 console.log('set', keyName, value);
                "channel": "realtimephaserFire2",
                "message": {value: value, fromServer: true}
            }).then((publishResponse) => {
    return request.ok(); // Return a promise when you're done 

This code saves the game state in the PubNub Function. The information saved is the contents of the JSON level information. Essentially if JSON information exists in the Function, publish that information to the newly connected user. If there is no information, use whatever JSON information is local on the client. Also the only time the JSON updates in the PubNub Function is when a coin is collected in the scene by any player.

Now click the + button in the PubNub Functions dashboard and create a new function and name it onLeave except call it only After Presence and on the channel realtimephaserFire2. Now copy and paste the following code:

export default (request) => { 
    const pubnub = require('pubnub');
    const db = require('kvstore');
    const xhr = require('xhr');
    if(request.message.occupancy === 0){ 
        for(var currentLevel = 0; currentLevel < 100; currentLevel++) {
            const keyName = "gamestate2_" + currentLevel;
    return request.ok(); // Return a promise when you're done 

This PubNub Function will only run when someone joins, leaves or timeouts of a channel. If the total occupancy of the game equals zero, it resets the current level cache to nothing so the coins will appear for anyone new that joins the game.

Wrapping Up

And that’s it! We hope you enjoyed the tutorial, and can use it as the basis for launching a multiplayer game of your own. We’re looking forward to launching more multiplayer gaming tutorials in the future so stay tuned.

Questions? Tweet me @NinjaPigStudios. I’m here to help!

Free up to 1MM msgs/ month
Customize fully-featured, in-app experiences—chat, notifications, geolocation, IoT devices, collaboration, and more.
Try our APIs
Free up to 1MM msgs/ month

More From PubNub