Loading deck

GameDev with phaser - pt 3

Playing with WebSockets

Who We are

We Make software
and have fun doing it

What we will build

 a base for multiplayer games

With the basic features
and patterns that can be followed to extend this into a game

screenshot

What we will use

 Websockets are awesome!!!

for pushing updates and broadcasting

server side WS node module
https://github.com/websockets/ws

WebRTC !

they are not the best solution for most multiplayer games

For that real time update

Incheon*

Real time multiplayer server framework

*It's new, I haven't tested it

Start with forking

just to get package.json

install dependencies with yarn or npm

https://github.com/GomaGames/Phaser-Socks

Websockets

with express

Hello world

Setup an express app

we'll only use express to serve static assets
you can use it later to expose an api

const express = require('express');
const app = express();

import 'ws' and 'http'

create an http server and wss server

const { Server : WebSocketServer } = require('ws');
const server = require('http').createServer();
const wss = new WebSocketServer({ server });

Configurable port

const PORT = process.env.PORT || 3000;

api endpoint example

app.get('/api/hello', (req, res) => {
  const hello = 'world';
  res.json({ hello });
});

Websocket server

wss.on('connection', client => {

  client.on('message', message  => {
    console.log('received: %s', message);
  });

  client.send('connected to ws server');
});

CONNECT LISTENER

server listen for requests

server.on('request', app);
server.listen(PORT, _ => 
  console.log('Server Listening on ' + server.address().port)
);

api requests and wss connections

start the server

npm run dev

Test the api

curl localhost:3000/api/hello

Test the wss

open http://www.websocket.org/echo.html

twice. set the server ws://localhost:3000 and connect

wss client management

track players on the server
Set a Standard protocol

The "OP" Protocol

looks like this

{
  "OP": [STANDARD OP CODE],
  "payload": [OPTIONAL, ANY MEANINGFUL DATA]
}
{
  "OP": "REGISTER",
  "payload": {
    "username": "Link"
  }
}
{
  "OP": "REGISTERACK"
}

Client sends

server sends

{
  "OP": "ERROR",
  "payload": {
    "error": "The username 'Link' is not available."
  }
}

or

initialize a map to store players

// "username" => client
const players = new Map();

handle client messages

wss.on('connection', client => {
  client.username = null;

  client.on('message', clientReceiveMessage.bind(client));

});

THE PRIMARY LOCATION FOR GAME LOGIC

function clientReceiveMessage( message ){

}

Parse incoming messages

using our standard "OP" protocol, and handle errors

in clientReceiveMessage
  let msg;
  try{
    msg = OP.parse(message);
  }catch(error){
    console.error(error);
    return this.send(OP.create(OP.ERROR, { error }));
  }

OP handler

shared module, between server and client

in ./public/js/OP.js
(function(){




  /* Make this module available to Node and Browser */
  const root = this;
  if( typeof exports !== 'undefined' ) {
    if( typeof module !== 'undefined' && module.exports ) {
      exports = module.exports = OP;
    }
    exports.OP = OP;
  }
  else {
    root.OP = OP;
  }

}).call(this);

OP handler

define the OP module

in ./public/js/OP.js
  /*
   * Helper methods
   */
  const parse = message => {
    let parsedMessage = JSON.parse(message);
    if( !parsedMessage.hasOwnProperty('OP') ){
      throw new Error('Improperly formatted OP message.');
    }
    return parsedMessage;
  };

  const create = (OP, payload) => JSON.stringify({
    OP,
    payload,
  });

  /*
   * OP codes
   */
  const ERROR = 'ERROR';

  /*
   * the module
   */
  const OP = {
    create,
    parse,
    ERROR,
  };

import the op module

const OP = require('./public/js/OP');
in ./index.js

register the user

  // trap unregistered users
  if( this.username === null ){
    // wait for OP:REGISTER
    if( msg.OP === OP.REGISTER ){
      // add the player to players
      if( players.has(msg.payload.username) ){
        // player name is taken
        const error = `username: '${msg.payload.username}' is not available.`;
        this.send(OP.create(OP.ERROR, { error }));
      } else {
        // username is available, register the player
        this.username = msg.payload.username;
        players.set(this.username, this);
        this.send(OP.create(OP.REGISTERACK));
      }
    } else {
      const error = `You are not registered yet. Register with OP:REGISTER first.`;
      this.send(OP.create(OP.ERROR, { error }));
    }
    return; // trap
  }

set the username and add to the players Map

in clientReceiveMessage

Add the two new OP codes

OP:REGISTER and OP:REGISTERACK

Test

websocket.org echo server

{"OP":"REGISTER","payload":{"username":"Link"}}

in the other tab send this to see the Error

{"OP":"REGISTER","payload":{"username":"Link"}}

now see that it works

{"OP":"REGISTER","payload":{"username":"Zelda"}}

wss client cleanup

prevent bad things from happening

When a client disconnects

'unregister' by removing the client from the map

hint

  client.on('close', clientDisconnect.bind(client));

handle client disconnect

function clientDisconnect(){
  if( this.username !== null ){
    if( players.has(this.username) ){
      players.delete(this.username);
    }
  }
  console.info(`Client username:'${this.username}' has disconnected.`);
}

handle socket write errors

if there's an error, disconnect the client

  client.sendOp = sendOp;
// handles errors
function sendOp(op, payload){
  this.send(OP.create(op, payload), error => {
    if( error !== undefined ){
      console.error(`Error writing to client socket`, error);
      clientDisconnect.call(this);
    }
  });
}

refactor all instances of   this.send(...)

Test with websocket.org

  1. Connect
  2. OP:REGISTER
  3. Disconnect
  4. Inspect logs
  5. Connect
  6. (don't register)
  7. Disconnect
  8. Inspect logs

 wss broadcast

OP:Chat, Payload:{ message }

client handle op

  this.clientHandleOp(msg);
function clientHandleOp( msg ){
  let error;

  switch( msg.OP ){
    case OP.REGISTER:
      error = `You are already registered as: '${this.username}'`;
      this.sendOp(OP.ERROR, { error });
      break;
    case OP.CHAT:

      break;
    default:
      error = `Unknown OP received. Server does not understand: '${msg.OP}'`;
      console.warn(error);
      this.sendOp(OP.ERROR, { error });
      return;
  }
}
  client.clientHandleOp = clientHandleOp;
in clientReceiveMessage

Using a new OP code

add this OP CODE to the op module

OP:CHAT

Send payload

{
  "OP":"CHAT",
  "payload": {
    "message": "Hi everyone!"
  }
}

Broadcast payload

{
  "OP":"CHAT",
  "payload": {
    "username": "Zelda",
    "message": "Hi everyone!"
  }
}

Implement Op:CHAT

on your own

Test in websocket.org

  1. Client 1 connect
  2. {"OP":"REGISTER","payload":{"username":"Link"}}
  3. Client 2 connect
  4. {"OP":"REGISTER","payload":{"username":"Zelda"}}
  5. {"OP":"CHAT","payload":{"message":"Hi everyone!"}}
  6. {"OP":"CHAT","payload":{"message":"Hi Zelda!"}}

Setup the phaser game

using the phaser-starter-kit

use git to grab the starter-kit

should commit first!

git checkout origin/feature/phaser-starter-kit public

This is super important!

do this carefully

serve up the game

using express static middleware

app.use(express.static('./public'));

test out the phaser app

open localhost:3000 in your browser

Implement OP:ENTER_WORLD

OP:ENTER_WORLD

When the player enters the game, give them the current state

in index.js
case OP.ENTER_WORLD:
  // give current player initial state of the game, no coords
  let playerUsernamesAvatars = [];
  for (let { username, avatarId } of players.values()) {
    playerUsernamesAvatars.push({username, avatarId});
  }
  this.sendOp(OP.ENTER_WORLD_ACK, playerUsernamesAvatars);

  // broadcast new player
  players.forEach( (player, playerUsername, map) => {
    if(player !== this){
      player.sendOp(OP.NEW_PLAYER, { username : this.username, avatarId : this.avatarId });
    }
  });
  break;

test it out

inspect the network inspector

Implement OP:MOVE_TO

Here's the pseudocode

implement this on your own or with your neighbors

case OP.MOVE_TO:
  // get the position from the payload
  
  // loop through every player
  //   fore every player that is NOT the current player (this)
  
  // send OP:MOVE_TO sending { username, position }

  break;

That's it!

what's next?

Final thoughts