NodeJS/React Multiplayer Space Sim walkthrough

Update- www.cmdship.net

This started out from the prior mmo game blog post and spiraled out of control somewhat. I will probably break it into parts as I expand on sections that are not clear, but for now it is in a single post. www.cmdship.net currently has a copy of the app running on it . My current idea is to develop a multiplayer RPG sim on that domain which will be based on the idea in this blog post - the details of which I will put into a post once the idea is fully formed.

Overview

So I am going to build a basic browser based multiplayer space game/sim using NodeJS/React/MongoDB and HTML5 canvas. As I build the part I will bung in the code or alterations, attempt to explain what it does/should do - which if you manage to decipher and understand should end up looking something like the following image. The latest source will be at: https://github.com/dbjsdev/cmdship. At the end of each part you will find a link to the source code for that part.

mainimage

Parts so far..

Part 1 - The Idea and Initial framework

Warning: This isn’t intended to be a guide on what is the best way to do things - I am making this up as I go, and then later coming back to fix it up - the further it goes the more mad it may get and performance will probably be terrible. It will routinely include code that probably violates all sorts of rules and some of it may not even make any sense due to the nature of how the early parts were written. I do however plan to keep returning and updating the prior sections as I work and improve the code. Some of the comments were written as I wrote the post, some I have added in later for clarity and will attempt to do this further as I get time. At times it may just seem like endless walls of code pasted in - as it progresses further this will hopefully reduce. At the end of each part I will put in a link to the source up until that point. If you spot any issues, or have ideas for how parts can be improved/done better, feel free to leave a comment at the bottom of the post or contact me and I will try implement it.

The basic idea is a browser based sim, with simple polygons and shapes only - no flash graphics, html5 canvas only,whereby a player registers/connects and is then loaded into a 2d game world in space, controlling a spaceship. The user can eject from the spaceship, and fly about as a PTU(a small pod thing kind of like in eve), and board other empty spacecraft. The game world will be broken into Zones or areas that represent star systems. Users can then move between star systems using a series of stargates. The player ship will have a hyperdrive which lets them move between locations inside a star system, and also move the ship using thrust and direction keys and also an autopilot. The ship will have modules which can fitted such as a cannon, mining laser, shield etc. The game interface will have an item system which allows cargo to moved between locations such as a storage object and the ships cargo hold. The player can also dock with space stations and store vehicles and cargo there. I am also going to integrate a cryptocurrency into the app for the in-game currency.

This app is going to consist of two parts. A frontend client which renders the game world to a canvas with React/Redux handling parts of the UI and interface. On the server side there will be a NodeJS server which handles the login/registration and runs the game server with data being persisted to a MongoDB database. The communication between client/server will be done with Express and Socket.io.

My initial task is going to be to build the basic framework for the client and server communicate, login in the user. To start this I will first get Express setup, delivering the client React app built using webpack.

I am going to whizz through and only briefly cover this intial express/react setup. You can find more details on what exactly is going on with some of this in my other posts, or by looking around on google which can explain webpack in more detail. I’ve started by creating a new repo and clone it locally and setup the basic folder structure we’ll use. (Incase it isn’t clear, when in the code snippets I use ‘…’, it is it to make it clear ive ommited sections of the file that I havn’t changed)

mkdir cmdship
cd cmdship
echo "cmdship" > README.md
mkdir dist
mkdir logs
mkdir src
cd src
mkdir server
mkdir client
mkdir common
cd ..

I then ran “npm init”, setting my main entry to ‘src/server/main.js’

I am creating this off using a template I already had so instead of installing what I need via a terminal I copied in the following into my package.json. (Some of these versions you’ll see in this tutorial are what they are due to compatability issues encountered later. It’s possible the issues are fixed by the time you are reading this)

Update your dependencies in package.json to the following and also be sure to add the scripts section.

{
  "name": "cmdship",
  "version": "1.0.0",
  "description": "cmdship",
  "main": "src/server/main.js",
  "scripts": {
    "start": "babel-node $npm_package_main --presets env",
    "start-debug": "babel-node $npm_package_main --inspect-brk --presets es2015",
    "start-debug-watch": "babel-watch $npm_package_main --inspect-brk --presets es2015",
    "start-dev": "babel-watch $npm_package_main --presets es2015",
    "nodemon": "nodemon $npm_package_main --exec babel-node --presets es2015",
    "postinstall": "webpack --display-error-details",
    "build": "webpack",
    "build-watch": "webpack --watch",
    "stats": "webpack --env production --profile --json > stats.json"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "babel-cli": "^6.26.0",
    "babel-loader": "^7.1.5",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "cors": "^2.8.4",
    "css-loader": "^1.0.0",
    "dotenv": "^6.0.0",
    "es2015": "0.0.0",
    "eventemitter3": "^1.2.0",
    "express": "^4.14.0",
    "html-webpack-plugin": "^3.2.0",
    "query-string": "^4.2.3",
    "react": "^16.4.2",
    "react-dom": "^16.4.2",
    "react-scripts": "1.1.4",
    "style-loader": "^0.23.0",
    "webpack": "^3.8.1"
  }
}

I then installed the above with

npm install

I created a .gitignore file on the root containing the /node_modules folder.

node_modules/
logs/

I created webpack.config.js on the root.

const path = require('path');
const fs = require('fs');

module.exports = {
  entry: './src/client/app.js', 
  output: {
      path: path.join(__dirname, 'dist'),
      filename: 'bundle.js'
  },
  module: {
    loaders: [
      { test: /\.css$/, loader: "style-loader!css-loader" },
      {
          test: /\.scss$/,
          loaders: ['style-loader', 'raw-loader', 'sass-loader']
      },
      { test: /\.(js|jsx)$/, loader: 'babel-loader', options: {
        presets: ['react','env']
      }, exclude: /node_modules/ }
    ]
  },
  resolve: {
      extensions: ['.js', '.jsx'],
  }
};

Next I created an index.html in my root folder

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>CMDSHIP</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="root"></div>
    <script src="./bundle.js" type="text/javascript"></script>
  </body>
</html>

I then created app.js in src/client/ containing the following basic react code which will output Hello in an H1.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

export default class App extends Component {
  render () {
    return (
      <div>
        <h1>Hello</h1>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

Next in order to run our express server I created the main.js within /src/server/ which we specified in our package.json.

import path from 'path';
import express from 'express';

const PORT = process.env.PORT || 3000;
const INDEX = path.join(__dirname, '../../index.html');

const server = express();

server.get('/', function(req, res) { res.sendFile(INDEX); });
server.use('/', express.static(path.join(__dirname, '../../dist')));

server.listen(PORT, () => console.log(`Listening on ${ PORT }`));

This code just sets up our express server and serves up our index.html on port 3000 to incoming requests.

Finally in order to run this to make sure it works:

npm run-script build
...
npm start

This screenshot of the folder structure in sublime and also the commands run at the terminal should give you an idea of what you should end up with.

folderstructpart1

hello

Part 2 - Authentication

The next step in our framework is going to be authentication. When a user connects to our game we want them to be able to first login and register.

We are going to use express to setup an API at /user with endpoint for register, login and logout. These will make calls to a UserManager which uses Mongoose/MongoDB to authenticate, or register the user.

On our client React app we will have an interface with forms for register and login which will make API calls to the server. When the user logs in or registers successfully, on the server we’ll create them a JWT token, which is sent back to the client and stored. This will be used in subsequent communications to authenticate them.

So first, lets setup the server side API. We’ll start by creating some new folders within /src/server

Before going further I am going to create a logger as we are going to want to see logs of events that happen on the server. We are going to use winston to do this.

Note: Through the subsequent chapers you will notice at times the logging appears incomplete or just wierd - this is due to being added in later on but then moved up here in the walkthrough.

npm install winston --save

So we’ll create /src/server/logger.js with the following code:

const winston = require('winston');

winston.level = process.env.LOG_LEVEL;

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({
      format: 'YYYY-MM-DD hh:mm:ss A ZZ',
    }),
    winston.format.json(),
  ),
  transports: [
    new winston.transports.File({ filename: './logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: './logs/combined.log' }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

module.exports = logger;

This will create a log file error.log which will contain purely errors in order for us to see them more easily should they occur, and another log file combined.log which contains everything. You’ll see this being used further down.

We also want to create a .env file here also… As you’ll see in our initial dependencies list we installed dotenv. This lets us set environment vars in a .env file on the root, which are then read in to be used by referencing process.env.variableName. We’ll use this for storing a few things such as our mongodb address, port and things like our JWT secret which we’ll come to in a bit. Below are the vars you need, with some example values which will need to adjust accordingly depending on your db.

(This needs to be in the root, the same folder as package.json and README.md)

/.env

NODE_ENV=DEVELOPMENT
PORT = 3000
MONGO_DB_URL = mongodb://127.0.0.1:27017/mmonode
LOG_LEVEL=DEBUG
JWTSECRET = dappyNdubz

Next we can create our User model, we need mongoose, and you’ll need mongodb installed. I used a local install of Mongo DB version 4.0.3. We also install bcrypt for our password hashing.

npm install mongoose bcrypt --save

For our User model we will start with the following in /src/server/model/user.js

import mongoose from 'mongoose';
import bcrypt from 'bcrypt';

const SALT_WORK_FACTOR = 10;
const UserSchema = new mongoose.Schema({
  username: {
    type: String,
    index: { unique: true },
    required: true
  },
  password: { type: String, required: true },
  created: {
    type: Date,
    required: true,
    default: new Date(),
  },
  status: { type: Number, default: 2 }
});

UserSchema.methods.comparePassword = function comparePassword(password, callback) {
  bcrypt.compare(password, this.password, callback);
};

// On save, hash the password
UserSchema.pre('save', function saveHook(next) {
  let user = this;

  if (!user.isModified('password')) {
    return next();
  }

  return bcrypt.genSalt(SALT_WORK_FACTOR, (err, salt) => {
    if (err) { return next(err); }

    return bcrypt.hash(user.password, salt, (hashError, hash) => {
      if (hashError) {
        return next(hashError);
      }
      user.password = hash;
      return next();
    });
  });
});


module.exports = mongoose.model('User', UserSchema);

We’ll add more to this User model later but for now this code gives us a user model with our username, password, status and datecreated with a hook on the save method which will has the password before saving. We then have a compare method on the schema for checking a login attempt against the hashed password.

We’ll now create our UserManager.

This is where we will do all our user related activity, in both our api and also later on in our game server. This will use the UserModel we just created. We’ll create this file in /src/server/managers/userManager.js

import logger from '../logger';
import UserModel from '../model/user';

const MIN_USERNAME = 2;
const MIN_PASSWORD = 2;

class UserManager {
  constructor() {

  }

  loginUser(username, password) {
    return new Promise((resolve, reject) => {
      if (username === undefined || password === undefined) {
        reject(new Error('Incorrect username/password'));
        return;
      }

      if (username.length < MIN_USERNAME || password.length < MIN_PASSWORD) {
        reject(new Error('Incorrect username/password'));
        return;
      }

      UserModel.findOne({ username }, (err, user) => {
        if (err) {
          logger.error('Error attempting to find(UserModel) in UserManager');
          reject(new Error('System error'));
          return;
        }

        if (!user) {
          logger.info('User not found');
          reject(new Error('Incorrect username/password'));
          return;
        }

        user.comparePassword(password, (err, isMatch) => {
          if (err) {
            logger.error('Error in compare password');
            reject(new Error('System error'));
            return;
          }

          if (isMatch) {
            resolve(user);
            return;
          }

          logger.info('Invalid password');
          reject(new Error('Incorrect username/password'));
        });
      });
    });
  }

  logoutUser(userID) {
    return new Promise((resolve, reject) => {
      resolve(userID);
    });
  }

  registerNewUser(username, password) {
    return new Promise((resolve, reject) => {
      if (username === undefined || password === undefined) {
        return resolve({result: false, message: "Invalid registration details"});
      }

      if(password.length < 6){
        return resolve({result: false, message: "Invalid registration details: Not long enough"});
      } else if (password.length > 20) {
        return resolve({result: false, message: "Invalid registration details: Too long"});
      } else if (password.search(/\d/) == -1) {
        return resolve({result: false, message: "Invalid registration details: No digit"});
      } else if (password.search(/[a-zA-Z]/) == -1) {
        return resolve({result: false, message: "Invalid registration details: No letter"});
      } else if (password.search(/[^a-zA-Z0-9\!\@\#\$\%\^\&\*\(\)\_\+\.\,\;\:]/) != -1) {
        return resolve({result: false, message: "Invalid registration details: Invalid character"});
      }

      if (username.search(/^[a-zA-Z0-9_]{5,}[a-zA-Z]+[0-9]*$/) == -1) {
        return resolve({result: false, message: "Invalid registration details: Invalid username"});
      }

      UserModel.findOne({ username }, (err, userfound) => {
        if (err) {
          logger.error('Error in UserManager reg');
          reject(new Error('System error'));
          return;
        }
        
        if(userfound) {
        return resolve({result: false, message: "Username already exists"});
        }

        let newUser = new UserModel({
          username,
          password,
        });

        newUser.save((err, user) => {
          if (err) {
            logger.error('Error creating user in UserManager save');
            reject(new Error('System error'));
            return;
          }

          return resolve({result: true, user: user});
        });
      });
    });
  }
  
  verifyActiveToken(userID, token) {
    return true;
  }  
}

module.exports = UserManager;

You’ll see we have three methods loginUser, logoutUser and registerNewUser all of which return a promise back to wherever called it. This ensures nothing is hanging around waiting whilst our queries occur. We also import our logger, and have some temp error messages in there. Our logging occurs simply through importing the logger and then calling logger.error or logger.info, with our message being written to the appropriate log file. The logoutUser doesn’t really do anything yet but will later on.

The method verifyActiveToken is not implemented yet but will be where our code can add the JWT to a list so that it can be blacklisted after the user logs out.

You will also notice some odd very error handling/throwing going on in the above and the code that follows. This is temporary and is going to be reworked later on.

Next we’ll create our controller for our API which will make the calls to our UserManager. This is going to make use of JWT tokens (you can read about these in my prior React/NodeJS tutorial or google so I’ll skip the explaination here) so I’ll install that first.

npm install jsonwebtoken --save

Then within /src/server/controllers , create the file authController.js with the following code.

const jwt = require('jsonwebtoken');
const logger = require('../logger');
const UserManager = require('../managers/userManager');

const userManager = new UserManager();

module.exports = {
  login: (username, password, callback) => {
    userManager.loginUser(username, password)
      .then((user) => {
        logger.info(`${username} logged in successfully.`);
        callback(null, { success: true, userid: user._id, username: username ,tokenID: jwt.sign({ username: user.username, _id: user._id}, process.env.JWTSECRET)});
      })
      .catch((e) => {
        if (e.message === 'System error') {
          callback(e, null);
        } else {
          callback(null, { success: false });
        }
      });
  },
  logout: (userid, username, callback) => {
    userManager.logoutUser(userid)
      .then((userid) => {
        logger.info(`${username} logged out.`);
        callback(null, { success: true });
      })
      .catch((e) => {
        console.log(e);
        if (e.message === 'System error') {
          callback(e, null);
        } else {
          callback(null, { success: false });
        }
      });
  },
  register: (username, password, callback) => {
    userManager.registerNewUser(username, password)
      .then((response) => {
        if(response.result){
          logger.info(`${username} Registered`);
          callback(null, {
            success: true,
            userid: response.user._id,
            username: username,
            tokenID: jwt.sign({ username: response.user.username, _id: response.user._id}, process.env.JWTSECRET)
          });
        } else {
          callback(null, {
            success: false,
            error: response.message
          });
        }
      }).catch((e) => {
        if (e.message === 'System error') {
          callback(e, null);
        }
      });
  },
};

So in there you’ll see the controller had 3 functions, login, logout and register.

The login function takes a username and password as arguments, and a callback and then makes a call to our UserManager’s login function, passing in those args. You’ll see we then do “.then(“, which means the returned Promise that our UserManager login method returns, has completed successfully. It then calls the callback passing in an object that contains a list of variables such as userid and username, and critically the JWT tokenID, which is signed using the userid, username and our JWT secret which we set in our .env environments file. If the UserManager login method fails to login the user in it will return an error which is caught by our “.catch” (I am going to alter this at somepoint to an alternate solution), and the callback is fired with “success: false”.

Logout does not really do anything beyond call the logout method in userManager (which does not do much yet beyond return true) and register is similar to login except it is calling the userManager.registernewuser method, creating a new user and then just like with login we create the JWT tokenID and return it in our callback.

Continuing on we now need our routes file which calls the above controller. Within /src/server/routes , create the file user.js with the following

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');

router.post('/logout', (req, res, next) => {
  authController.logout(req.userData.userid, req.userData.username, (err, result) => {
    if (err) {
      console.log(err);
      res.status(500).json({
        success: 0,
        error: err,
      });
      return;
    }

    if (result.success) {
      res.status(200).json({
        success: 1
      });
    } else {
      res.status(401).json({
        success: 0,
        error: '',
      });
    }
  });
});

router.post('/login', (req, res, next) => {
  authController.login(req.body.username, req.body.password, (err, result) => {
    if (err) {
      console.log(err);
      res.status(500).json({
        success: 0,
        error: err,
      });
      return;
    }

    if (result.success) {
      res.status(200).json({
        success: 1,
        tokenID: result.tokenID,
        username: result.username,
        userid: result.userid,
      });
    } else {
      res.status(401).json({
        success: 0,
        error: '',
      });
    }
  });
});

router.post('/', (req, res, next) => {
  authController.register(req.body.username, req.body.password, (err, result) => {
    if (err) {
      console.log(err);
      res.status(500).json({
        success: 0,
        error: err,
      });
      return;
    }
    if (result.success) {
      res.status(200).json({
        success: 1,
        tokenID: result.tokenID,
        username: result.username,
        userid: result.userid
      });
    } else {
      res.status(400).json({
        success: 0,
        error: result.error
      });
    }
  });
});

module.exports = router;

In the above file we have 3 posts: ‘/logout’, ‘/login’ and ‘/’ (our register). These make calls to the methods in the authController we just made. Upon a successful login or register callback we return a status 200 back to the client and pass back a structure containing the JWT tokenID, username, userid and a status.

One note about the /logout route. You might notice it uses “req.userData.userid” and “req.userData.username”. This does not exist yet. This is a structure which our authentication middleware will pass in which will contain the userid based on their JWT. In order for this to happen we will next create this middleware. In /src/server/middleware/ create the file authCheck.js with the following code:

const jwt = require('jsonwebtoken');
const UserManager = require('../managers/userManager');

const um = new UserManager();

module.exports = (req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).end();
  }

  const token = req.headers.authorization.split(' ')[1];

  return jwt.verify(token, process.env.JWTSECRET, (err, decoded) => {
    if (err) {
      return res.status(401).end();
    }

    if (!um.verifyActiveToken(decoded._id, token)) {
      return res.status(401).end();
    }

    req.userData = {};
    req.userData.tokenID = token;
    req.userData.userid = decoded._id;
    req.userData.username = decoded.username;

    return next();
  });
};

This middleware code takes the JWT, decodes it, and then extracts the users userid and username and places it into the request data for our further routes to use. This helps to ensure the user is who they say they are. You’ll note the use of UserManager.verifyActiveToken - that would be where our yet to be implemented JWT blacklist was checked against. This would help stop anyone reusing someone elses JWT after they log out if they were in some way able to get hold of it.

The final step is now to update our /src/server/main.js file to take into account these changes.

import path from 'path';
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import bodyParser from 'body-parser';
import dotenv from 'dotenv';
dotenv.config()

import logger from './logger';
import userRoutes from './routes/user';
import authCheckMiddleware from './middleware/authCheck';

const PORT = process.env.PORT || 3000;
const INDEX = path.join(__dirname, '../../index.html');

const dbURL = process.env.MONGO_DB_URL;

const server = express();

server.use(cors());
server.options('*', cors());
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: true }));

server.use('/user/logout', authCheckMiddleware);
server.use('/user', userRoutes);

server.get('/', function(req, res) { res.sendFile(INDEX); });
server.use('/', express.static(path.join(__dirname, '../../dist')));

let requestHandler = server.listen(PORT, () => console.log(`Listening on ${ PORT }`));

new Promise((resolve, reject) => {
  mongoose.connect(dbURL, (err) => {
    if (err) {
      logger.error(`Error connecting to: ${dbURL}`);
      return reject(err);
    }
    logger.info('Connected to MongoDB server successfully.');
    return resolve();
  });
}).then(() => {
  logger.info('Ready to initalise game server...');
});

You’ll see we setup our routes on /user and prepare our mongodb connection ready for our game server to be intiialised later on.

We now have a functioning API for registering and logging a user in. The calls can come into our express server, route into our router user.js, which then calls our authController, this in turn calls userManager which checks against/registers to our mongodb. authController takes the result, generates our JWT if needs be, returns the result in an object to the router which then sends the result back to the client.

We now need to build our client which will make the calls to our API.

The next few steps will add quite a few things in. We are going to integrate in react redux. Our store, reducer and actions. Some of the styling here for the UI stuff here is a bit hacked together.

First for our redux structure we’ll create 4 folders, /src/client/actions, /src/client/constants/, /src/client/reducers, /src/client/stores.

We need to install a few things here for react redux

npm install redux react-redux redux-thunk --save

I then installed reactstrap. As a lot of the UI in what follows is a bit of a bodge and if you don’t use the same version numbers I used it is possible you may get issues.

To install what I did was:

npm install --save bootstrap@4.1.3 reactstrap@6.4.0

( There are some instructions here that might help if you have problems: https://www.npmjs.com/package/reactstrap )

Beginning with our redux setup, within /src/client/constants/ create the file actionTypes.js with the following.

export default {
  USER_LOGGED_IN: 'user_logged_in',
  USER_LOGGED_OUT: 'user_logged_out',
  UPDATE_AUTHENTICATIONMESSAGE: 'update_authenticationmessage',
};

Next within /src/client/reducers/ create the file worldReducer.js. In this app we are going to only have a single reducer for everything.

import actionTypes from '../constants/actionTypes';

let initialState = {
  authenticationResponse: localStorage.getItem('token') ? 'Ready to launch' : 'Awaiting login/registration...',
  loggedInStatus: localStorage.getItem('token') ? true : false,
  userid: localStorage.getItem('userid') ? localStorage.getItem('userid') : '',
  username: localStorage.getItem('username') ? localStorage.getItem('username') : ''
};

export default (state = initialState, action) => {
  let updated = Object.assign({}, state)
  switch (action.type) {
    case actionTypes.UPDATE_AUTHENTICATIONMESSAGE:
      updated['authenticationResponse'] = action.msg;
      return updated;
    case actionTypes.USER_LOGGED_IN:
      updated['username'] = action.username;
      updated['userid'] = action.userid;
      updated['loggedInStatus'] = true;
      return updated;
    case actionTypes.USER_LOGGED_OUT:
      updated['username'] = '';
      updated['userid'] = '';
      updated['loggedInStatus'] = false;
      return updated;
    default:
      return state;
  }
};

The above implements our three actions, storing the username, userid, loggedInStatus. You’ll also notice we default our initial state to include items in local storage. This is where we are going to store our JWT returned from the server and our returned userid. This will assist us if the user refreshes the browser by retaining the token so that it immediatly loads them back into the game without needing to log in again.

Next within /src/client/actions/ create the file actions.js as follows:

import actionTypes from '../constants/actionTypes';

export const logoutUser = (socket) => {
  var token = localStorage.getItem('token') || null;

  return (dispatch) => {
    dispatch(updateAuthenticationResponseMessage('Logging out'));
    dispatch(updateAuthenticationResponseMessage('Logged out successfully'))
    dispatch(userLoggedOut());
    localStorage.clear();

    return fetch('http://localhost:3000/user/logout', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization' : `Bearer ${token}`
      },
      body: JSON.stringify({}),
      mode: 'cors'})
    .then( (response) => {
      if (!response.ok) {
        localStorage.clear();
        throw Error(response.statusText);
      }
      return response.json();
    })
    .then( (data) => {
      localStorage.clear();
    })
    .catch( (e) => console.log(e) );
  }
}

export const loginUser = (socket, username, password) => {
  const data = { username, password };

  return (dispatch) => {
    dispatch(updateAuthenticationResponseMessage('Attempting login..'))

    return fetch('http://localhost:3000/user/login', {
      method: 'POST',
       headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      body: JSON.stringify(data),
      mode: 'cors'})
    .then( (response) => {
      if (!response.ok) {
        dispatch(updateAuthenticationResponseMessage('Incorrect username/password'));
        throw Error(response.statusText);
      }
      return response.json();
    })
    .then( (data) => {
      localStorage.setItem('username', data.username);
      localStorage.setItem('userid', data.userid);
      localStorage.setItem('token', data.tokenID);

      dispatch(updateAuthenticationResponseMessage('Ready to launch.'))
      dispatch(userLoggedIn(data.username, data.userid));
    })
    .catch( (e) => {
      dispatch(updateAuthenticationResponseMessage('System offline'));
      console.log(e)
    });
  }
}


export const registerNewUser = (socket, username, password) => {
  const data = { username, password };
  return dispatch => {
    dispatch(updateAuthenticationResponseMessage('Attempting registration..'))

    return fetch('http://localhost:3000/user/', {
      method: 'POST',
       headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      body: JSON.stringify(data),
      mode: 'cors'})
      .then( (response) => {
        if (!response.ok) {
          if(response.status==400){
            return response.json();
          }
          throw Error(response.statusText);
        }
        return response.json();
      })
      .then( (data) => {
        if(data.success){
          localStorage.setItem('username', data.username);
          localStorage.setItem('userid', data.userid);
          localStorage.setItem('token', data.tokenID);

          dispatch(updateAuthenticationResponseMessage('Registered successfully and logged in'))
          dispatch(userLoggedIn(data.username, data.userid));
        } else {
          dispatch(updateAuthenticationResponseMessage(data.error))
        }
      })
      .catch( (e) => {
        dispatch(updateAuthenticationResponseMessage('System offline'));
      });
  }
}


export const userLoggedIn = (username, userid) => {
    return {
        type: actionTypes.USER_LOGGED_IN,
        username: username,
        userid: userid
    }
}

export const userLoggedOut = () => {
    return {
        type: actionTypes.USER_LOGGED_OUT
    }
}

export const updateAuthenticationResponseMessage = (msg) =>{
    return {
        type: actionTypes.UPDATE_AUTHENTICATIONMESSAGE,
        msg: msg,
    }
}

The key things in there are our loginUser, logoutUser and registerNewUser. Each one dispatches “updateAuthenticationResponseMessage”, passing in a message in order to update the UI on an authentication box which we will build shortly. They then do a fetch request against the API calling the relevant resource, gets the response, updates properties in local storage (or in the case of logout, deletes them), then dispatches a login/logout/register action and finally invokes “updateAuthenticationResponseMessage” to inform the UI with what has happened.

We now need to make our store. In /src/client/stores/ create store.js with the following:

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import worldReducer from '../reducers/worldReducer';

const store = createStore(
  combineReducers({
    world: worldReducer,
  }),
  applyMiddleware(
    thunk,
  ),
);

export default store;

With that in place we can now build the foundations of our UI which will dispatch the actions.

We will alter our ./src/client/app.js to be the following:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import 'bootstrap/dist/css/bootstrap.min.css';

import store from './stores/store';
import CmdshipClient from './cmdshipClient';

export default class App extends Component {
  render () {
    return (
      <div>
        <Provider store={store}>
          <CmdshipClient />
        </Provider>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

In there we have added in redux and our store and also you will notice we have imported the bootstrap css for our UI.

Next up we will create that new cmdshipClient.js in /src/client

import React, { Component } from 'react';
import WindowSystem from './UI/windowSystem';

class CmdshipClient extends Component {

  render() {
    return (
      <div>
        <WindowSystem style={{zIndex:1 , position: 'absolute', top:0, left:0, width: 200}}/>
      </div>
    );
  }
}

export default CmdshipClient;

For now don’t worry why that is called WindowSystem (It is going to form the basis of some mental draggable window UI later on).

We’ll make a UI folder within /src/client and then create windowSystem.js within /src/client/UI

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Authentication from './authentication';

class WindowSystem extends Component{
  constructor(props){
    super(props);
  }

  render(){
    const styles = {
      top: 0,
      left: 0,
      width: window.innerWidth,
      height: window.innerHeight,
      position: 'absolute',
      color: '#000000',
    }

    return (
      <div style={styles} id="wr">
        <Authentication />
      </div>
    );
  }
}

const mapStateToProps = state => {
    return {
        world: state.world
    }
}

export default connect(mapStateToProps)(WindowSystem);

This for now will just show our Authentication box.. which we will build in /src/client/UI/authentication.js with the following code:

(Warning: The HTML and CSS in here (and onwards) is a bit naff (as is usual with all html/css/front end I do) - In a later section it will get tidied up slightly )

import React, { Component } from 'react';
import { connect } from 'react-redux'
import { Container, Col, Row, Button, Form, FormGroup, Label, Input, FormText } from 'reactstrap';
import {
  registerNewUser,
  loginUser,
  logoutUser,
  updateAuthenticationResponseMessage
} from '../actions/actions.js';

class Authentication extends Component {

  constructor () {
    super();

    this.state = {
      username: '',
      password: '',
      regUsername: '',
      regPassword: '',
      regPassword2: '',
      toggleReg: false
    }
  }

  componentDidMount(){
    
  }

  handleRegisterButtonClick(e){
    if(this.state.regPassword !== this.state.regPassword2){
      this.props.dispatch(updateAuthenticationResponseMessage('Error: Passwords do not match'));
    }else{
      this.props.dispatch(registerNewUser(this.props.socket, this.state.regUsername, this.state.regPassword));
      this.setState({regUsername: '', regPassword: '', regPassword2: ''});
    }
  }

  handleLaunchButtonClick(e){

  }

  handleDisconnectButtonClick(e){

  }

  handleLoginButtonClick(e){
    this.props.dispatch(loginUser(this.props.socket, this.state.username, this.state.password));
    this.setState({ password: '' });
  }

  handleLogoutButtonClick(e){
    this.props.dispatch(logoutUser(this.props.socket));
  }

  handleUsernameChange(e) {
    this.setState({username: e.target.value});
  }

  handlePasswordChange(e) {
    this.setState({password: e.target.value});
  }

  handleRegUsernameChange(e) {
    this.setState({regUsername: e.target.value});
  }

  handleRegPasswordChange(e) {
    this.setState({regPassword: e.target.value});
  }

  handleRegPassword2Change(e) {
    this.setState({regPassword2: e.target.value});
  }

  showRegistration(val){
    this.props.dispatch(updateAuthenticationResponseMessage('Awaiting login/registration'));
    this.setState({toggleReg: val, password: ''});
  }

  render () {

    const navStyle = {
      backgroundColor: '#222222'
    }

    const navButtonStyle = {
      padding: "5px",
      margin: '5px',
      backgroundColor: '#4444FF',
      color: '#FFFFFF'
    }

    const navButtonStyleInActive = {
      padding: "5px",
      margin: '5px',
      backgroundColor: '#2222BB',
      color: '#FFFFFF'
    }

    const boxStyle = {
      color: '#FFFFFF',
      backgroundColor: '#222222',
      textAlign: "left",
      padding: "1em",
      margin: "1em",
      border: "2px solid #d3d3d3",
      verticalAlign: "middle",
      marginLeft: "auto",
      marginRight: "auto",
      marginTop: "100px"
    }

    const formStyle = {
      padding: '1em'
    }

    const labelStyle = {
      display: 'flex',
      fontWeight: 600
    }

    const buttonStyle = {
      justifyContent: 'flex-end'
    }

    const regPanel = (
      <Form style={formStyle}>

        <Col>
          <FormGroup>
            <Label>Username (In-game character name)</Label>
            <Input
              type="text"
              name="regUsername"
              id="regUsername"
              placeholder="username"
              value={this.state.regUsername}
              onChange={this.handleRegUsernameChange.bind(this)}
            />
          </FormGroup>
        </Col>

        <Col>
          <FormGroup>
            <Label for="examplePassword">Password</Label>
            <Input
              type="password"
              name="password"
              value={this.state.regPassword}
              id="examplePassword"
              placeholder="********"
              onChange={this.handleRegPasswordChange.bind(this)}
            />
          </FormGroup>
        </Col>

        <Col>
          <FormGroup>
            <Label for="examplePassword">Confirm password</Label>
            <Input
              type="password"
              name="password2"
              id="password2"
              value={this.state.regPassword2}
              placeholder="********"
              onChange={this.handleRegPassword2Change.bind(this)}
            />
          </FormGroup>
        </Col>

        <Button style={navButtonStyle} onClick={this.handleRegisterButtonClick.bind(this)} >Register</Button>
      </Form>
  );

  const loginPanel = (
      <Form style={formStyle}>
        <Col>
          <FormGroup>
            <Label>Username</Label>
            <Input
              type="username"
              name="username"
              id="username"
              placeholder="username"
              value={this.state.username || ''}
              onChange={this.handleUsernameChange.bind(this)}
            />
          </FormGroup>
        </Col>
        <Col>
          <FormGroup>
            <Label for="examplePassword">Password</Label>
            <Input
              type="password"
              name="password"
              id="examplePassword"
              placeholder="********"
              value={ this.state.password || '' }
              onChange={this.handlePasswordChange.bind(this)}
            />
          </FormGroup>
        </Col>
        <Button style={navButtonStyle} onClick={this.handleLoginButtonClick.bind(this)}>Login</Button>
      </Form>
    );

  const panelToShow = (this.state.toggleReg) ? regPanel : loginPanel;
  const loginButtonStyle = (this.state.toggleReg) ? navButtonStyle : navButtonStyleInActive;
  const regButtonStyle = (!this.state.toggleReg) ? navButtonStyle : navButtonStyleInActive;

  const notLoggedInPanel = (
  <div>
    <div style={navStyle}>
      <Button style={loginButtonStyle} onClick={this.showRegistration.bind(this, false)}>Log In</Button>
      <Button style={regButtonStyle} onClick={this.showRegistration.bind(this, true)}>Register</Button>
    </div>
    {panelToShow}
  </div>);

  const loggedInPanel = (
  <div>
    <ul>
      <li>Status:</li>
      <li>Online since:</li>
    </ul>
    <Button style={navButtonStyle} onClick={this.handleLaunchButtonClick.bind(this)}>Launch</Button>
    <Button style={navButtonStyle} onClick={this.handleLogoutButtonClick.bind(this)}>Log out</Button>

  </div>);

  const disconnectButton = (<Button style={navButtonStyle} onClick={this.handleDisconnectButtonClick.bind(this)}>Disconnect</Button>);

  return(
    <Container>

      <Row>
      <Col xs="3" sm="3" lg="3" xl="3" md="3">

      </Col>

      <Col style={boxStyle} xs="4" sm="4" lg="4" xl="4" md="4">
      <h1>
      Launchpad
      </h1>
      <p>Status: {this.props.world.authenticationResponse}</p>
        {(!this.props.world.loggedInStatus) ? notLoggedInPanel : loggedInPanel}
      </Col>

      <Col style={boxStyle} xs="2" sm="2" lg="2" xl="2" md="2">
        <div>
         <div></div>
        </div>
      </Col>

      <Col xs="3" sm="3" lg="3" xl="3" md="3">

      </Col>
      </Row>
    </Container>
  );
  }
}

const mapStateToProps = state => {
  return {
    world: state.world
  }
}

export default connect(mapStateToProps)(Authentication);

You will see that the Authentication panel displays the authentication state message that we have in our model. You may notice the socket props - for now don’t worry about that.

At this stage launch and disconnect do nothing, and we will add those in next but for now we can test that our authentication panel works against the API

Incase anything about the above structure wasn’t clear, this should now be your folder structure:

folderstruct

If we now build and run this…

npm run-script build
...
npm start

We should get the following:

loginbox

and you should be able to register, logout and login

loginbox

Link to source files for Part 2

Part 3 - Foundations of the game server

When logged in our client now has a JWT with which it can authenticate itself. We are now going to add socket.io to our server and also implement authentication on that socket connection using the JWT already created by our API. This is so that when our players are sending messages to the game server we know who they are.

To begin this we will build the foundations of our game server by creating a game server class that will be loaded up when our server starts. When users connect they will be registered on the server and on disconnect they will be removed. When a user connects they will be loaded into a Zone. A Zone is a world in the game. We are going to have a Zone for each star system, with the user moving between zones as they travel between star systems.

npm install socket.io --save

To start we are going to create a file in /src/common/ called constants.js with the following:

module.exports = {
  SYSTEM_OFFLINE: 0,
  SYSTEM_STARTING: 2,
  SYSTEM_NOTYETACCEPTINGCONNECTIONS: 3,
  SYSTEM_ACTIVE: 1,
  CONNECT_READY: 'connect_ready',
  INIT_HANDSHAKE: 'init_handshake',
  USER_READY: 'user_ready',
  REGISTER_USER: 'register_user',
  LOGIN_USER: 'login_user',
  REGISTRATION_RESPONSE: 'registration_response',
  LOGIN_RESPONSE: 'login_response',
  LAUNCH_REQUEST: 'launch_request',
  DISCONNECTED: 'disconnected',
  CONNECTED: 'connected',
  CONNECTING: 'connecting'
};

These constants are going to be messages which are common between both our client and server.

We will now make a user class which will represent the live user instance on the server. In /src/server/ we’ll create the file user.js with the following code:

import logger from './logger';

class User {
  constructor(socket, userDocument, userid, username) {
    this.socket = socket;
    this.userid = userid;
    this.username = username;
    this.zoneid = 0;
    this.status = userDocument.status;
  }

  setZone(zoneid) {
    logger.info(`Setting zone on user: ${this.userid} to ${zoneid}`);
    this.zoneid = zoneid;
  }

  getSocket() {
    return this.socket;
  }
}

module.exports = User;

The user object keeps a reference to the users socket and some other data such as their userid, the ID of the zone on the server they are in.

Then we will create our Zone in /src/server/zone.js as follows:

import logger from './logger';
import constants from '../common/constants';

class Zone {
  constructor(zm, um, zoneID, zoneName) {
    this.zoneid = zoneID;
    this.zoneName = zoneName;
    this.zoneManager = zm;
    this.userManager = um;
    this.registeredUsers = [];
  }

  init() {
    return new Promise((resolve, reject) => {
      resolve();
    });
  }

  run() {
  }

  unloadUserFromZone(user) { 
    logger.info('unloading user from zone... ');
    this.removeUserRegistration(user.userid);
  }

  loadUserIntoZone(user) {
    if(this.userIsRegistered(user)){
      logger.error('Attempted to load a user already registered ', user.username);
      return;
    }

    this.registerUserToZone(user);
  }

  userIsRegistered(user) {
    return this.registeredUsers.find((u) => { return u.userid == user.userid; });
  }

  registerUserToZone(user) {
    this.registeredUsers.push(user);
    user.zoneid = this.zoneid;
  }

  removeUserRegistration(useridToRemove) {
    this.registeredUsers = this.registeredUsers.filter((user) => { return user.userid !== useridToRemove});
  }
}

module.exports = Zone;

Now we’ll create this zoneManager… in /src/server/managers/zoneManager.js

import logger from '../logger';
import Zone from '../zone';
import constants from '../../common/constants';

class ZoneManager {
  constructor() {
    this.zones = [];
  }

  init(um) {
    this.userManager = um;
  }

  createZone(zoneid, zoneName){
    return new Zone(this, this.userManager, zoneid, zoneName);
  }

  addZone(newZone) {
    this.zones.push(newZone);
  }

  getZones() {
    return this.zones;
  }

  getZone(zoneid) {
    return this.zones.find((z) => { return z.zoneid === parseInt(zoneid); });
  }
}

module.exports = ZoneManager;

It’s quite straightforward, just a collection of zones and a means of looking up a zone, creating a new zone etc

As you can see each zone has an id and a name. Each zone will have objects and users within it. The zone has a reference to the global zoneManager and userManager (which we will setup shortly) of the server. When a user joins the zone they are added to the ‘registeredUsers’ array, and upon leaving they are removed. An example of how this will work is that, given each Zone is a Star system, as a user travels between zones using ‘stargates’ (which we will build later), the user is registered and removed from zones, but all throughout this time the single global userManager will keep their data. So the userManager keeps track of all users on the server and the zone keeps track of only the users within that particular zone.

Now we will make some changes to the start of /src/server/managers/userManager.js - all the way up to the loginUser method.

import logger from '../logger';
import UserModel from '../model/user';
import User from '../user';

const MIN_USERNAME = 2;
const MIN_PASSWORD = 2;

class UserManager {
  constructor() {

  }
  
  init(zm){
    this.users = []; 
    this.zoneManager = zm;   
  }

  loadUser(socket, userid) {
    return new Promise((resolve, reject) => {
      logger.info(`Attempting to load user ${userid} `);

      UserModel.findOne({ _id: userid }, (err, user) => {
        if (err) {
          logger.error('Error attempting to find(UserModel) in UserManager userLoad');
          reject(new Error('System error'));
          return;
        }
        if(user == null) {
          return reject();
        }

        let newUser = new User(socket, user, userid, user.username);

        resolve(newUser);
      });
    });
  }

  registerUser(newUser){
    this.addUser(newUser);
  }

  unloadUser(socket, userid) {
    logger.info(`Unloading user ${userid} from UserManager`);
    let unloadingUser = this.getUser(userid);
    if (unloadingUser !== undefined) {
      console.log('userzoneid: ', unloadingUser.zoneid);
      let uz = this.zoneManager.getZone(unloadingUser.zoneid)
      if (uz !== undefined) {
        // User is in a zone. Unload them
        uz.unloadUserFromZone(unloadingUser);
      }
      this.removeUser(userid);
    }
  }

  getUser(userid) {
    return this.users.find((user) => { return user.userid == userid; });
  }

  getUsers() {
    return this.users;
  }

  addUser(user) {
    this.users.push(user);
  }

  removeUser(userToRemove) {
    this.users = this.users.filter(
      (user) => { return user.userid !== userToRemove}
    );
  }

  loginUser(username, password) {
  ...
  ... *** From loginUser onwards the file stays the same, only the start has been edited ***
... 

You will see we have added a user collection, and also that the init method takes a reference to the zoneManager. loadUser looks up the users details in the db and if found creates a user object (which we just previously wrote) and returns it ready for registerUser to be called which adds it to the collection of ‘online users’.

unloadUser does the opposite, this would be called on an event such as a user disconnecting. Given a userid it looks up the user, finds what zone they are in, removes them from it, and then finally removes them from the global user collection.

With that all done we can integrate these zones and users into our server. But before going further we need to install socketio-jwt first. It seems this is a bit out of date but it does the job we want.

npm install socketio-jwt --save

Now we will finally create our game server. In /src/server/ create cmdShipServer.js with the following:

import socketioJwt from 'socketio-jwt';

import ZoneManager from './managers/zoneManager';
import UserManager from './managers/userManager';
import logger from './logger';
import constants from '../common/constants';

class CmdShipServer {

  constructor(io) {

    this.systemStats = {
      startupTime: new Date(),
      status: constants.SYSTEM_OFFLINE
    };

    this.io = io;

    this.userManager = new UserManager();
    this.zoneManager = new ZoneManager();

    this.userManager.init(this.zoneManager);
    this.zoneManager.init(this.userManager);

    io.sockets.on('connection', socketioJwt.authorize({
        secret: process.env.JWTSECRET,
        timeout: 15000
      })).on('authenticated', (socket) => {

      if (this.systemStats.status === constants.SYSTEM_OFFLINE || this.systemStats.status === constants.SYSTEM_STARTING) {
        socket.emit(
          'command_log',
          (this.systemStats.status === constants.SYSTEM_OFFLINE) ? 'System offline' : 'System starting up'
        );
        logger.info('Disconnecting user attempting connection.');
        socket.disconnect();
        return;
      }

      logger.info(`Client established connection and authenticated. ${socket.id} ${socket.decoded_token.username}. Connected clients: ${io.engine.clientsCount}`);
      socket.emit(
        constants.CONNECT_READY,
        { systemStats: this.systemStats },
      );
      socket.on(constants.INIT_HANDSHAKE, (data, fn) => {
        logger.info(`${socket.decoded_token.username} invoked handshake.`);
        fn(new Date().getTime());
      });

      socket.on(constants.LAUNCH_REQUEST, (data) => {
        logger.info(`${socket.decoded_token.username} invoked launched.`);
        this.userManager.loadUser(socket, socket.decoded_token._id).then((loadedUser) => {
          this.userManager.registerUser(loadedUser);
          this.userLoad(loadedUser);
        }).catch((e) => {
          console.log(e);
          logger.info(`User cannot launch.`);
          socket.disconnect();
        });
      });

      socket.on('disconnect', () => {
        logger.info(`${socket.decoded_token.username} disconnected.`);
        let user = this.userManager.getUser(socket.decoded_token._id);
        if (user !== undefined && user !== null) {
          this.userExit(user).then(() => {
            this.userManager.unloadUser(socket, socket.decoded_token._id);
          });
        }
      });


    });

    return this;
  }

  init() {
    return new Promise((resolve, reject) => {
      logger.info(`Game Server init began...`);

      let zoneLoadPromises = [];

      let newZone = this.zoneManager.createZone(1, "Alpha Centuri B");
      zoneLoadPromises.push(newZone.init());
      this.zoneManager.addZone(newZone);

      return Promise.all(zoneLoadPromises).then(() => {
        return resolve();
      }).catch((e) => {
        console.log(e);
        logger.error('Error initializing Zones');
      });
    });
  }

  run() {
  }

  start() {
    console.log('started...')
  }

  userLoad(user) {
    logger.info(`Game: User loaded ${user.userid}`);

    let userZone = this.zoneManager.getZone(1);
    userZone.loadUserIntoZone(user);    
  }

  userExit(user) {
    return new Promise((resolve, reject) => {
      logger.info(`Game: User exiting ${user.userid}`);
      resolve();
    });
  }

}

export default CmdShipServer;

You’ll see the constructor takes an argument io, this is our socket server. main.js is going to create our gameserver and pass in this arg. In the constructor you will see we first create an object containing some systemStats, this is going to be information on the state of the server such as whether it is online/offline/initialising. We are going to have it so that our clients can request this information via the API so that it is displayed on the authentication screen. We then create server wide instances of userManager and zoneManager, and then initialise them, passing each a reference to the other (there are probably far better ways of doing this obviously). Following that is the socket code..socketioJwt is used to check that the client socket is connecting with a valid JWT (which the client should have recieved when they logged in via the API). I’ll break down what is happening there:

    io.sockets.on('connection', socketioJwt.authorize({secret: process.env.JWTSECRET, timeout: 15000})).on('authenticated', (socket) => {
      if (this.systemStats.status === constants.SYSTEM_OFFLINE || this.systemStats.status === constants.SYSTEM_STARTING) {
        socket.emit('command_log', (this.systemStats.status === constants.SYSTEM_OFFLINE) ? 'System offline' : 'System starting up' );
        logger.info('Disconnecting user attempting connection.');
        socket.disconnect();
        return;
      }

So on a connection which has been authenticated.. first check if the server is online (using this.systemStats.status). If it isn’t.. we send back a message to the user (via their socket using .emit) - telling them it is either offline or starting up, and then we disconnect the socket.

      logger.info(`Client established connection and authenticated. ${socket.id} ${socket.decoded_token.username}. Connected clients: ${io.engine.clientsCount}`);
      socket.emit(
        constants.CONNECT_READY,
        { systemStats: this.systemStats },
      );

if it was online.. it proceeds, logs out some debug info, and then sends a CONNECT_READY message to the connecting client

When our client receives this CONNECT_READY - it is going to send to the server a ‘INIT_HANDSHAKE’ message (We will code this shortly)

      socket.on(constants.INIT_HANDSHAKE, (data, fn) => {
        logger.info(`${socket.decoded_token.username} invoked handshake.`);
        fn(new Date().getTime());
      });

The above code is the handler for the server recieving that INIT_HANDSHAKE from the client. It returns back the current time on the server (Our client needs to know the time on the server for keeping things in sync and determining lag).

When the client receives this response (and timestamp).. it will finally send in a LAUNCH_REQUEST message.

      socket.on(constants.LAUNCH_REQUEST, (data) => {
        logger.info(`${socket.decoded_token.username} invoked launched.`);
        this.userManager.loadUser(socket, socket.decoded_token._id).then((loadedUser) => {
          this.userManager.registerUser(loadedUser);
          this.userLoad(loadedUser);
        }).catch((e) => {
          console.log(e);
          logger.info(`User cannot launch.`);
          socket.disconnect();
        });
      });

The above is the handler for that LAUNCH_REQUEST message from the client. This is the stage which loads the user into the game. Using the JWT it calls our loadUser method in userManager (which if you remember pulls out the users data), it then registers the user with the userManager before finally calling userLoad. userLoad gets the users zone (for now there is only 1), and then passes the user to the zone telling it to load the user into it (For now zone.userLoad does nothing other than register the user as being in the zone)

      socket.on('disconnect', () => {
        logger.info(`${socket.decoded_token.username} disconnected.`);
        let user = this.userManager.getUser(socket.decoded_token._id);
        if (user !== undefined && user !== null) {
          this.userExit(user).then(() => {
            this.userManager.unloadUser(socket, socket.decoded_token._id);
          });
        }
      });


    });

Finally we have our handler for a user disconnecting. It looks up the user using the JWT, then invokes our previously written userManager.unloadUser method which looks up what zone they are in, removes them from it, and then unloads them from the global userManager.

Also within the cmdShipServer you will see the init method. This currently only sets up 1 zone and initialises it. Later on this will contain multiple zones. As loading a zone will involve loading up persistant objects into it such as space stations, it may take some time. For this reason we will place the promises from the init methods into a collection, and then using Promises.all we wait until they are all ready.

Currently the start method does nothing, but later on this will be where our gameLoop runs.

Next…

Finally we need to alter main.js to launch and setup this game server - In /src/server/main.js we modify as follows:

import path from 'path';
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import bodyParser from 'body-parser';
import socketIO from 'socket.io';
import dotenv from 'dotenv';
dotenv.config()

import logger from './logger';
import userRoutes from './routes/user';
import authCheckMiddleware from './middleware/authCheck';
import CmdShipServer from './cmdShipServer';
import constants from '../common/constants';

const PORT = process.env.PORT || 3000;
const INDEX = path.join(__dirname, '../../index.html');

const dbURL = process.env.MONGO_DB_URL;

const server = express();

server.use(cors());
server.options('*', cors());
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: true }));

server.use('/user/logout', authCheckMiddleware);
server.use('/user', userRoutes);

server.use('/status', authCheckMiddleware);
server.get('/status', (req, res) => {
  res.send(cmdShipServer.systemStats);
});

server.get('/', function(req, res) { res.sendFile(INDEX); });
server.use('/', express.static(path.join(__dirname, '../../dist')));

let requestHandler = server.listen(PORT, () => console.log(`Listening on ${ PORT }`));
const io = socketIO(requestHandler);

const cmdShipServer = new CmdShipServer(io);

new Promise((resolve, reject) => {
  mongoose.connect(dbURL, (err) => {
    if (err) {
      logger.error(`Error connecting to: ${dbURL}`);
      return reject(err);
    }
    logger.info('Connected to MongoDB server successfully.');
    return resolve();
  });
}).then(() => {
  logger.info('Initializing game server...');
  cmdShipServer.systemStats.status = constants.SYSTEM_STARTING;
  return cmdShipServer.init();
}).then(() => {
  logger.info('Init complete. Starting game server process...');
  cmdShipServer.start();
  cmdShipServer.systemStats.status = constants.SYSTEM_ACTIVE;
});

So… main.js has changed. I’ll step through these changes - first, instead of just being an express server delivering an API, we now created a cmdShipServer passing in socketIO.

let requestHandler = server.listen(PORT, () => console.log(`Listening on ${ PORT }`));

const io = socketIO(requestHandler);
const cmdShipServer = new CmdShipServer(io);

Beneath this you can see our promise chain whereby after our mongooseDB has successfully connected, we run an init method on this cmdShipServer (which we just covered - the zone init methods), which will setup up our game server, when this promise returns successfully we then run start on the cmdShipServer.

You might also spot this:

server.use('/status', authCheckMiddleware);
server.get('/status', (req, res) => {
  res.send(cmdShipServer.systemStats);
});

This now lets clients call /status on the server in order to pull down the online/offline state of the gameServer

Ok that is the server parts updated… Next the client needs updating so that it makes a socket connection to the server and sends the required messages, we’ll also have our client make use of the new getSystemStatus abilities of the server so that when our user is logging in, they know whether the server is operational or not.

We’ll begin by altering /src/client/constants/actionTypes.js to have our new actions, as follows:

export default {
  USER_LOGGED_IN: 'user_logged_in',
  USER_LOGGED_OUT: 'user_logged_out',
  UPDATE_AUTHENTICATIONMESSAGE: 'update_authenticationmessage',
  RESET_GAME_STATE: 'reset_game_state',
  USER_LOGGED_IN: 'user_logged_in',
  USER_LOGGED_OUT: 'user_logged_out',
  SYSTEM_STATUS: 'system_status',
  COMMAND_LOG_RECEIVED: 'command_log_received',
  DISCONNECT: 'disconnect',
  USER_READY: 'user_ready',
  INIT_HANDSHAKE_RETURNED: 'init_handshake_returned',
  CONNECTING: 'connecting'  
};

Now in /src/client/actions/actions.js we are going to add some methods: (remember, … means I have excluded the existing unchanged code)

...
import constants from '../../common/constants.js';
...
export const launchUser = () => {
  return (dispatch) => {
    dispatch(resetGameState());
    dispatch(connecting());
  }
}

export const disconnectUser = (socket) => {
  return (dispatch) => {
    dispatch(disconnectReceived());
    dispatch(resetGameState());
    socket.disconnect();
  }
}


export const getSystemStatus = () => {
  var token = localStorage.getItem('token') || null;

  return (dispatch) => {
    return fetch('http://localhost:3000/status', {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization' : `Bearer ${token}`
      },
      mode: 'cors'})
    .then( (response) => {
      if (!response.ok) {
          throw Error(response.statusText);
      }
      return response.json();
    })
    .then( (data) => {
      dispatch(systemStatusReceived(data));
    })
    .catch( (e) => console.log(e) );
  }
}

export const initHandshake = (socket) => {
  return (dispatch) => {
    let client_timestamp = new Date().getTime();
    socket.emit(
      constants.INIT_HANDSHAKE,
      { ts: client_timestamp }, (data) => {
        console.log('remote ts: ', data);
        dispatch(initReturned(socket, (new Date().getTime() - client_timestamp), client_timestamp, data))
    });

  }
}

export const systemStatusReceived = (status) => {
    return {
        type: actionTypes.SYSTEM_STATUS,
        status: status
    }
}

export const initReturned = (socket, lag, cts, sts) => {
    socket.emit(constants.LAUNCH_REQUEST, {}, () => {});

    return {
        type: actionTypes.INIT_HANDSHAKE_RETURNED,
        lag: lag,
        cts: cts,
        sts: sts
    }
}

export const resetGameState = () => {
    return {
        type: actionTypes.RESET_GAME_STATE
    }
}

export const userReadyReceived = () => {
    return {
        type: actionTypes.USER_READY
    }
}

export const disconnectReceived = () =>{
    return {
        type: actionTypes.DISCONNECT
    }
}

export const connecting = () =>{
    return {
        type: actionTypes.CONNECTING
    }
}

export const commandLogReceived = (data) =>{
    return {
        type: actionTypes.COMMAND_LOG_RECEIVED,
        log: data
    }
}

Now again in /src/client/actions/actions.js we will makes changes to loginUser and registerNewUser

export const loginUser = (socket, username, password) => {
  const data = { username, password };

  return (dispatch) => {
    dispatch(updateAuthenticationResponseMessage('Attempting login..'))

    return fetch('http://localhost:3000/user/login', {
      method: 'POST',
       headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      body: JSON.stringify(data),
      mode: 'cors'})
    .then( (response) => {
      if (!response.ok) {
        dispatch(updateAuthenticationResponseMessage('Incorrect username/password'));
        throw Error(response.statusText);
      }
      return response.json();
    })
    .then( (data) => {
      localStorage.setItem('username', data.username);
      localStorage.setItem('userid', data.userid);
      localStorage.setItem('token', data.tokenID);

      dispatch(updateAuthenticationResponseMessage('Ready to launch.'))
      dispatch(userLoggedIn(data.username, data.userid));
      dispatch(getSystemStatus());      
    })
    .catch( (e) => {
      dispatch(updateAuthenticationResponseMessage('System offline'));
      console.log(e)
    });
  }
}

export const registerNewUser = (socket, username, password) => {
  const data = { username, password };
  return dispatch => {
    dispatch(updateAuthenticationResponseMessage('Attempting registration..'))

    return fetch('http://localhost:3000/user/', {
      method: 'POST',
       headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      body: JSON.stringify(data),
      mode: 'cors'})
      .then( (response) => {
        if (!response.ok) {
          if(response.status==400){
            return response.json();
          }
          throw Error(response.statusText);
        }
        return response.json();
      })
      .then( (data) => {
        if(data.success){
          localStorage.setItem('username', data.username);
          localStorage.setItem('userid', data.userid);
          localStorage.setItem('token', data.tokenID);

          dispatch(updateAuthenticationResponseMessage('Registered successfully and logged in'))
          dispatch(userLoggedIn(data.username, data.userid));
          dispatch(getSystemStatus());         
        } else {
          dispatch(updateAuthenticationResponseMessage(data.error))
        }
      })
      .catch( (e) => {
        dispatch(updateAuthenticationResponseMessage('System offline'));
      });
  }
}

These changes above are that we imported our common constants, and then added some new actions. You will see many of these are for the messages we created handlers for on our server such as INIT_HANDSHAKE and LAUNCH_REQUEST. We also added the getSystemStatus API call so our client can get the state of the gameServer. commandLogReceived was also added - this is going to be part of a client side log of messages.

You will also notice we made a slight change to loginUser and registerNewUser, it is the same except that on a successful login/register we also invoke ‘dispatch(getSystemStatus());’. This gets the latest game server status once they are logged in/registered.

After that we need to make some modifications to the reducer in /src/client/reducers/worldReducer.js - which can be changed to the following:

import actionTypes from '../constants/actionTypes';
import constants from '../../common/constants.js';

let initialState = {
  authenticationResponse: localStorage.getItem('token') ? 'Ready to launch' : 'Awaiting login/registration...',
  loggedInStatus: localStorage.getItem('token') ? true : false,
  userid: localStorage.getItem('userid') ? localStorage.getItem('userid') : '',
  username: localStorage.getItem('username') ? localStorage.getItem('username') : '',
  connectionStatus: constants.DISCONNECTED,
  systemStatus: {
    startupTime: null,
    status: 0,
  },
  commandLog: ''
};

export default (state = initialState, action) => {
  let updated = Object.assign({}, state)
  switch (action.type) {
    case actionTypes.UPDATE_AUTHENTICATIONMESSAGE:
      updated['authenticationResponse'] = action.msg;
      return updated;
    case actionTypes.USER_LOGGED_IN:
      updated['username'] = action.username;
      updated['userid'] = action.userid;
      updated['loggedInStatus'] = true;
      return updated;
    case actionTypes.USER_LOGGED_OUT:
      updated['username'] = '';
      updated['userid'] = '';
      updated['loggedInStatus'] = false;
      return updated;
    case actionTypes.CONNECTING:
      updated['connectionStatus'] = constants.CONNECTING;
      return updated;
    case actionTypes.USER_READY:
      return updated;
    case actionTypes.DISCONNECT:
      updated['connectionStatus'] = constants.DISCONNECTED;

      return updated;
    case actionTypes.INIT_HANDSHAKE_RETURNED:
      updated['connectionStatus'] = constants.CONNECTED;
      updated['client_timestamp'] = action.cts;
      updated['server_timestamp'] = action.sts;
      updated['lagEstimate'] = action.lag;

      return updated
    case actionTypes.COMMAND_LOG_RECEIVED:
      updated['commandLog'] = updated['commandLog'] + action.log + '\n';

      return updated;
    case actionTypes.SYSTEM_STATUS:
      updated['systemStatus'] = action.status;

      return updated;
    default:
      return state;
  }
};

All of that is quite simple. We added some elements to our state, and handled their updating, such as our commandLog text string, the systemStatus, the timestamps from the INIT_HANDSHAKE and also the setting of connectionStatus.

Next we will install the socket.io-client which will allow us to open a socket connection to the server.

npm install socket.io-client --save

The next part is going to be one of the key parts of the client. The client side game world UI. For now this is going to contain just some basic code for handling the connection but will be expanded on later.

In /src/client/UI/ create the file gameWorld.js with the following

import React, { Component } from 'react';
import { connect } from 'react-redux';
import SocketClient from 'socket.io-client';
import constants from '../../common/constants.js';

import {
  disconnectReceived,
  updateAuthenticationResponseMessage,
  initHandshake,
  userReadyReceived,
  commandLogReceived
} from '../actions/actions.js';

class GameWorld extends Component {

  constructor () {
    super();
  }

  initSocket(){

    this.socket.on('connect', () => {
      this.props.dispatch(commandLogReceived('Connecting...'));
      this.socket.emit('authenticate', {token: localStorage.getItem('token')});

      this.socket.on('authenticated', () => {
          console.log(this.socket.id);
          this.props.dispatch(commandLogReceived('Successfully connected and authenticated.'));
          this.start();
        });
      this.socket.on('unauthorized', (msg) => {
          console.log("unauthorized: " + JSON.stringify(msg.data));
          this.props.dispatch(updateAuthenticationResponseMessage('Unauthorized'));
      })
    });
    this.socket.on('connect_error', () => { console.log('Error connecting to server');});
    this.socket.on('connect_timeout', () => { console.log('Timedout while attempting to connect..'); });

    this.socket.on('disconnect', () => {
      if (this.socket) {
        this.socket.destroy();
        delete this.socket;
      }
      this.props.dispatch(updateAuthenticationResponseMessage('Disconnected'));   
      this.props.dispatch(disconnectReceived());

    });

    this.socket.on(constants.CONNECT_READY, (data) => {
      console.log('Connect ready. Invoking handshake.');
      this.props.dispatch(initHandshake(this.socket));
    });

    this.socket.on(constants.USER_READY, (data) => {
      console.log('User ready recieved.');
      this.props.dispatch(userReadyReceived());
    });

    this.socket.on('command_log', (data) => {
      this.props.dispatch(commandLogReceived(data));
    });

  }

  componentDidMount () {
    this.socket = this.props.socket;

    this.initSocket();

    this.socket.connect();
  }

  start() {

  }

  render () {
    return (
        <div><h1>Game World</h1></div>
    );
  }
}

const mapStateToProps = state => {

    return {
        world: state.world
    }
}

export default connect(mapStateToProps)(GameWorld)

The initSocket method you can see above is the client side version of the socket connection method we earlier went through on cmdShipServer. It emits an AUTHENTICATE message to the server, passing in the JWT. It then handles the CONNECT_READY message that comes in from the server, which fires an initHandshake action. Finally the LAUNCH_REQUEST will be fired via the handshake response handler in actions.js.

To begin to integrate all this together we will make some alterations to /src/client/UI/windowSystem.js which can be changed to the following:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import SocketClient from 'socket.io-client';

import GameWorld from './gameWorld';
import Authentication from './authentication';
import constants from '../../common/constants.js';

class WindowSystem extends Component{
  constructor(props){
    super(props);

    this.socket = SocketClient('http://localhost:3000', { reconnection: false, autoConnect: false });
  }

  render(){
    const styles = {
      top: 0,
      left: 0,
      width: window.innerWidth,
      height: window.innerHeight,
      position: 'absolute',
      color: '#000000',
    }

    const inGame = (this.props.world.loggedInStatus && this.props.world.connectionStatus !== constants.DISCONNECTED);
    const gameScreen = (<div style={{zIndex:1}}><GameWorld socket={this.socket} userid={this.props.world.userid}/></div>);
    const screen = (inGame) ? gameScreen : <div style={{zIndex: 2}}><Authentication socket={this.props.socket} /></div>;

    return (
      <div style={styles} id="wr">
        {screen}
      </div>
    );
  }
}

const mapStateToProps = state => {
    return {
        world: state.world
    }
}

export default connect(mapStateToProps)(WindowSystem);

So this creates our socket, and then passes it into our components as a prop. In our render method we have some code which will either show the Authentication component, OR the gameWorld component we just created. Which is shown is based on whether the user is logged in, and also the connectionStatus. We are now going to alter authentication.js so that when the user clicks launch, it fires an action which alters connectionStatus to ‘connecting’, and thus the screen should change to our gameWorld - which will fire off the socket connection code in componentDidMounth within gameWorld that we just wrote. (Note that the socket i create in WindowSystem has autoConnect set to false - this is because we want the connection to open when screen switches to the gameWorld component)

We’ll now make alterations to /src/client/UI/authentication.js as follows:

...
import {
  registerNewUser,
  loginUser,
  logoutUser,
  updateAuthenticationResponseMessage,
  launchUser,
  disconnectUser,
  getSystemStatus
} from '../actions/actions.js';
...
  componentDidMount(){
    this.props.dispatch(getSystemStatus());
  }

  handleLaunchButtonClick(e){
    this.props.dispatch(launchUser(this.props.socket));
  }

  handleDisconnectButtonClick(e){
    this.props.dispatch(disconnectUser(this.props.socket));
  }
...
      const loggedInPanel = (
      <div>
        <ul>
          <li>Status: {this.props.world.systemStatus.status}</li>
          <li>Online since: { (this.props.world.systemStatus.startupTime !== null) ? this.props.world.systemStatus.startupTime : 'Offline'}</li>
        </ul>
        <Button style={navButtonStyle} onClick={this.handleLaunchButtonClick.bind(this)}>Launch</Button>
        <Button style={navButtonStyle} onClick={this.handleLogoutButtonClick.bind(this)}>Log out</Button>

      </div>);

      const disconnectButton = (<Button style={navButtonStyle} onClick={this.handleDisconnectButtonClick.bind(this)}>Disconnect</Button>);

  return(
    <Container>

      <Row>
      <Col xs="3" sm="3" lg="3" xl="3" md="3">

      </Col>

      <Col style={boxStyle} xs="4" sm="4" lg="4" xl="4" md="4">
      <h1>
      Launchpad
      </h1>
      <p>Status: {this.props.world.authenticationResponse}</p>
        {(!this.props.world.loggedInStatus) ? notLoggedInPanel : loggedInPanel}
      </Col>

      <Col style={boxStyle} xs="2" sm="2" lg="2" xl="2" md="2">
        <div>
         {this.props.world.connectionStatus}
         {(this.props.world.connectionStatus === 'connected') ? disconnectButton : <div></div>}
        </div>
      </Col>

      <Col xs="3" sm="3" lg="3" xl="3" md="3">

      </Col>
      </Row>
    </Container>
  );
...

You’ll see we have updated the handlers for launch and disconnect so that they now call the new actions. We also added in the system status information, with the component invoking the API call via getSystemStatus() in componentDidMount.

If we now build and run all this…

If we now build and run this…

npm run-script build
...
npm start

socketReady

You’ll see the server is starting up, with main.js successfully connecting to mongoDB and then initialising our gameServer and one zone.

If you then log in..

loggedInWithStatus

You’ll see our launchpad now has the gameServer status and time.

If we then hit launch…

launched

You can see from the logging that our client successfully connected and authenticated using the JWT, did the handshake process, had it’s user record loaded in from the db using userManager and then added to the zone.

If we now hit refresh on our browser, we can now see what happens on disconnection…

disconnect

As you can see the client loaded back in at the launchpad, the socket handled the disconnect with the userManager then unloading the user from the Zone and also from its collection of global users.

To recap so far…

We now have a gameServer which is started up in main.js, it has a UserManager which manages and holds a collection of all users on the server. The game server also has a zoneManager which holds a collection of Zones (each representing an area in the game universe - currently we have just 1).

A user logs in via REST API - recieves a JWT. They then connect to our game server using that JWT and when a user successfully connects they are added to the UserManagers collection of online users and also placed into a zone.

We are now going to add in some stuff…

First we need to send some game config to the client after it has connected. In /src/client/actions/action.js add the following two functions:

...
export const getSystemConfig = () => {
  var token = localStorage.getItem('token') || null;

  return (dispatch) => {
    return fetch('http://localhost:3000/game/config', {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization' : `Bearer ${token}`
      },
      mode: 'cors'})
    .then( (response) => {
      if (!response.ok) {
          throw Error(response.statusText);
      }
      return response.json();
    })
    .then( (data) => {
      dispatch(gameConfigReceived(JSON.parse(new Buffer(data.data, 'base64').toString('ascii'))));
    })
    .catch( (e) => console.log(e) );
  }
}

export const gameConfigReceived = (data) =>{
    return {
        type: actionTypes.CONFIG_RECEIVED,
        config: data
    }
}
...

This is the method which will call our API to get the config data, and also the action for receiving it.

We’ll now update /src/client/reducers/worldReducer.js so that it stores this config data:

let initialState = {
...
  configData: {}
}
...

    case actionTypes.CONFIG_RECEIVED:
        updated['configData'] = action.config;

        return updated;
...

This adds configData to our initial state, and also updates it on recieving the action. Next we need to add that action to the list in /src/client/constants/actionTypes.js

...
  CONFIG_RECEIVED: 'config_received',
...

Now we need to integrate this into the client application. Ideally this should take place before the user can click launch so that we have the game data ready. What we will do is at it in three places. The first when our Authentication box loads incase the user is already logged in, and then again when the login action returns successfully, or register is successful.

/src/client/actions/actions.js

...
export const loginUser = (socket, username, password) => {
  ...
  return (dispatch) => {
    dispatch(updateAuthenticationResponseMessage('Attempting login..'))

    return fetch('http://localhost:3000/user/login', {
      ...
    })
    .then( (data) => {
      ....
      dispatch(getSystemConfig());          
    })
    ...
  }
}
...
export const registerNewUser = (socket, username, password) => {
  const data = { username, password };
  return dispatch => {
    dispatch(updateAuthenticationResponseMessage('Attempting registration..'))

    return fetch('http://localhost:3000/user/', {
      method: 'POST',
       headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      body: JSON.stringify(data),
      mode: 'cors'})
      .then( (response) => {
        if (!response.ok) {
          if(response.status==400){
            return response.json();
          }
          throw Error(response.statusText);
        }
        return response.json();
      })
      .then( (data) => {
        if(data.success){
          localStorage.setItem('username', data.username);
          localStorage.setItem('userid', data.userid);
          localStorage.setItem('token', data.tokenID);

          dispatch(updateAuthenticationResponseMessage('Registered successfully and logged in'))
          dispatch(userLoggedIn(data.username, data.userid));
          dispatch(getSystemStatus());         
          dispatch(getSystemConfig());          
        } else {
          dispatch(updateAuthenticationResponseMessage(data.error))
        }
      })
      .catch( (e) => {
        dispatch(updateAuthenticationResponseMessage('System offline'));
      });
  }
}

The line added is ‘dispatch(getSystemConfig());’, at the end of the promise response once the login/register fetch has returned.

THe next change is in /src/client/UI/authentication.js - alterting componentDidMount() to be the following:

import {
  ...
  getSystemConfig
} from '../actions/actions.js';
...
  componentDidMount(){
    this.props.dispatch(getSystemStatus());
    this.props.dispatch(getSystemConfig());
  }
...

The change here is that we added a dispatch for getSystemConfig at the end of componentDidMount - this will fail if they are not logged in and should only dispatch if they are logged in but for now it will do.

That is the client side of this done, we now need to implement the server side.

We’ll begin by creating our config file in /src/common/game.json, with just some basic things.

{
"settings": {
},
"stars":[
],
"itemcategories": {
},
"itemtypes" : [
],
"modules" : [
],
"zones": []
}

Next in /src/server/controllers/ create gameController.js

const logger = require('../logger');
const gameConfig = require('../../common/game.json');

module.exports = {
  getConfig: (callback) => {
    callback(null, {
      itemtypes: gameConfig.itemtypes,
      itemcategories: gameConfig.itemcategories
    });
  },
};

You will see it imports in the gameConfig, and then returns back the two parts of it in the function. The reason for this is that this gameConfig will later contain other things that we may not want the client receiving.

Next we’ll make a new route. In /src/server/routes/ create game.js with the following:

const express = require('express');
const router = express.Router();
const gameController = require('../controllers/gameController');
const Buffer = require('Buffer');

router.get('/config', (req, res, next) => {
  gameController.getConfig((err, data) => {
    if (err) {
      console.log(err);
      res.status(500).json({
        success: 0,
        error: err,
      });
      return;
    }

    res.status(200).json({
      data: new Buffer(JSON.stringify(data)).toString('base64')
    });
  });
});

module.exports = router;

This one is another straightforward one, it calls the gameController, gets the config in and returns it back. You will see we used Buffer, you’ll need to run the following code to install that

npm install Buffer --save

To make it work fully we then need to alter /src/server/main.js to import the gameRoutes and then use them as follows:

...
import gameRoutes from './routes/game';
...
server.use('/game', authCheckMiddleware);
server.use('/game', gameRoutes);
...

We are now going to create a config manager which will access and use that data on the server.

In /src/server/managers/ create configurationManager.js as follows:

import logger from '../logger';

class ConfigurationManager {

  init(config) {
    this.gameConfig = config;
  }

  getConfig() {
    return this.gameConfig;
  }

  getSettings() {
    return this.gameConfig.settings;
  }

  getSetting(settingName) {
    return this.gameConfig.settings[settingName];
  }

  getZones() {
    return this.gameConfig.zones;
  }

  getItemData(itemID) {
    for (let i = 0, len = this.gameConfig.itemtypes.length; i < len; i++) {
      if (this.gameConfig.itemtypes[i].id.toString() === itemID.toString()) {
        return this.gameConfig.itemtypes[i];
      }
    }

    return null;
  }

  getCategoryTypeBaseCategory(itemcatid) {
    if (this.gameConfig.itemcategories[itemcatid].base == 1) {
      return itemcatid;
    }

    // Get this category
    let parentID = this.gameConfig.itemcategories[itemcatid].parentid;

    if (parentID === 0) {
      return -1;
    }

    return this.getCategoryTypeBaseCategory(parentID);
  }

  itemTypeIsInOrUnderCategory(itemcatid, catid) {

    if (itemcatid.toString() === catid.toString()) {
      return true;
    }

    // Get this category
    const parentID = this.gameConfig.itemcategories[itemcatid].parentid;

    if (parentID === 0) {
      return false;
    }

    return this.itemTypeIsInOrUnderCategory(parentID, catid);
  }
}

module.exports = new ConfigurationManager();

All this does is return elements in the game config although currently we don’t have much data in it so don’t worry too much about that for now.

Next we will initialise this with our game config in our game server. In /src/server/cmdShipServer.js make the following changes:

...
import ConfigurationManager from './managers/configurationManager';
import gameConfigSettings from '../common/game.json';
...
class CmdShipServer {

  constructor(io) {
  ...  
    ConfigurationManager.init(gameConfigSettings);
  ...
  }
  ...

In the above there are three changes. We imported the ConfigurationManager and the gameConfig json file, and then within the constructor i ran init on the configurationManager passing in the json game data.

We now have a lot of the server framework in place. main.js is finished.

We are going to next expand our clientside gameWorld. In /src/client/UI/gameWorld.js

...
let cam = { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
let context = null;
let requestID;

class GameWorld extends Component {

  constructor () {
    super();

    context = null;
    this.canvasref = React.createRef();

  }
...
  componentDidMount () {
    this.socket = this.props.socket;

    this.initSocket();

    context = this.canvasref.current.getContext('2d');

    document.addEventListener('keydown', this.keyDownCheck.bind(this));
    document.addEventListener('keyup', this.keyUpCheck.bind(this));
    document.addEventListener('click',  this.clickCheck.bind(this));

    this.socket.connect();
  }

  clickCheck(e) {
  }

  keyDownCheck(e){
  }

  keyUpCheck(e){
  }

  start() {
    let renderLoop = (timestamp) => {
        this.lastTimestamp = this.lastTimestamp || timestamp;
        this.update(timestamp, timestamp - this.lastTimestamp);
        this.lastTimestamp = timestamp;
        window.requestAnimationFrame(renderLoop);
    };

    window.requestAnimationFrame(renderLoop);
  }

  update (ts, dt) {
    this.clear(cam);
    this.draw(cam);
  }

  draw(observationPoint) {
  }

  clear (observationPoint) {
    context.fillStyle = '#000000';
    context.fillRect(0, 0, observationPoint.width, observationPoint.height);
  }


  render () {
    return (
        <div><canvas style={{zIndex:-1}} width={cam.width} height={cam.height} ref={this.canvasref} /></div>
    );
  }
...

The changes above are that we added a “cam” structure. This is going to store where we are within the current game world, acting like a camera. We created some variables in the constructor that we’ll use later and in componentDidMount we get the context of the canvas we have added into the render return method and also we created some event listeners for our clients update (though currently the handlers are empty). We also added our client side game loop to the start method using “window.requestAnimationFrame”. This loops calling update which for now just clears the canvas and runs draw (which currently draws nothing). Later on this draw method is going to draw all the objects the client currently knows about and all the effects going on.

We will come back to this in the next part which is the construction of our game world world.

It should all build and run at this point but for now you won’t see anything on the gameWorld

Link to source files for Part 3

Part 4 - Game objects and Object Manager

In this part we are going to:

There will be 3 versions of an object. The persisted copy in the db, the object in memory on the server when it is within the game world, and also the representation of that object on the client.

We will start by creating the db model schema of our world objects.

In /src/server/model/ create the folder /objects and then within that create the file worldObject.js with the following:

import mongoose from 'mongoose';

const baseOptions = {
  discriminatorKey: 'itemtype',
  collection: 'items'
};

const WorldObjectSchema = new mongoose.Schema({
  dockedBaseID: { type: String, default: '' },
  dockedUserGarageID: { type: String, default: '' },
  positionState: { type: Number, required: true },
  positionStateParams: { type: Object, required: true },
  objectState: {},
  lastX: Number, // Last known X
  lastY: Number, // Last known Y
  angle: Number,
  typeID: Number,
  zoneID: Number,
  name: String,
  created: {
    type: Date,
    required: true,
    default: new Date()
  },
  owner: { type: String, default: '' },
  status: { type: Number, default: 1 }
}, baseOptions);

module.exports = mongoose.model('WorldObject', WorldObjectSchema);

For now don’t worry about what everything in there is for. This is the objects persistant state. Info such as it’s last X,Y position, it’s angle, the zone it was in, it’s type, whether it is docked in a base or not etc

The baseOptions structure that contains the discriminatorKey ‘itemType’ allows us to create ‘sub schemas’ from this.

In /src/server/model/objects/ we will next add our first object by creating ship.js:

import mongoose  from 'mongoose';
import WorldObject from './worldObject';

const Ship = WorldObject.discriminator('Ship', new mongoose.Schema({
  dockedUserID: { type: String, default: '' },
}));

module.exports = mongoose.model('Ship');

Here we have created a Ship schema, which is based on our WorldObject schema, but with one extra property dockedUserID which will contain the userID of the player currently occupying that Spaceship.

With the DB aspect of a world object in place, now we will add in some code which loads the object from the db, into an object within the game server. An example of this happening is when a user connects.

In this game the user will always be in a spaceship of some kind. The idea is that the player is piloting a small capsule (similar to pods in Eve), these can then dock/board with spacecraft and the user assumes control of that spacecraft. The capsule we will call a PTU (Personal Transport Unit). This will be the smallest type of vehicle (Although it will be of type Ship, when speaking of Spaceships we mean all things that the PTU can dock with). When the user boards or docks with a spacecraft, the PTU will load into the spacecraft. From a code perspective, what will happen is that upon the user chosing to ‘dock/board’ a vehicle (If they have permission or capable of doing so), the PTU Ship object they currently control will be unloaded from the Zone, its location changed to be inside the new vehicle and persisted to the DB, the user will then have their ‘current vehicle id’ changed to that of the new vehicle and from the perspective of the player it will look like their PTU docked with the new vehicle.

For now don’t worry about all this too much because we are going to have only a single type of ‘Ship’ to begin with - that being the PTU, the small capsule the players control.

We’ll now do the sections of code which define the player objects and then load the players spacecraft from the DB into game world as those objects.

In /src/server/ create a new folder ‘gameObjects’ , and within that create the file spaceObject.js

import logger from '../logger';
import constants from '../../common/constants';

class SpaceObject {

  constructor(zone, objectState, objectSpecification) {
    this.objectID = objectState._id.toString();
    this.specification = objectSpecification;

    this.setZone(zone);

    this.title = objectState.name;
    this.width = objectSpecification.width;
    this.height = objectSpecification.height;

    this.status = 1;

    this.core = {
      objectID: this.objectID,
      typeID: parseInt(objectState.typeID), 
      x: objectState.lastX,
      y: objectState.lastY,
      angle: (objectState.angle !== null && objectState.angle !== undefined) ? objectState.angle : 0,
      lastTimestamp: new Date().getTime()
    }
  }

  doStep(dt) {
  }

  setZone(z) {
    this.zone = z;
  }

  update(ts, dt) {

  }

  hasActiveOccupant() {
    return false;
  }

  setPosition(sX, sY) {
  }

}

module.exports = SpaceObject;

Again, some of this isn’t created yet such as the zone oms (Object Management System) so don’t worry about what it is. The key thing to know is that the above is the root class from which all objects in our game world extend from.

The important attributes within an object are:

These properties will go in a core structure along with other things such as the position and angle.

Now we will create a intermediate class BoardableEntity which will extend from this SpaceObject class. In /src/server/gameObjects create boardableEntity.js as follows:

import SpaceObject from './spaceObject';
import constants  from '../../common/constants';

class BoardableEntity extends SpaceObject {
  constructor (zone, sm, spec) {
    super(zone, sm, spec);
    
    this.dockedBaseID = '';
    this.dockedUserGarageID = '';

    this.dockedUserID = null;
    this.dockedName = '';
  }

  hasActiveOccupant(){ 
    return (this.dockedUserID !== null);
  }

  getOccupantID(){
    return this.dockedUserID;
  }

  setDockedLocation(dbID, dugID) {
    this.dockedBaseID = dbID;
    this.dockedUserGarageID = dugID;
  }

  setOccupancyData(dockedUserID, dockedName){
    this.dockedUserID = dockedUserID;
    this.dockedName = dockedName;
  }

  setDockedUser(user) {
    this.setOccupancyData(user.userid, user.username);
    return true;
  }
}

module.exports = BoardableEntity;

This class is going to be between spaceObject and ship and extends spaceObject and some methods which allow a user to ‘dock/board’ a vehicle and control it.

Now we will create a ship which extends from this boardableEntity class. In /src/server/gameObjects create ship.js

import logger from '../logger';
import ShipModel from '../model/objects/ship';
import ConfigurationManager from '../managers/configurationManager';
import BoardableEntity from './boardableEntity';
import constants from '../../common/constants';

class Ship extends BoardableEntity {
  constructor (zone, sm, shipSpec) {
    super(zone, sm, shipSpec);

    this.core = Object.assign({
      angularVelocity: 0,
      speedX: 0,
      speedY: 0,
      keyH: 0, // -1, 0, 1
      keyV: 0,
      keyS: 0,
      inputQueue: []
    }, this.core);    
  }

  init(){
    return new Promise((resolve, reject) => {
      resolve();
    });
  }

  unload() {
  }
}

module.exports = Ship;

For now our ship does not have much in it, just the basic core values with some keyinput data and movement data (both of which we will use later)

We will now add the PTU to our game config file with basic properties such as its title and type. We’ll also add some item categories:

..
"itemcategories": {
 "2" : { "name": "Ships", "parentid": 0, "base": 1},
 "3" : { "name": "PTU", "parentid": 2, "hidden":1 }
},
"itemtypes" : [
  {
   "title": "PTU", "type": 3, "id": 6,
     "name" : "PTU",
     "width" : 2,
     "height" : 4
  } 
],
...

When a user connects, their spacecraft is loaded from the persisted copy in the db (/model/objects/ship.js). An object is then created on the server which represents this within the game world (/gameObjects/ship.js). There will also be other world objects such as Asteroids and spacestations which are loaded up when the Zone is loaded. All of these objects will be stored in the zone in a collection.

We are now going to create a system so that a Zone can manage these objects in memory.

In /src/server/managers/ create objectManager.js

import logger from '../logger';
import constants from '../../common/constants';
import ConfigurationManager from './configurationManager';
import Ship from '../gameObjects/ship';
import ShipModel from '../model/objects/ship';

class ObjectManager {
  constructor(zone) {
    this.worldObjects = [];
    this.zone = zone;
    this.init();
  }

  init() {

  }

  static createShip(typeID, spec) {
    return new Promise((resolve, reject) => {
      spec.typeID = typeID;
      var newShip = new ShipModel(spec);

      newShip.save().then( (res) => {
        logger.info(`Ship created: ${typeID}`);
        resolve(res);
      }).catch( (e) => {
        logger.error('Error creating ship');
        logger.error(e);
        reject();
      });
    });
  }

  static loadShipFromID(shipID){
    return new Promise((resolve, reject) => {
      ShipModel.findOne({ _id: shipID }, (err, shipRecord) => {
        if (err) {
          console.log(err);
          return;
        }
        
        return resolve(shipRecord);
      });
    });
  }

  instantiateShipFromRecord(shipRecord) {
    return new Promise((resolve, reject) => {
      if(shipRecord == null) {
        logger.error(`A ship instantiation was attempted with a null record`);
        return reject();
      }

      let shipTypeSpecification = ConfigurationManager.getItemData(shipRecord.typeID);
      let sh = new Ship(this.zone, shipRecord, shipTypeSpecification);
      sh.init().then((res) => {
        return resolve( sh );
      }).catch((e) => {
        console.log(e);
      });
    });
  }

  addObjectToWorld(object, params) {
    logger.info(`Adding object to world: ${object.objectID}`);
    this.worldObjects.push(object);
  }

  removeObjectFromWorld(objectToRemove, params) {
    logger.info(`Removing object from world:: ${objectToRemove.objectID}`);

    for(var i = this.worldObjects.length - 1; i >= 0; i--) {
      if(this.worldObjects[i].objectID === objectToRemove.objectID) {
        logger.info('Found and removed'); 
        this.worldObjects.splice(i, 1);
      }
    }
  }

  getWorldObjects() {
    return this.worldObjects;
  }

  getWorldObject(objectID) {
    for (let i = 0, len = this.worldObjects.length; i < len; i++) {
      if (this.worldObjects[i].objectID === objectID) {
        return this.worldObjects[i];
      }
    }

    return null;
  }
}

module.exports = ObjectManager;

This will be expanded on later but for now it is mostly about methods for managing the worldObjects array which is a list of the space objects in the Zone. Methods for getting a SpaceObject in the collection, adding and removing. There are also 3 other new methods in there that we will use shortly:

We will now have /src/server/zone.js instantiate a new ObjectManager in its constructor

...
import ObjectManager from './managers/objectManager';
...

  constructor(zm, um, zoneID, zoneName) {
...
    this.oms = new ObjectManager(this);
  }
...  

The two changes were, we imported the objectManager, and then in the constructor instantiated it as the instance variable oms, and passed in the zone.

We need to take a detour here in order to give each user an initial spaceship. First we will extend the user schema to include id’s for their current spaceship and also their ‘PTU’

...
const UserSchema = new mongoose.Schema({
  ...
  dockedVehicle: String,
  ptuVehicleID: String,
  zoneid: { type: Number, default: 1 },
  ...
});
...

The changes above are that we added three new properties to the user model. dockedVehicle - which is the id of a spaceObject, and ptuVehicleID - which is the users PTU ‘pod’ id. When the user is in not in a vehicle and is flying around just as their PTU, the dockedVehicle attribute will be the same as their PTU id, and also zoneid - which we will use later on.

When a user joins the server and a user object is created in UserManager, we need to populate their user object with this data from the db record. In /src/server/user.js add the following changes to the constructor:

...
class User {
  constructor(socket, userDocument, userid, username) {
    ...
    this.ptuVehicleID = userDocument.ptuVehicleID;
    this.vehicleid = userDocument.dockedVehicle;
    this.zoneid = userDocument.zoneid;
  }
...  

With the above in place, the ptuVehicleID, vehicleID, zoneid variables will be set from the userDocument object when the user object is created.

Next, When a user registers we will create a new spaceship in the DB for them. Change /src/server/managers/userManager.js as follows:

...
import ObjectManager from './objectManager';
...
  newUserSetup(userid) { 
    return new Promise((resolve, reject) => {
      ObjectManager.createShip(6, {
        lastX: Math.floor(Math.random() * 500),
        lastY: Math.floor(Math.random() * 500),
        zoneID: 1,
        typeID: 6,
        dockedUserID: userid,
        positionState: 8,
        positionStateParams: { zoneid: 1 },
      }).then( (ship) => {
        logger.info('PTU created for new user');

        UserModel.findOne({ _id: userid }, (err, user) => {
          if (err) {
            logger.error('Error attempting to find(UserModel) in Temp setupuser');
            reject(new Error('System error'));
            return;
          }

          logger.info('user record found. preparing to update');

          user.dockedVehicle = ship._id;
          user.ptuVehicleID = ship._id;
          user.status = 1;

          user.save(function(err, user) {
            if(err){
              console.log('error updating user');
              console.log(err);
              return;
            }

            logger.info('userid updated: ${user._id}')

            return resolve(ship);

          });
        });
         
      });
    });
  }
  ...
  registerNewUser(username, password) {
    return new Promise((resolve, reject) => {
      ...
      newUser.save((err, user) => {
        if (err) {
           ... 
        }

        // Create new user ship
        this.newUserSetup(user._id).then((ns)=>{
          resolve(user);
        }); 
      });
    });
  }
  ...

The two changes in the above are we added a new method ‘newUserSetup’, which creates a new spaceship for the user. We then made a change to the end of the method registerNewUser, removing the existing resolve() and instead calling this new method to setup the users spaceship.

Next back within /src/server/zone.js we need to update the loadUserIntoZone and the unloadUserFromZone method:

...
  unloadUserFromZone(user) { 
    logger.info('Unloading user from zone... ');
    this.removeUserRegistration(user.userid);

    logger.info(`Unloading user vehicle: ${user.vehicleid}`);

    let userVehicle = this.oms.getWorldObject(user.vehicleid);
    if(userVehicle !== null){
      this.oms.removeObjectFromWorld(userVehicle, {});
    }
  }

  loadUserIntoZone(user) {
    if(this.userIsRegistered(user)){
      logger.error('Attempted to load a user already registered ', user.username);
      return;
    }

    this.registerUserToZone(user);
    
    ObjectManager.loadShipFromID(user.vehicleid).then((userShipRecord) => {
      this.oms.instantiateShipFromRecord(userShipRecord).then((shipObject) => {
        
        logger.info('A ship has been spawned and loaded...');

        // Assign user to vehicle
        shipObject.setDockedUser(user);

        logger.info('Adding object to world');
        this.oms.addObjectToWorld(shipObject); // Add to object collection, grids and send adds

      });
    });
  }
...  

The loadUserIntoZone method will add the user to the collection of users registered in the zone, pull the users spaceship record from the db, instantiate a Ship game object, set the user as being in it (docked with) and then it adds it to the OMS. unloadUserFromZone will do the reverse and remove the object from the OMS.

To test this, I will register a new account…

Listening on 3000
(node:14818) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(node:14818) DeprecationWarning: collection.ensureIndex is deprecated. Use createIndexes instead.
info: Connected to MongoDB server successfully. {"timestamp":"2018-12-18 08:32:00 AM +0000"}
info: Initializing game server... {"timestamp":"2018-12-18 08:32:00 AM +0000"}
info: Game Server init began... {"timestamp":"2018-12-18 08:32:00 AM +0000"}
info: Init complete. Starting game server process... {"timestamp":"2018-12-18 08:32:00 AM +0000"}
started...
info: Ship created: 6 {"timestamp":"2018-12-18 08:35:29 AM +0000"}
info: PTU created for new user {"timestamp":"2018-12-18 08:35:29 AM +0000"}
info: user record found. preparing to update {"timestamp":"2018-12-18 08:35:29 AM +0000"}
info: userid updated: ${user._id} {"timestamp":"2018-12-18 08:35:29 AM +0000"}
info: test4 Registered {"timestamp":"2018-12-18 08:35:29 AM +0000"}

I will then connect and see that my ship is loaded into the zone OMS

info: Client established connection and authenticated. L5ALqZPYI7tBUJLEAAAA test4. Connected clients: 1 {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: test4 invoked handshake. {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: test4 invoked launched. {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: Attempting to load user 5c18b1510642fa39e2df8570  {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: Game: User loaded 5c18b1510642fa39e2df8570 {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: A ship has been spawned and loaded... {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: Adding object to world {"timestamp":"2018-12-18 08:36:01 AM +0000"}
info: Adding object to world: 5c18b1510642fa39e2df8571 {"timestamp":"2018-12-18 08:36:01 AM +0000"}

And upon disconnect the reverse happens

info: test4 disconnected. {"timestamp":"2018-12-18 08:36:18 AM +0000"}
info: Game: User exiting 5c18b1510642fa39e2df8570 {"timestamp":"2018-12-18 08:36:18 AM +0000"}
info: Unloading user 5c18b1510642fa39e2df8570 from UserManager {"timestamp":"2018-12-18 08:36:18 AM +0000"}
userzoneid:  1
info: Unloading user from zone...  {"timestamp":"2018-12-18 08:36:18 AM +0000"}
info: Unloading user vehicle: 5c18b1510642fa39e2df8571 {"timestamp":"2018-12-18 08:36:18 AM +0000"}
info: Removing object from world:: 5c18b1510642fa39e2df8571 {"timestamp":"2018-12-18 08:36:18 AM +0000"}
info: Found and removed {"timestamp":"2018-12-18 08:36:18 AM +0000"}

We now need to make a change in order that both our user and and spaceobject has the ability to persist its attributes. Our object persistance is going to work by using a boolean flag on spaceObject. When we want the object to persist its data we set this flag to true. In our zone’s game loop at the end of each loop we will then get all the objects which have been flagged as needing updating, and then we will call a persist method which the sub classes will overload. We will start with altering /src/server/gameObjects/spaceObject.js

...
class SpaceObject {
  constructor(zone, objectID, typeID, objectSpecification, objectState) {
    ...
    // Whether the object has been flagged for its data to be persiste on the next opportunity.
    this.markedForPersistance = false;
    ...
  }
  ...
  setPersistanceMark(flag) {
    this.markedForPersistance = flag;
  }

  doPersistanceUpdateCheck() {
    if (this.markedForPersistance) {
      this.persistObjectData().then(() => { this.setPersistanceMark(false); });
    }
  }

  persistObjectData() {
    return new Promise((resolve, reject) => {
      resolve();
    });
  }
...

The changes here are that we have added the ‘markedForPersistance’ property to the constuctor, and then added two methods doPersistanceUpdateCheck (which will be called by the zone) and ‘persistObjectData’ which will be overridden by the sub classes to include their specific properties. We will now implement ‘persistObjectData’ in /src/server/gameObjects/ship.js

...
  persistObjectData() {
    return new Promise((resolve, reject) => {
      ShipModel.findOneAndUpdate({ _id: this.objectID },{
        lastX: this.core.x,
        lastY: this.core.y,
        angle: this.core.angle,
        zoneID: this.zone.zoneid,
        dockedUserID: (this.dockedUserID == null) ? '' : this.dockedUserID,
        dockedBaseID: this.dockedBaseID,
        dockedUserGarageID: this.dockedUserGarageID,
        status: this.status,
      } , {}, (err, doc) => {
        if (err){
          reject(err);
          return;
        }
        logger.info('Ship object persistance complete.');
        resolve();
      });
    });
  }
...

We will expand which properties are persisted later but for now it will just be basic position info, the zone and which user is docked in the vehicle, and the two attributes which determined if it stored somewhere.

We need to do another critical detour to make this work. This is going to be the update method of our space objects. When the game loop in each zone runs, it will iterate over all space objects and fire an update method on them.

/src/server/zone.js

...
  run() {
    let currentServerTime = new Date().getTime();

    let worldObjects = this.oms.getWorldObjects();
    for(var i = worldObjects.length - 1; i >= 0; i--) {
      worldObjects[i].update(currentServerTime, currentServerTime - this.lastTimestamp);
    }

    this.lastTimestamp = currentServerTime;
  }
...

/src/server/gameObjects/ship.js

...
  update(ts, dt){
    this.doPersistanceUpdateCheck();
  }
...

/src/server/cmdShipServer.js

...
const PERIOD = 1000 / 60;
...
  run() {
    let runStartTick = (new Date()).getTime();

    let zoneCollection = this.zoneManager.getZones();
    for (let z = 0; z < zoneCollection.length; z++) {
      zoneCollection[z].run();
    }

    setTimeout(this.run.bind(this), (runStartTick + PERIOD) - (new Date()).getTime());
  }

  start() {
    console.log('started...');
    setTimeout(this.run.bind(this));   
  }
...

So we now have our game server run method, which invokes the ‘run’ game loop on each zone.

We will now add in this “this.setPersistanceMark(true)” on the space object whenever we want it to persist. For now we will do this in two places, when setting an objects zone and also setting its occupant.

/src/server/gameObjects/boardableEntity.js

...
  setDockedLocation(dbID, dugID) {
    this.dockedBaseID = dbID;
    this.dockedUserGarageID = dugID;
    this.setPersistanceMark(true);
  }

  setOccupancyData(dockedUserID, dockedName){
    this.dockedUserID = dockedUserID;
    this.dockedName = dockedName;
    this.setPersistanceMark(true);
  }
...

/src/server/gameObjects/spaceObject.js

...
  setZone(z) {
    this.zone = z;
    this.setPersistanceMark(true);
  }
...

On zone change or occupant change, our vehicle will now persist that info to the DB.

Now we will implement user persistance:

/src/server/managers/userManager.js

...
  init(zm) {
    ...
    this.usersAwaitingPersistance = {};
  }

  getUsersAwaitingPersistanceAndClear() {
    let userKeys = Object.keys(this.usersAwaitingPersistance);
    this.usersAwaitingPersistance = {};
    return userKeys;
  }

  addUserToPersistanceQueue(userid) {
    this.usersAwaitingPersistance[userid] = true;
  }

  persistUser(userid){
    return new Promise((resolve, reject) => {

      let userToRemove = this.getUser(userid);
      if (!userToRemove) {
        logger.info('Attempted to persist a user that has already left');
        return resolve();
      }

      logger.info(`Attempting to persist user ${userid} `);

      UserModel.findOneAndUpdate({ _id: userid },{
          zoneid: userToRemove.zoneid,
          roleid: userToRemove.roleid,
          status: userToRemove.status,
          dockedVehicle: userToRemove.vehicleid
        } , {}, (err, doc) => {
          if (err){
            logger.info(err);
            reject(err);
            return;
          }

          logger.info('User persistance complete.')

          resolve();
      });
    });
  }
...

We added new collection ‘usersAwaitingPersistance’, this will store the users that need to be persisted across the server.

We’ll then alter our main server run loop in /src/server/cmdShipServer.js to make use of these methods:

...
  run() {
    let runStartTick = (new Date()).getTime();

    let zoneCollection = this.zoneManager.getZones();
    for (let z = 0; z < zoneCollection.length; z++) {
      zoneCollection[z].run();
    }

    let usersToSave = this.userManager.getUsersAwaitingPersistanceAndClear();
    for(var i = 0, len = usersToSave.length; i < len; i++){
      this.userManager.persistUser(usersToSave[i]);
    }

    setTimeout(this.run.bind(this), (runStartTick + PERIOD) - (new Date()).getTime());
  }
...

Our server loop now gets the users on the server awaiting update and persists them. Whenever we want to update our user, such as when they change zone, get in a new vehicle, dock in a structure - we will fire this method on the user ‘addUserToPersistanceQueue’, currently we won’t yet use it.

Now that we have user and object persistance, we will go back to the management of our objects in the Zone.

These zones are going to be big with it taking a long time to travel across. We don’t want users to aware of all objects in the Zone and only want them aware of things in a local area.. because of this we are going to break down a Zone into a grid. When an object moves across the Zone it will be registered to a grid and as it moves into the next grid box it will then change its registration. A benefit of this is that it will reduce overhead when the server wants to do things such as check collision detection, with it being able to not need to check against objects we are not nearby.

We’ll begin by adding some config for our grid sizes to /src/common/game.json

"settings": {
  "gridWidth": 1500,
  "gridHeight": 750
},
...

We’ll then add a new property to our spaceObject which keeps track of what grid the object is currently in:

/src/server/gameObjects/spaceObject.js

...
class SpaceObject {

  constructor(zone, objectState, objectSpecification) {
    ...
    this.registeredGrid = null;
  }
...

In the above we added the single line, ‘this.registeredGrid = null;’ to the constructor

We’ll now create a new manager in /src/server/managers/ called gridManager.js

import logger from '../logger';
import constants from '../../common/constants';

class GridManager {
  constructor(oms, width, height) {
    this.oms = oms;

    this.width = width;
    this.height = height;

    this.grids = {};
  }

  addObjectToZone(object, nX, nY, params) {
    logger.info(`Adding object to zone at: ${nX} ${nY}`);
    // Add the object to the grid
    if (object.registeredGrid !== null){
      return false;
    }

    // Find the grid and add it, and set the users registered grid
    let nObjects = this.getGridAtPosition(nX, nY).objects;
    // Check to see if object already in that zone - even though impossible
    if(nObjects.includes(object.objectID)){
      logger.error(`Corrupt call to addObjectToZone ${object.objectID}. Already in Grid.`)
      return;
    }

    nObjects.push(object.objectID);
    this.setGridObjectsAtPosition(nX, nY, nObjects);
    object.registeredGrid = this.getGridKeyOfPosition(nX, nY);
  }

  removeObjectFromZone(object, params) {
    // Remove the object from the grid
    this.removeObjectGridRegistration(object);
  }

  objectPositionChange(object, nX, nY) {

    if ((object.registeredGrid !== this.getGridKeyOfPosition(nX, nY))) {

      this.removeObjectGridRegistration(object); // Remove object from existing grid registration, set to null

      // Add to new grid, set registered grid
      let nObjects = this.getGridAtPosition(nX, nY).objects;
      nObjects.push(object.objectID);
      this.setGridObjectsAtPosition(nX, nY, nObjects);
      object.registeredGrid = this.getGridKeyOfPosition(nX, nY);

      object.core.x = nX;
      object.core.y = nY;

    } else {
      // No grid change but update position anyway
      object.core.x = nX;
      object.core.y = nY;
    }

    return true;
  }

  removeObjectGridRegistration(aObject) {
    if (aObject.registeredGrid !== null) {
      let gObjects = this.getGridKeyObjects(aObject.registeredGrid);

      gObjects = gObjects.filter((object) => {
        return aObject.objectID !== object
      });

      this.setGridKeyObjects(aObject.registeredGrid, gObjects);
      aObject.registeredGrid = null;
    }
  }

  setGridKeyObjects(gridKey, objects) {
    if (this.grids[gridKey] === undefined) {
      this.grids[gridKey] = { objects: [] };
    }

    this.grids[gridKey].objects = objects;

    this.checkForEmptyGridAndPurge(gridKey);
  }

  checkForEmptyGridAndPurge(gridKey) {
    if (this.grids[gridKey] !== undefined) {
      if (this.grids[gridKey].objects.length == 0 ) {
        delete this.grids[gridKey];
      }
    }
  }

  getGridKeyObjects(gKey) {
    if (this.grids[gKey] !== undefined) {
      return this.grids[gKey].objects;
    }

    return undefined;
  }

  setGridObjectsAtPosition(sX, sY, objects) {
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    let gridKey = this.getGridKey(cGrid.x, cGrid.y);

    this.setGridKeyObjects(gridKey, objects);
  }

  getGridAtPosition(sX, sY) {
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    let gridKey = this.getGridKey(cGrid.x, cGrid.y);

    if (this.grids[gridKey] !== undefined) {
      return this.grids[gridKey];
    }

    return { objects: [] };
  }

  getGridKeyOfPosition(sX, sY) {
    if (sX == null && sY == null) {
      return 'null_null'
    }
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    return this.getGridKey(cGrid.x, cGrid.y);
  }

  getGridKey(x, y) {
    return ''+x+'_'+y+'';
  }

  calculateGridSurrounds(x, y) {
    let cGrid = { x: Math.floor(x / this.width) * this.width, y: Math.floor(y / this.height) * this.height };

    let x1 = Math.floor(cGrid.x - this.width);
    let x2 = Math.floor(cGrid.x + this.width);
    let y1 = Math.floor(cGrid.y - this.height);
    let y2 = Math.floor(cGrid.y + this.height);

    return [
      { x: x1 , y: y1 },
      { x: cGrid.x , y: y1 },
      { x: x2 , y: y1 },
      { x: x1 , y: cGrid.y},
      { x: cGrid.x, y: cGrid.y},
      { x: x2, y: cGrid.y},
      { x: x1 , y: y2 },
      { x: cGrid.x , y: y2 },
      { x: x2, y: y2}
      ];
  }
}

module.exports = GridManager;

This gridManager for now only tracks objects but later will include those observing the world. Each cell in the grid has a width and height (this.width, this.height). Each element in the grid is stored using a ‘grid key’ - this is the coordinates of that cell within the grid. So an object at x:100, y:100, within the grid (based on our current settings our grid cell dimensions are 1500 by 750) would at grid with key “0_0”. An object at x: 1501, y: 100 would be in cell “1_0”. We then store objects within our grid collection using this key in a collection ‘objects’ - this.grids[gKey].objects’. Most of the methods are for managing objects adding and removing from this grid collection.

Some other important methods are:

Next we’ll integrate this into our OMS at /src/server/managers/objectManager.js

...
import GridManager from './gridManager';
...
class ObjectManager {
  constructor(zone) {
    ...
    this.gm = new GridManager(
      this,
      ConfigurationManager.getSetting('gridWidth'),
      ConfigurationManager.getSetting('gridHeight')
    );
    ...
  }
...
  addObjectToWorld(object, params) {
    logger.info(`Adding object to world: ${object.objectID}`);
    this.worldObjects.push(object);
    this.gm.addObjectToZone(object, object.core.x, object.core.y, params)
  }

  removeObjectFromWorld(objectToRemove, params) {
    logger.info(`Removing object from world:: ${objectToRemove.objectID}`);

    this.gm.removeObjectFromZone(objectToRemove, params);

    ...
  }   

  updateObjectPosition(object, sX, sY) {
    return this.gm.objectPositionChange(object, parseFloat(sX), parseFloat(sY));
  }

...

The five changes above are that we imported the new gridManager, then in the constructor we instantiated the gridManager, and then we integrated the addObjectToZone, removeObjectFromZone methods into the add/remove methods of the OMS. With this in place our object will now be added to a grid when it is added to the oms. We also have a method for calling our objectPositionChange within the gridManager.

We will make another change here in /src/server/gameObjects/spaceObject.js, placing in our OMS updateObjectPosition method.

...
  setPosition(sX, sY) {
    return this.zone.oms.updateObjectPosition(this, sX, sY);
  }
...

We will continue on now onto how players observe the world.

Link to source files for Part 4

Part 5 - Players observing objects in the world

With the objects in the Zone, assigned to a grid, we need some way of the player seeing into the Zone at a particular position (like a camera point) and being updated of the state of objects around that point.

We will do this by extending our grid system so that aswell as objects registering themselves with a grid, a ‘watcher’ will also register with a grid. A watcher will be a kind of listener or camera, which is notified of objects add/removing from their current grid and the adjoining grids.

When a player joins a watcher will be created and assigned to them, and the registered to the current position of the spaceship they control. When the spaceship moves the occupants watcher will also be moved. Then as the users spaceship moves across grids, the watcher will also move and the user will recieve the appropriate add/remove messages.

To begin we need a means by which to message users. We will start this by adding some methods to /src/server/zone.js

...
  broadcastToUser(user, message, params, props){
    let eventTime = new Date().getTime();
    this.transmitToSocket( user.getSocket(), message, params, eventTime );
  }  

  transmitToSocket(socket, message, params, eventTime) {
    let dp = {m: message, p: params, w: eventTime};

    socket.emit(
      'cmu',
      dp
    );
  }
...  

For now the above methods are quite straight forward, sending a ‘cmu’ command to the user. cmu commands will for game events. In the message we also include the time, which will be critical later on for making things appear to occur smoothly.

We now need to return to the client.. and build in a means to recieve and process our game zone messages.

In /src/client/UI/gameWorld.js

...
const DELAY = 800;
...
class GameWorld extends Component {

  constructor () {
    ...
    this.inboundMessages = [];  
    this.outboundMessages = [];
    ...
  }
  ...
  initSocket(){
    ...
    this.socket.on('cmu', (data) => {
      this.inboundMessages.unshift(data);
    });
  }
...
  handleInboundMessage(ts) {
    for (var i = this.inboundMessages.length - 1; i >= 0; i--) {
      if (this.inboundMessages[i].w < ts) {
        this.inboundMessageParser(this.inboundMessages[i].m, this.inboundMessages[i].p);
        this.inboundMessages.splice(i, 1);
      }
    }
  }

  inboundMessageParser(cmd, data){
    switch(cmd) {
      case 'TEST_MESSAGE'
      break;
    }
  }

  handleOutboundInput() {
    for (var x = 0; x < this.outboundMessages.length; x++) {
      this.socket.emit(this.outboundMessages[x].command, this.outboundMessages[x].data);
    }
    this.outboundMessages = [];
  }      
...

  getCurrentServerTimeFromTS(){
    return this.props.world.server_timestamp + (Number(new Date().getTime()) - this.props.world.client_timestamp) - DELAY;
  }

  update (ts, dt) {
    let sTime = this.getCurrentServerTimeFromTS();

    this.handleInboundMessage(sTime);
    this.handleOutboundInput();
    ...
  }
..  

So you will see we have added an inbound and an outbound message queue which is processed on each run of the client side game loop. You will see it also checks the time of the inbound message before processing, using the passed in event time. This is because we are going to have it so that the clients are always slightly behind the server to ensure things run smoothly. In some games this would be impossible such as FPS type games, but due to the nature of the game mechanics in this game, we can do it. As it processes the inbound message queue they are sent to ‘inboundMessageParser’ which will contain case statements for our different CMU events such as add/remove objects - which we will add in shortly.

Next back on the server we’ll create a new file in /src/server/ called watcher.js

import logger from './logger';

class Watcher {
  constructor(zone, type, id, owner, coupled) {
    this.zone = zone;
    this.type = type;
    this.registeredGrid = null;
    this.regX = null;
    this.regY = null;
    this.coupled = coupled;

    this.owner = owner;
    this.id = id;
  }

  notifyOwner(message, params) {
    this.zone.broadcastToUser(this.owner, message, params, {});

    return true;
  }
}

module.exports = Watcher;

For now don’t worry too much about what some of this will be for. Later on this watcher will be potentially for NPC’s also and not just users. The important things are that it records the registeredGrid (as with spaceObject), the owner (the user) , and the zone it is in. There is then a method which will broadcast to the owner a given message.

When our clients recieve messages from the server such as for adding objects, they need to know what they are composed of - their specification:

To do this will have some ‘getComposition’ methods - for now we will just have one which returns some basic details on the spaceObject such as it’s id and type, we’ll then add one to ship.js which returns the same but also the docked user id.

/src/server/gameObjects/spaceObject.js

...
  getComposition(){
    return {
      core: this.core,    
      stateSpec: {
        title: this.title
      }
    };
  }
...

/src/server/gameObjects/ship.js

...
  getComposition(){
    return {
      core: this.core,    
      stateSpec: {
        dockedUser: this.dockedUserID,
        title: this.title
      }
    };
  }
...

This allows our server to call .getComposition() on any spaceObject/ship and recieves its makeup - which can then be sent out the clients.

Moving on to our client messages - we need to add them to our constants:

/src/common/constants.js

...
  ADD_OBJECT: 'add_object',
  REMOVE_OBJECT: 'remove_object',
  REMOVE_ALL_OBJECTS: 'remove_all_objects',
  GRID_CHANGE: 'grid_change',
...

Next is quite a big change to /src/server/managers/gridManager.js - if it is confusing it might be worth comparing the differences between old and new gridManager to understand what is going on. I will attempt to explain it after the code:

import logger from '../logger';
import constants from '../../common/constants';

class GridManager {
  constructor(oms, width, height) {
    this.oms = oms;

    this.width = width;
    this.height = height;

    this.watchers = [];
    this.grids = {};
  }

  addObjectToZone(object, nX, nY, params) {
    logger.info(`Adding object to zone at: ${nX} ${nY}`);
    if (object.registeredGrid !== null){
      return false;
    }

    // Get current objects and add object to the collection
    let nObjects = this.getGridAtPosition(nX, nY).objects;
    if(nObjects.includes(object.objectID)){
      logger.error(`Corrupt call to addObjectToZone ${object.objectID}. Already in Grid.`)
      return;
    }

    nObjects.push(object.objectID);
    this.setGridObjectsAtPosition(nX, nY, nObjects);

    object.registeredGrid = this.getGridKeyOfPosition(nX, nY);

    // Notify everyone watching that grid of an object add
    const watchersObservingPoint = this.getWatchersAroundPoint(nX, nY);

    for (let i = 0, len = watchersObservingPoint.length; i < len; i++) {
      watchersObservingPoint[i].notifyOwner(constants.ADD_OBJECT, object.getComposition(), params);
    }
  }

  removeObjectFromZone(object, params) {
    // Remove the object from the grid
    this.removeObjectGridRegistration(object);

    // Notify everyone watching that grid of an object removal
    const watchersObservingPoint = this.getWatchersAroundPoint(object.core.x, object.core.y);
    for (let i = 0, len = watchersObservingPoint.length; i < len; i++) {
      watchersObservingPoint[i].notifyOwner(constants.REMOVE_OBJECT, { objectID: object.objectID, params: params });
    }
  }

  objectPositionChange(object, nX, nY) {

    if ((object.registeredGrid !== this.getGridKeyOfPosition(nX, nY))) {

      this.removeObjectGridRegistration(object); // Remove object from existing grid registration, set to null

      // Add to new grid, set registered grid
      let nObjects = this.getGridAtPosition(nX, nY).objects;
      nObjects.push(object.objectID);
      this.setGridObjectsAtPosition(nX, nY, nObjects);
      object.registeredGrid = this.getGridKeyOfPosition(nX, nY);

      const watchersObservingPreviousPoint = this.getWatchersAroundPoint(object.core.x, object.core.y);
      const watchersObservingNextPoint = this.getWatchersAroundPoint(nX, nY);

      // Notify watchers who have lost awareness of the object
      for (let i = 0, len = watchersObservingPreviousPoint.length; i < len; i++) {
        let found = false;
        for (let j = 0, jlen = watchersObservingNextPoint.length; j < jlen; j++) {
          if ( watchersObservingNextPoint[j].id === watchersObservingPreviousPoint[i].id) {
            found = true;
          }
        }
        if (!found) {
          watchersObservingPreviousPoint[i].notifyOwner(constants.REMOVE_OBJECT, { objectID: object.objectID, params: {} });
        }
      }

      // Do the move before updating the new awareness
      object.core.x = nX;
      object.core.y = nY;

      // Notify watchers who have gained awareness of the object
      for (let j = 0, jlen = watchersObservingNextPoint.length; j < jlen; j++) {
        let found = false;
        for (let i = 0, len = watchersObservingPreviousPoint.length; i < len; i++) {
          if( watchersObservingNextPoint[j].id == watchersObservingPreviousPoint[i].id) {
            found = true;
          }
        }

        if (!found) {
          watchersObservingNextPoint[j].notifyOwner(constants.ADD_OBJECT, object.getComposition(), {});
        }
      }

      if (object.hasActiveOccupant()) {
        let watcher = this.getWatcherByID(object.getOccupantID());
        if (watcher !== null && watcher.coupled) {
          this.watcherPositionChange(watcher, nX, nY);
        }
      }

    } else {
      // No grid change - just update position
      object.core.x = nX;
      object.core.y = nY;
    }

    return true;
  }

  addWatcherToZone(watcher, nX, nY) {
    const result = this.addWatcher(watcher);
    if (!result) {
      logger.error('Adding watcher to a zone which it already contains.');
      return;
    }

    this.updateVisibleObjectsForWatcher(watcher, nX, nY);

    let nObjects = this.getGridAtPosition(nX, nY).watchers;
    nObjects.push(watcher);
    this.setGridWatchersAtPosition(nX, nY, nObjects);
    watcher.registeredGrid = this.getGridKeyOfPosition(nX, nY);

    let thisGrid =  this.getGridDataOfPoint(nX, nY);
    watcher.regX = thisGrid.x;
    watcher.regY = thisGrid.y;
  }

  removeWatcherFromZone(watcher) {
    this.removeWatcherGridRegistration(watcher);
    watcher.registeredGrid = null;
    watcher.notifyOwner(constants.REMOVE_ALL_OBJECTS, {});
    this.removeWatcherByID(watcher.id);
  }

  watcherPositionChange(watcher, nX, nY) {
    if ((watcher.registeredGrid !== this.getGridKeyOfPosition(nX, nY))) {
      this.updateVisibleObjectsForWatcher(watcher, nX, nY);

      this.removeWatcherGridRegistration(watcher);

      let nObjects = this.getGridAtPosition(nX, nY).watchers;
      nObjects.push(watcher);
      this.setGridWatchersAtPosition(nX, nY, nObjects);
      watcher.registeredGrid = this.getGridKeyOfPosition(nX, nY);

      let thisGrid =  this.getGridDataOfPoint(nX, nY);
      watcher.regX = thisGrid.x;
      watcher.regY = thisGrid.y;
    }
  }

  updateVisibleObjectsForWatcher(watcher, nX, nY){
    let oldObjs = [];
    if( watcher.regX !== null && watcher.regY !== null){
      oldObjs = this.getWorldObjectsAroundPoint(watcher.regX, watcher.regY);
    }
    let newObjs = this.getWorldObjectsAroundPoint(nX, nY);

    let userRemoves = [];
    let userAdds = [];

    for (let i = 0, len = oldObjs.length; i < len; i++) {
      let found = false;
      for (let j = 0, jlen = newObjs.length; j < jlen; j++) {
        if ( newObjs[j].objectID === oldObjs[i].objectID) {
          found = true;
        }
      }
      if (!found) {
        userRemoves.push(oldObjs[i].objectID);
      }
    }

    for (let j = 0, jlen = newObjs.length; j < jlen; j++) {
      let found = false;
      for (let i = 0, len = oldObjs.length; i < len; i++) {
        if( newObjs[j].objectID == oldObjs[i].objectID) {
          found = true;
        }
      }

      if (!found) {
        userAdds.push(newObjs[j].getComposition());
      }
    }
    
    watcher.notifyOwner(constants.GRID_CHANGE, { r: userRemoves, a: userAdds });
  }

  removeObjectGridRegistration(aObject) {
    if (aObject.registeredGrid !== null) {
      let gObjects = this.getGridKeyObjects(aObject.registeredGrid);

      gObjects = gObjects.filter((object) => {
        return aObject.objectID !== object
      });

      this.setGridKeyObjects(aObject.registeredGrid, gObjects);
      aObject.registeredGrid = null;
    }
  }

  getWorldObjectsAroundPoint(x, y) {
    if (x == null && y == null) {
      return [];
    }

    let surroundingGrids = this.calculateGridSurrounds(x, y);
    let surroundingObjects = this.getObjectsInGrids(surroundingGrids);

    let objs = [];
    for (let i = 0, len = surroundingObjects.length; i < len; i++) {
      objs.push(this.oms.getWorldObject(surroundingObjects[i]));
    }

    return objs;
  }

  getWatchersAroundPoint(x, y) {
    if(x == null || y == null){
      return [];
    }

    let surroundingGrids = this.calculateGridSurrounds(x, y);
    let surroundingWatchers = this.getWatchersInGrids(surroundingGrids);

    let objs = [];
    for (let i = 0, len = surroundingWatchers.length; i < len; i++) {
      objs.push(surroundingWatchers[i]);
    }

    return objs;
  }

  removeWatcherGridRegistration(watcher) {
    if (watcher.registeredGrid !== null) {
      let gObjects = this.getGridKeyWatchers(watcher.registeredGrid);

      gObjects = gObjects.filter((object) => {
        return watcher.id !== object.id
      });

      this.setGridKeyWatchers(watcher.registeredGrid, gObjects);
    }
  }

  addWatcher(watcher) {
    if (this.getWatcherByID(watcher.id) !== null) {
      return false;
    }
    this.watchers.push(watcher);
    return true;
  }

  getWatcherByID(id) {
    for (let i = 0, len = this.watchers.length; i < len; i++) {
      if (this.watchers[i].id === id) {
        return this.watchers[i];
      }
    }

    return null;
  }

  getAllWatchers() {
    return this.watchers;
  }

  removeWatcherByID(id) {
    this.watchers = this.watchers.filter((watcher) => {
      return watcher.id !== id
    });
  }

  setGridKeyObjects(gridKey, objects) {
    if (this.grids[gridKey] === undefined) {
      this.grids[gridKey] = { objects: [], watchers: [] };
    }

    this.grids[gridKey].objects = objects;

    this.checkForEmptyGridAndPurge(gridKey);
  }

  setGridKeyWatchers(gridKey, watchers) {
    if (this.grids[gridKey] === undefined) {
      this.grids[gridKey] = { objects: [], watchers: [] };
    }

    this.grids[gridKey].watchers = watchers;

    this.checkForEmptyGridAndPurge(gridKey);
  }

  checkForEmptyGridAndPurge(gridKey) {
    if (this.grids[gridKey] !== undefined) {
      if (this.grids[gridKey].objects.length == 0 && this.grids[gridKey].watchers.length == 0) {
        delete this.grids[gridKey];
      }
    }
  }

  getGridKeyObjects(gKey) {
    if (this.grids[gKey] !== undefined) {
      return this.grids[gKey].objects;
    }

    return undefined;
  }

  getGridKeyWatchers(gKey) {
    if (this.grids[gKey] !== undefined) {
      return this.grids[gKey].watchers;
    }

    return undefined;
  }

  getWatchersInGrids(gridList) {
    let foundObjects = [];

    for (let i = 0, len = gridList.length; i < len; i++) {
      let gridKey = this.getGridKey(gridList[i].x, gridList[i].y);

      if (this.grids[gridKey] !== undefined) {
        foundObjects = foundObjects.concat(this.grids[gridKey].watchers);
      }
    }

    return foundObjects;
  }

  getObjectsInGrids(gridList) {
    let foundObjects = [];

    for (let i = 0, len = gridList.length; i < len; i++) {
      let gridKey = this.getGridKey(gridList[i].x, gridList[i].y);

      if (this.grids[gridKey] !== undefined) {
        foundObjects = foundObjects.concat(this.grids[gridKey].objects);
      }
    }

    return foundObjects;
  }

  setGridObjectsAtPosition(sX, sY, objects) {
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    let gridKey = this.getGridKey(cGrid.x, cGrid.y);

    this.setGridKeyObjects(gridKey, objects);
  }

  setGridWatchersAtPosition(sX, sY, objects) {
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    let gridKey = this.getGridKey(cGrid.x, cGrid.y);

    this.setGridKeyWatchers(gridKey, objects);
  }

  getGridAtPosition(sX, sY) {
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    let gridKey = this.getGridKey(cGrid.x, cGrid.y);

    if (this.grids[gridKey] !== undefined) {
      return this.grids[gridKey];
    }

    return { objects: [], watchers: [] };
  }

  getGridKeyOfPosition(sX, sY) {
    if (sX == null && sY == null) {
      return 'null_null'
    }
    let cGrid = { x: Math.floor(sX / this.width) * this.width, y: Math.floor(sY / this.height) * this.height };
    return this.getGridKey(cGrid.x, cGrid.y);
  }

  getGridKey(x, y) {
    return ''+x+'_'+y+'';
  }

  getGridDataOfPoint(x, y) {
    return { x: Math.floor(x / this.width) * this.width, y: Math.floor(y / this.height) * this.height };
  }

  calculateGridSurrounds(x, y) {
    let cGrid = { x: Math.floor(x / this.width) * this.width, y: Math.floor(y / this.height) * this.height };

    let x1 = Math.floor(cGrid.x - this.width);
    let x2 = Math.floor(cGrid.x + this.width);
    let y1 = Math.floor(cGrid.y - this.height);
    let y2 = Math.floor(cGrid.y + this.height);

    return [
      { x: x1 , y: y1 },
      { x: cGrid.x , y: y1 },
      { x: x2 , y: y1 },
      { x: x1 , y: cGrid.y},
      { x: cGrid.x, y: cGrid.y},
      { x: x2, y: cGrid.y},
      { x: x1 , y: y2 },
      { x: cGrid.x , y: y2 },
      { x: x2, y: y2}
      ];
  }
}

module.exports = GridManager;

Quite a lot changes in there compared to the previous version. The first thing was that wheras before at each grid key there was just the “.objects” collection. We added in “.watchers” - which stores which watchers are located in that grid.

We now need to add in some methods to /src/server/managers/objectManager.js for a user registering themselves as a watcher in the OMS, and for leaving, making use of our new gridManager code.

...
import Watcher from '../watcher';
...
  userWatcherJoin(user, x, y) {
    let wNew = new Watcher(this.zone, 'user', user.userid, user, true);
    this.gm.addWatcherToZone(wNew, x, y);
  }

  userWatcherLeave(user) {
    let w = this.gm.getWatcherByID(user.userid)
    if (w !== null) {
      this.gm.removeWatcherFromZone(w);
    }
  }
...

Now we will modify /src/server/zone.js to use these OMS methods

...
  loadUserIntoZone(user) {
    ...
    ObjectManager.loadShipFromID(user.vehicleid).then((userShipRecord) => {
      this.oms.instantiateShipFromRecord(userShipRecord).then((shipObject) => {
        
        logger.info('A ship has been spawned and loaded...');

        // Assign user to vehicle
        shipObject.setDockedUser(user);

        // Load the user in at the point we will load in at
        logger.info('User is loaded in as a watcher at ship position');
        this.oms.userWatcherJoin(user, shipObject.core.x, shipObject.core.y);
        ...
      });
    });
  }
...
  unloadUserFromZone(user) { 
    logger.info('Unloading user from zone... ');
    this.removeUserRegistration(user.userid);

    this.oms.userWatcherLeave(user);
    ...
  }
...

The two lines added were one each in unload and load, this.oms.userWatcherJoin(user, shipObject.core.x, shipObject.core.y); going between the ‘setDockedUser’ and ‘addWorldObject’ methods, and this.oms.userWatcherLeave(user); going after ‘removeUserRegistration’ in the user unload.

Now we need to switch back to our client and add in the handling for our new server messages. Alter the inboundMessageParser method in /src/client/UI/gameWorld.js as follows:

...
  inboundMessageParser(cmd, data){
    switch(cmd) {
      case constants.ADD_OBJECT:
        console.log('Add object recieved:')
        console.log(data);
      break;

      case constants.GRID_CHANGE:
        console.log('Grid change received:');
        console.log(data)
      break;

      case constants.REMOVE_OBJECT:
        console.log('remove object recieved:');
      break;

      case constants.REMOVE_ALL_OBJECTS:
        console.log('remove all objects recieved:')
      break;
    }
  }
...

If we compile and run this on the server and the have our client connect, in the client console window we should see the following:

clientmessages

So to summarize this section, we created the ‘Watcher’, we had it so a watcher can be created for a user when they join. Watchers can be assigned to grids just as objects are. When a user joins they are effectively subscribed to add/removes that happen on the grids surrounding them.

Now that we have messages being recieved on the client, we need to create a visual representation of those objects on the screen but to begin that we need to build the foundations of our game world engine.

Link to source files for Part 5

Part 6 - Game World Engine foundations

We are now going to start on our game world engine which runs within each zone on the server, and also on the client.

First we will make some objects on the client.

Create the folder ‘entities’ in /src/client/ and then within /src/client/entities create the folder ‘objects’. Finally within /src/client/entities/objects create the file spaceObject.js with the following:

export default class SpaceObject {
  constructor (typeConfig, comp) {
    this.typeConfig = typeConfig;
    this.objectID = comp.core.objectID;
    this.typeID = comp.core.typeID;

    this.spec = comp.stateSpec;

    this.title = comp.stateSpec.title;

    this.width = typeConfig.width;
    this.height = typeConfig.height;

    this.core = {
      x: comp.core.x,
      y: comp.core.y,
      angle: comp.core.angle,
      lastTimestamp: 0
    }
  }

  doStep(dt) {

  }
}

This is the client side equivilent of the spaceObject class on the server. Next we’ll do the ship. In /src/client/entities/objects/ create ship.js

import SpaceObject from './spaceObject'
import constants  from '../../../common/constants';

export default class Ship extends SpaceObject {
  constructor (typeConfig, comp) {
    super(typeConfig, comp);

    this.rotationSpeed = comp.stateSpec.rotationSpeed; //Angles per second
    this.speed = comp.stateSpec.speed; // pixels per second

    this.core = {
      x: comp.core.x,
      y: comp.core.y,
      lastTimestamp: 0,
      angle: comp.core.angle,
      angularVelocity: 0,
      speedX: 0,
      speedY: 0,
      keyH: 0, // -1, 0, 1
      keyV: 0,
      keyS: 0
    }
  }

  update (currentTime) {

  }

  draw (context, cam) {
    let w = this.typeConfig.width
    let h = this.typeConfig.height

    context.save()

    context.strokeStyle = '#AAAAAA'
    context.lineWidth = 1

    context.translate((this.core.x - cam.x), (this.core.y - cam.y))
    context.rotate((this.core.angle+90) * Math.PI / 180)

    context.beginPath();

    context.moveTo(0, - (h / 2))
    context.lineTo(0 - (w / 2), (h / 2))
    context.lineTo((w / 2), (h / 2))
    context.lineTo(0, 0 - (h / 2))

    context.closePath()
    context.stroke()

    context.restore()
  }
}

The game world engine on the client is going to share code with the game world on the server. This is so that the events happening on the client are the same as that on the server (albeit slightly delayed). The server is going to send messages to the clients keeping them updated on the change of state of objects, and as the ‘world code’ both the server and client run is the same it should in theory mean they are doing the same thing. I’ll explain this more clearly as it comes together..

For now I will create a skeletal version of this engine.

In /src/common/ create gameWorldEngine.js

class GameWorldEngine {

  constructor(parent, objectCollection) {
  }

  run(ts, dt) {
  }

}

module.exports = GameWorldEngine;

The above common gameWorldEngine will be from what our server and client gameEngines extend from.

Next in /src/client/ create clientWorldEngine.js with the following:

import GameWorldEngine from '../common/gameWorldEngine';
import constants from '../common/constants';
import Ship from './entities/objects/ship';

class ClientWorldEngine extends GameWorldEngine {

  constructor(client, collection, configData) {
    super(client, collection);

    this.worldObjects = collection;
    this.configData = configData;

    this.playerUserID = null;
    this.playerShip = null;

    this.watchX = 0;
    this.watchY = 0;
  }

  fullUpdate(d){

  }

  setWatchPosition(x, y) {
    this.watchX = x;
    this.watchY = y;
  }

  addObject(composition){
    let obj = null;
    let itemConfig = null;
    for (let i = 0, len = this.configData.itemtypes.length; i < len; i++) {
      if (this.configData.itemtypes[i].id === composition.core.typeID) {
        itemConfig = this.configData.itemtypes[i];
      }
    }

    if (itemConfig == null) {
      console.log('no item config');
      return;
    }

    switch(composition.core.typeID){
      case 6:
        obj = new Ship(
          itemConfig,
          composition );
        break;
    }

    if(obj!==null){
      if(composition.stateSpec.dockedUser){
        if(composition.stateSpec.dockedUser == this.playerUserID){
          console.log('setting as player');

          this.playerShip = obj;
          this.watchX = null;
          this.watchY = null;
        }
      }

      this.worldObjects.push(obj);
    }
  }

  removeAllObjects() {
    this.worldObjects.length = 0;
  }

  removeObject(objectID, params) {
    for(var i = this.worldObjects.length - 1; i >= 0; i--) {
      if(this.worldObjects[i].objectID === objectID) {
         this.worldObjects.splice(i, 1);
      }
    }
  }

  resetGameState() {
    this.worldObjects.length = 0;
  }

}

module.exports = ClientWorldEngine;

This client side world engine for now is just holding our game objects that the client is aware of. If the object added is our players ship then we set a reference to it.

Next in /src/client/UI/gameWorld.js we need to integrate in our new client engine.

...
import ClientWorldEngine from '../clientWorldEngine';
...
  constructor () {
    super();
    ...
    this.doRun = true;
    ...
  }
...
  componentDidMount () {
    this.socket = this.props.socket;
    this.gameWorldEngine = new ClientWorldEngine(this, [], this.props.world.configData);

    this.initSocket();
    this.gameWorldEngine.playerUserID = this.props.userid;

    context = this.canvasref.current.getContext('2d');

    document.addEventListener('keydown', this.keyDownCheck.bind(this));
    document.addEventListener('keyup', this.keyUpCheck.bind(this));
    document.addEventListener('click',  this.clickCheck.bind(this));

    this.socket.connect();
  }

...
  inboundMessageParser(cmd, data){
    switch(cmd) {
      case constants.ADD_OBJECT:
        console.log('Add object recieved:')
        console.log(data);
        this.gameWorldEngine.addObject(data);
      break;

      case constants.GRID_CHANGE:
        console.log('Grid change received:');
        for (let i = 0; i < data.a.length; i++) {
          this.gameWorldEngine.addObject(data.a[i]);
        }
        for (let i = 0; i < data.r.length; i++) {
          this.gameWorldEngine.removeObject(data.r[i], {});
        }
      break;

      case constants.REMOVE_OBJECT:
        console.log('remove object recieved:');
        this.gameWorldEngine.removeObject(data.objectID, data.params);
      break;

      case constants.REMOVE_ALL_OBJECTS:
        console.log('remove all objects recieved:')
        this.gameWorldEngine.removeAllObjects(data.params);
      break;
    }
  }  
...
  update (ts, dt) {
    let sTime = this.getCurrentServerTimeFromTS();
    this.handleInboundMessage(sTime);
    this.handleOutboundInput();

    if(this.doRun) {
      this.gameWorldEngine.run(sTime, dt);
    }

    this.clear(cam);
    this.draw(cam);
  }

...
  draw(observationPoint) {
    for (let i = 0; i < this.gameWorldEngine.worldObjects.length; i++) {
      let obj = this.gameWorldEngine.worldObjects[i];

      obj.draw(context, observationPoint);
    }    
  }
...  

In the above we created our client game engine when the component mounts. We added a doRun flag to the constructor for pausing the game loop if needs be. We then altered the inboundMessageParser to make calls into the game engine for each of our recieved object messages. Next we altered update to run our game engine. We then altered the draw method to run on each object within the game engine.

Now we will create the server side game world engine in /src/server/serverWorldEngine.js

import GameWorldEngine from '../common/gameWorldEngine';

class ServerWorldEngine extends GameWorldEngine {
  constructor(client, objectCollection) {
    super(client, objectCollection);
  }
}

module.exports = ServerWorldEngine;

As you can see this extends our common engine, similar to the client. Now we need to setup the engine in our zone.

/src/server/zone.js

...
import ServerWorldEngine from './serverWorldEngine';
...
  constructor(zm, um, zoneID, zoneName) {
    ...
    this.serverWorldEngine = new ServerWorldEngine(this, this.oms.worldObjects);
  }
...
  run() {
    ...
    this.serverWorldEngine.run(currentServerTime, currentServerTime - this.lastTimestamp)

    this.lastTimestamp = currentServerTime;
  }
...  

The change there is that We setup the world engine in the constructor, and for now directly pass in our OMS object collection. In the run method we added a call to the run method of the world engine inserted before setting the lastTimeStamp.

We can now do a multiplayer test of this to see that the ship add/remove messages are coming in.

npm run-script build
...
npm start

Next open up two different browsers, run localhost:3000 in each. Create a new game account on each. Then launch just one of them:

multi1

You can see the single ship, and the console debug output on the client. Now we will click launch on the 2nd client..

multi2

The second ship now appears on the 1st client (both are not visible on the 2nd client due to it being off screen). Now we refresh the 2nd client to make it disconnect and take it back to launchpad…

multi3

And there are the messages on the 1st client removing the 2nd ship due to the 2nd player disconnecting.

The server should have the output along the lines of the following:

Listening on 3000
(node:18246) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(node:18246) DeprecationWarning: collection.ensureIndex is deprecated. Use createIndexes instead.
info: Connected to MongoDB server successfully. {"timestamp":"2018-12-24 09:03:12 AM +0000"}
info: Initializing game server... {"timestamp":"2018-12-24 09:03:12 AM +0000"}
info: Game Server init began... {"timestamp":"2018-12-24 09:03:12 AM +0000"}
info: Init complete. Starting game server process... {"timestamp":"2018-12-24 09:03:12 AM +0000"}
started...
info: Client established connection and authenticated. F2ySxcjsGK3z6voZAAAA test5. Connected clients: 1 {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: test5 invoked handshake. {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: test5 invoked launched. {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: Attempting to load user 5c20a0bcae09ba46fa1934ec  {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: Game: User loaded 5c20a0bcae09ba46fa1934ec {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: A ship has been spawned and loaded... {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: User is loaded in as a watcher at ship position {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: Adding object to world {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: Adding object to world: 5c20a0bcae09ba46fa1934ed {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: Adding object to zone at: 138 51 {"timestamp":"2018-12-24 09:03:15 AM +0000"}
(node:18246) DeprecationWarning: collection.findAndModify is deprecated. Use findOneAndUpdate, findOneAndReplace or findOneAndDelete instead.
info: Ship object persistance complete. {"timestamp":"2018-12-24 09:03:15 AM +0000"}
info: Client established connection and authenticated. evtM3YAZXjJCRs0MAAAB test6. Connected clients: 2 {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: test6 invoked handshake. {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: test6 invoked launched. {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: Attempting to load user 5c20a0caae09ba46fa1934ee  {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: Game: User loaded 5c20a0caae09ba46fa1934ee {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: A ship has been spawned and loaded... {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: User is loaded in as a watcher at ship position {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: Adding object to world {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: Adding object to world: 5c20a0caae09ba46fa1934ef {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: Adding object to zone at: 212 81 {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: Ship object persistance complete. {"timestamp":"2018-12-24 09:03:27 AM +0000"}
info: test6 disconnected. {"timestamp":"2018-12-24 09:03:35 AM +0000"}
info: Game: User exiting 5c20a0caae09ba46fa1934ee {"timestamp":"2018-12-24 09:03:35 AM +0000"}
info: Unloading user 5c20a0caae09ba46fa1934ee from UserManager {"timestamp":"2018-12-24 09:03:35 AM +0000"}
userzoneid:  1
info: Unloading user from zone...  {"timestamp":"2018-12-24 09:03:35 AM +0000"}
info: Unloading user vehicle: 5c20a0caae09ba46fa1934ef {"timestamp":"2018-12-24 09:03:35 AM +0000"}
info: Removing object from world:: 5c20a0caae09ba46fa1934ef {"timestamp":"2018-12-24 09:03:35 AM +0000"}
info: Found and removed {"timestamp":"2018-12-24 09:03:35 AM +0000"}

Next.. we are going to now add some movement using our new world engine.

Link to source files for Part 6

Part 7 - Game Engine Movement and collision detection

You may have seen the following variables on ship.js within the ‘core’ data structure: keyH, keyV, keyS.

Using these is how our game movement will occur. They each represent a pair of keys onkeyboard and have 3 potential values.

keyH - rotating left or right, or neither (A and D) keyV - accelerating forward, reversing, or neither (W and S) keyS - L and K (these are not for movement but for modules such as moving a turret and will be used later)

Each ship/spaceObject has a “step” method. This is run on each run of the game engine, we our is to make the actions taken on the step on the server spaceobject’s, be the same as the actions taken by each clients spaceObjects. This step method is given a time in ms, the object will then this amount of time worth’s of actions and movement. This will become clearer later.

When a player presses down a key on their client, the key press will be sent to the game server, added to an input queue on that players ship with a timestamp. The keypress will also at this point be dispatched to all nearby players. As it arrives on those clients it will also be added to a clientside input queue for the ship. If all works correctly both the server game engine, and all the client game engines that are aware of the ship (and the keypress) will then process this keypress in the same way - translating the keypress into a change of state of one those 3 variables above (accelerating/reversing/turning left/turning right etc), the clients being slightly behind the server due to our designed in lag.

We’ll add some more constants in /src/common/constants.js

module.exports = {
  ...
  MOVECMD: 'movecmd',
  KEY_RIGHT_U: 'right_u',
  KEY_LEFT_U: 'left_u',
  KEY_REV_U: 'rev_u',
  KEY_FOR_U: 'for_u',
  KEY_L_U: 'l_u',
  KEY_K_U: 'k_u',
  KEY_RIGHT_D: 'right_d',
  KEY_LEFT_D: 'left_d',
  KEY_REV_D: 'rev_d',
  KEY_FOR_D: 'for_d',
  KEY_L_D: 'l_d',
  KEY_K_D: 'k_d'
};

In /src/client/UI/gameWorld.js we need to start sending input

...
let controls = {
  leftDown: false,
  rightDown: false,
  forwardDown: false,
  reverseDown: false,
  kDown: false,
  lDown: false
}
...
class GameWorld extends Component {
...
  keyDownCheck(e){
    if(!controls.kDown){
      if(e.keyCode == 75){
        controls.kDown = true;
        this.sendInput(constants.KEY_K_D, { movement: true });
      }
    }

    if(!controls.lDown){
      if(e.keyCode == 76){
        controls.lDown = true;
        this.sendInput(constants.KEY_L_D, { movement: true });
      }
    }

    if(!controls.forwardDown){
      if(e.keyCode == 87){
        controls.forwardDown = true;
        this.sendInput(constants.KEY_FOR_D, { movement: true });
      }
    }

    if(!controls.reverseDown){
      if(e.keyCode == 83){
        controls.reverseDown = true;
        this.sendInput(constants.KEY_REV_D, { movement: true });
      }
    }

    if(!controls.leftDown){
      if(e.keyCode == 65){
        controls.leftDown = true;
        this.sendInput(constants.KEY_LEFT_D, { movement: true });
      }
    }

    if(!controls.rightDown){
      if(e.keyCode == 68){
        controls.rightDown = true;
        this.sendInput(constants.KEY_RIGHT_D, { movement: true });
      }
    }
  }

  keyUpCheck(e){
    if(controls.kDown){
      if(e.keyCode == 75){
        controls.kDown = false;
        this.sendInput(constants.KEY_K_U, { movement: true });
      }
    }

    if(controls.lDown){
      if(e.keyCode == 76){
        controls.lDown = false;
        this.sendInput(constants.KEY_L_U, { movement: true });
      }
    }

    if(controls.forwardDown){
      if(e.keyCode == 87){
        controls.forwardDown = false;
        this.sendInput(constants.KEY_FOR_U, { movement: true });
      }
    }

    if(controls.reverseDown){
      if(e.keyCode == 83){
        controls.reverseDown = false;
        this.sendInput(constants.KEY_REV_U, { movement: true });
      }
    }

    if(controls.leftDown){
      if(e.keyCode == 65){
        controls.leftDown = false;
        this.sendInput(constants.KEY_LEFT_U, { movement: true });
      }
    }

    if(controls.rightDown){
      if(e.keyCode == 68){
        controls.rightDown = false;
        this.sendInput(constants.KEY_RIGHT_U, { movement: true });
      }
    }
  }

  sendInput(input, type) {
    var message = {
        command: constants.MOVECMD,
        data: {
            input: input,
            type: type
        }
    };

    this.outboundMessages.push(message);
    this.messageIndex++;
  }
...

In the above We changed the two key press handlers and added a new method sendInput. You will see we capture the key presses for A,S,W,D etc, then call sendInput which adds the event to the outbound message queue - ready to be sent to the server.

We are going to soon be ‘broadcasting’ to clients in a particular area, to do this we need to add some more methods in our zone OMS.

/src/server/managers/objectManager.js

...
  broadcastInRangeOfObject(object, message, params, props) {
    // Is the object on the grid
    if (object.registeredGrid == null){
      console.log('Object not on grid so not sending out Broadcast: ',message);
      return;
    }
    
    this.broadcastInRange(object.core.x, object.core.y, message, params, props);
  }

  broadcastInRange(x, y, message, params, props) {
    if (props == undefined) {
      props = {};
      props.except = null;
    }

    let eventTime = new Date().getTime();

    let objs = this.gm.getWatchersAroundPoint(x, y)
    for (let i = 0, len = objs.length; i < len; i++) {
      this.zone.transmitToSocket( objs[i].owner.getSocket(), message, params, eventTime, false );
    }
  }
  ...

This broadcast makes use of ‘getWatchersAroundPoint’ within the GridManager, finds everyone watching that area and broadcasts whatever message to them.

We’ll now alter the common gameserver slightly so that it can handle input:

/src/common/gameWorldEngine.js

...
  constructor(parent, objectCollection) {
    this.parent = parent;
    this.objects = objectCollection;
    this.inputQueue = [];
    this.lastProcessTimestamp = new Date().getTime();
  }

  addToInputQueue(queueItem) {
    this.inputQueue.push(queueItem);
  }  
...

Next we need to handle these messages as they arrive on the server.

In /src/server/cmdShipServer.js

...
  constructor(io) {
    ...
    io.sockets.on('connection', socketioJwt.authorize({
        secret: process.env.JWTSECRET,
        timeout: 15000
      })).on('authenticated', (socket) => {
      ...
      socket.on(constants.MOVECMD, (data) => {
        let user = this.userManager.getUser(socket.decoded_token._id);
        if (user !== undefined && user !== null) {
          this.zoneManager.getZone(user.zoneid).onReceivedMoveInput(user, data);
        }
      });
    });

    return this;
  }
...

In the above we added a socket.on for MOVECMD to the end of the “io.sockets.on(‘connection’” handler. This routes the input into the users zone.

Now in /src/server/zone.js we’ll add this ‘onReceivedMoveInput’ method:

...
  constructor(zm, um, zoneID, zoneName) {
    ...
    this.playerInputQueues = {};
    this.maxKeyItems = 1;
  }
...
  onReceivedMoveInput(user, data) {
    if (!this.playerInputQueues.hasOwnProperty(user.userid))
        this.playerInputQueues[user.userid] = [];
    let queue = this.playerInputQueues[user.userid];
    queue.push(data);
  }

  createInputQueueItem(objectID, delay, key){
    this.maxKeyItems += 1;
    return { qID: this.maxKeyItems, objectID, ts: (new Date().getTime()+delay), key };
  }

  processPlayerInputQueues() {
    for(let userID of Object.keys(this.playerInputQueues)) {
      let inputQueue = this.playerInputQueues[userID];

      for(let iIndex = 0; iIndex < inputQueue.length; iIndex++) {

        let theUser = this.userManager.getUser(userID);
        if(!theUser){
          continue;
        }

        let playerVehicle = this.oms.getWorldObject(theUser.vehicleid);

        if(playerVehicle) {
          let newInputItem = this.createInputQueueItem(theUser.vehicleid, 0, inputQueue[iIndex].input);

          this.serverWorldEngine.addToInputQueue(newInputItem);
          playerVehicle.addToInputQueue(newInputItem); 
        }
      }

      this.playerInputQueues[userID] = [];
    }
  }
...
  run() {
    let currentServerTime = new Date().getTime();

    this.processPlayerInputQueues();
    ...
  }  
...

In the above we added some queue variables to the constructor, added some new methods for handling it and receiving the input. You will also see we added the processing of the input queue to our zone run method.

At this point our messages are coming in from the clients and being pushed into the serverGameEngine on the zone which invokes the run method in the common game engine.

We also need to create the method ‘addToInputQueue’ on /src/server/gameObjects/ship.js

...
  addToInputQueue(queueItem) {
    if(this.registeredGrid == null){
      console.log('object is off grid')
      return;
    }

    this.zone.oms.broadcastInRangeOfObject(this,
       constants.SHIPKEY,
       {qID: queueItem.qID, objectID: this.objectID, ts: queueItem.ts, inputData: queueItem.key},
       {});

    this.core.inputQueue.push(queueItem);
  }
...

In /src/common/constants.js we’ll add that shipKey constant

...
  SHIPKEY: 'shipKey'
...

So with that working, when our a ships key presses come into the server they will added to the queue for that ship, and within the above method ‘addToInput’ queue, the key change will be broadcast to those nearby also using ‘broadcastInRangeOfObject’.

Now back to the client and add the handler for that shipkey message, so in /src/client/UI/gameWorld.js add the following to the inbound message parser:

...
  inboundMessageParser(cmd, data){
    switch(cmd) {
      ...
      case constants.SHIPKEY:
        console.log('shipkey received ',data.inputData);
        let theShip = this.gameWorldEngine.getObject(data.objectID);
        if(theShip){
          this.gameWorldEngine.addToInputQueue({qID: data.qID, objectID: theShip.objectID, ts: data.ts, key: data.inputData});
        }
      break;      
    }
  }
...

So just as our server added the ships keypress to its input queue (and then dispatched to all clients), the client now also recieves it (if it is in range) and adds it to its world engine queue.

Next we need to begin processing these input queues:

Both our server game engine and client game engine extend from the same common game engine in /src/common/clientWorldEngine.js, it is in here we will now build the input processor:

class GameWorldEngine {

  constructor(parent, objectCollection) {
    this.parent = parent;
    this.objects = objectCollection;
    this.inputQueue = [];
    this.lastProcessTimestamp = new Date().getTime();
  }

  addToInputQueue(queueItem) {
    this.inputQueue.push(queueItem);
  }

  doStepsToTime(stepDt){
    for (let i = 0, len = this.objects.length; i < len; i++) {
      this.objects[i].doStep(stepDt);
    }
  }

  run(ts, dt) {
    let keysDue = this.getInputQueueItemsDue(ts); // Get keys due
    let endStamp;

    if(keysDue.length > 0){
      let preKeyStepDt = keysDue[0].ts - this.lastProcessTimestamp; // Get everything up until first keypress

      this.doStepsToTime(preKeyStepDt)
      
      for (let k = 0; k < keysDue.length; k++) {
        if(k < keysDue.length - 1){
          endStamp = keysDue[k+1].ts;
        } else {
          endStamp = ts;
        }

        let inputShip = this.getObject(keysDue[k].objectID);

        if(inputShip){
          inputShip.applyKeyChange(keysDue[k]); // Apply key to ship
        }

        this.doStepsToTime(endStamp - keysDue[k].ts)
      }
    } else {
      this.doStepsToTime(ts - this.lastProcessTimestamp)
    }

    this.lastProcessTimestamp = ts;
  }

  getInputQueueItemsDue(ts) {
    let keysDue = [];
    for (var i = this.inputQueue.length - 1; i >= 0; i--) {
      if (this.inputQueue[i].ts < ts) {
        keysDue.push(this.inputQueue[i]);
        this.inputQueue.splice(i, 1);
      }
    }

    keysDue.sort( (a,b) => {
      return (a.ts > b.ts) ? 1 : ((b.ts > a.ts) ? -1 : 0);
    });

    return keysDue;
  }

  getObject(objectID) {
    for (let i = 0, len = this.objects.length; i < len; i++) {
      if (this.objects[i].objectID == objectID){
        return this.objects[i];
      }
    }

    return undefined;
  }
}

module.exports = GameWorldEngine;

If you remember the important aspect of our movement is the ‘step’ on each object (We have not yet written out these step methods). Our processor works out how long has elapsed since the last time it ran, up until the first input item in the queue and then ‘steps’ every object this amount of time. We then iterate over all inputs and apply them one by one moving forward through the input changes and then ‘stepping’ forward to the next input.

You will see ‘applyKeyChange’. Our inputs are applied to the ships using this method (it isnt created yet but we’ll make it next). It will take the input key press for that ship, and then alters the previously mentioned key settings (keyH, keyV etc) to alter its rotationSpeed, acceleration and so on.

So to summarise the processor, we go over each inputkey press, apply it. Then apply movement on everything up until the next keypress is due. Apply it, and then apply movement again and so on, up until the present.

Now we will add the applyKeyChange to our ship objects on both the client and server (as both will be called due to it being the shared processor code)

First in /src/server/gameObjects/ship.js add the following method:

...
  applyKeyChange(keyChange) {
    if (keyChange.key === constants.KEY_FOR_D) {
        this.core.keyV = 1;
    } else if (keyChange.key === constants.KEY_REV_D) {
        this.core.keyV = -1;
    } else if (keyChange.key === constants.KEY_RIGHT_D) {
        this.core.keyH = 1;
    } else if (keyChange.key === constants.KEY_LEFT_D) {
        this.core.keyH = -1;
    } else if (keyChange.key === constants.KEY_L_D) {
        this.core.keyS = 1;
    } else if (keyChange.key === constants.KEY_K_D) {
        this.core.keyS = -1;
    } else if (keyChange.key === constants.KEY_FOR_U) {
        this.core.keyV = 0;
    } else if (keyChange.key === constants.KEY_REV_U) {
        this.core.keyV = 0;
    } else if (keyChange.key === constants.KEY_RIGHT_U) {
        this.core.keyH = 0;
    } else if (keyChange.key === constants.KEY_LEFT_U) {
        this.core.keyH = 0;
    } else if (keyChange.key === constants.KEY_L_U) {
        this.core.keyS = 0;
    } else if (keyChange.key === constants.KEY_K_U) {
        this.core.keyS = 0;
    }

    // remove this keyChange from our inputQueue
    this.core.inputQueue = this.core.inputQueue.filter((item) => {
      return item.qID !== keyChange.qID
    });
  }
...

Then on the client at /src/client/entities/object/ship.js add this method:

import constants  from '../../../common/constants';
...
  applyKeyChange(keyChange) {
    if (keyChange.key === constants.KEY_FOR_D) {
        this.core.keyV = 1;
    } else if (keyChange.key === constants.KEY_REV_D) {
        this.core.keyV = -1;
    } else if (keyChange.key === constants.KEY_RIGHT_D) {
        this.core.keyH = 1;
    } else if (keyChange.key === constants.KEY_LEFT_D) {
        this.core.keyH = -1;
    } else if (keyChange.key === constants.KEY_L_D) {
        this.core.keyS = 1;
    } else if (keyChange.key === constants.KEY_K_D) {
        this.core.keyS = -1;
    } else if (keyChange.key === constants.KEY_FOR_U) {
        this.core.keyV = 0;
    } else if (keyChange.key === constants.KEY_REV_U) {
        this.core.keyV = 0;
    } else if (keyChange.key === constants.KEY_RIGHT_U) {
        this.core.keyH = 0;
    } else if (keyChange.key === constants.KEY_LEFT_U) {
        this.core.keyH = 0;
    } else if (keyChange.key === constants.KEY_L_U) {
        this.core.keyS = 0;
    } else if (keyChange.key === constants.KEY_K_U) {
        this.core.keyS = 0;
    }
  }
...

You will notice both are almost identical. Ideally further on we can refactor the server side and client side objects to have a base class so that we can share a lot of the code between the two but for now they will remain seperate.

Ok, so we have our input processor running, applying key presses to the objects. Next the important method - the actual step method themselves that do the movement.

As with applyKeyChange we need to do this on both client and server ships.

/src/server/gameObjects/ship.js

...
  doStep(dt) {
    if(this.registeredGrid == null){
      return;
    }

    if (this.core.keyH == 0 && this.core.keyV == 0 && this.core.speedX == 0 && this.core.speedY == 0) {
      return;
    }

    // Move
    let movement = 0;
    let angularMovement = 0

    let thrust = 20;
    let rotationSpeed = 25;

    let decceleration = 20;
    let maxSpeed = 40;

    let proposedNewAngle = this.core.angle;
    let proposedNewX = this.core.x;
    let proposedNewY = this.core.y;

    if (this.core.keyH == 1) {
      this.core.angularVelocity = rotationSpeed;
    } else if(this.core.keyH == -1) {
      this.core.angularVelocity = 1-rotationSpeed;
    }else {
      this.core.angularVelocity = 0;
    }

    angularMovement = this.core.angularVelocity * (dt/1000);

    proposedNewAngle += angularMovement;

    if(proposedNewAngle > 360) proposedNewAngle -= 360;
    if(proposedNewAngle < 0) proposedNewAngle += 360;

    if (this.core.keyV === 1) {
      this.core.speedX += ((thrust * (dt/1000)) * Math.cos(proposedNewAngle * (Math.PI/180)));
      this.core.speedY += ((thrust * (dt/1000)) * Math.sin(proposedNewAngle * (Math.PI/180)));
    } else if(this.core.keyV === -1) {
      this.core.speedX -= (thrust * Math.cos(proposedNewAngle * (Math.PI/180))  * (dt/1000));
      this.core.speedY -= (thrust * Math.sin(proposedNewAngle * (Math.PI/180))  * (dt/1000));
    } else if(this.core.keyV === 0){
      if(this.core.speedX < 0) {
        this.core.speedX += decceleration * (dt/1000);
        if(this.core.speedX > 0) this.core.speedX = 0;
      }

      if(this.core.speedX > 0) {
        this.core.speedX -= decceleration * (dt/1000);
        if(this.core.speedX < 0) this.core.speedX = 0;
      }

      if(this.core.speedY < 0) {
        this.core.speedY += decceleration * (dt/1000);
        if(this.core.speedY > 0) this.core.speedY = 0;
      }

      if(this.core.speedY > 0) {
        this.core.speedY -= decceleration * (dt/1000);
        if(this.core.speedY < 0) this.core.speedY = 0;
      }
    }

    let vel = Math.sqrt((this.core.speedX*this.core.speedX)+(this.core.speedY*this.core.speedY));

    if (vel > maxSpeed) {
      this.core.speedX *= maxSpeed / vel;
      this.core.speedY *= maxSpeed / vel;
    }

    proposedNewX += this.core.speedX * (dt/1000);
    proposedNewY += this.core.speedY * (dt/1000);

    if(this.setPosition(proposedNewX, proposedNewY)){ 
      this.core.x = proposedNewX;
      this.core.y = proposedNewY;
      this.core.angle = proposedNewAngle;
    }
  }
...

/src/client/entities/objects/ship.js

...
  doStep(dt) {
    if (this.core.keyH == 0 && this.core.keyV == 0 && this.core.speedX == 0 && this.core.speedY == 0) {
      return;
    }

    let movement = 0;
    let angularMovement = 0

    let thrust = 20;
    let rotationSpeed = 25;

    let decceleration = 20;
    let maxSpeed = 40;

    let proposedNewAngle = this.core.angle;
    let proposedNewX = this.core.x;
    let proposedNewY = this.core.y;

    if (this.core.keyH == 1) {
      this.core.angularVelocity = rotationSpeed;
    } else if(this.core.keyH == -1) {
      this.core.angularVelocity = 1-rotationSpeed;
    }else {
      this.core.angularVelocity = 0;
    }

    angularMovement = this.core.angularVelocity * (dt/1000);

    proposedNewAngle += angularMovement;

    if(proposedNewAngle > 360) proposedNewAngle -= 360;
    if(proposedNewAngle < 0) proposedNewAngle += 360;

    if (this.core.keyV === 1) {
      this.core.speedX += ((thrust * (dt/1000)) * Math.cos(proposedNewAngle * (Math.PI/180)));
      this.core.speedY += ((thrust * (dt/1000)) * Math.sin(proposedNewAngle * (Math.PI/180)));
    } else if(this.core.keyV === -1) {
      this.core.speedX -= (thrust * Math.cos(proposedNewAngle * (Math.PI/180))  * (dt/1000));
      this.core.speedY -= (thrust * Math.sin(proposedNewAngle * (Math.PI/180))  * (dt/1000));
    } else if(this.core.keyV === 0){

      if(this.core.speedX < 0) {
        this.core.speedX += decceleration * (dt/1000);
        if(this.core.speedX > 0) this.core.speedX = 0;
      }

      if(this.core.speedX > 0) {
        this.core.speedX -= decceleration * (dt/1000);
        if(this.core.speedX < 0) this.core.speedX = 0;
      }

      if(this.core.speedY < 0) {
        this.core.speedY += decceleration * (dt/1000);
        if(this.core.speedY > 0) this.core.speedY = 0;
      }

      if(this.core.speedY > 0) {
        this.core.speedY -= decceleration * (dt/1000);
        if(this.core.speedY < 0) this.core.speedY = 0;
      }
    }

    let vel = Math.sqrt((this.core.speedX*this.core.speedX)+(this.core.speedY*this.core.speedY));

    if (vel > maxSpeed) {
      this.core.speedX *= maxSpeed / vel;
      this.core.speedY *= maxSpeed / vel;
    }

    proposedNewX += this.core.speedX * (dt/1000);
    proposedNewY += this.core.speedY * (dt/1000);

    this.core.x = proposedNewX;
    this.core.y = proposedNewY;
    this.core.angle = proposedNewAngle;
  }
...

The code in the above two methods again is very similar and is some basic physics. If the move keys are down it accelerates forward/backwards by the step time and similarly with rotation. You will notice some odd code on the server method on how it sets the position at the end.. this will be clearer why later but for now can be ignored.

Also you see we are using hardcoded values for speed/maxSpeed etc - this will be changed to none harded later once we add in ship modules and begin using the ship spec.

And if you now build and test that.. you should be able to use the W and S keys to move forward and the A and D keys to rotate. If you connect two clients to your server and move on them you should see them move accordingly on the other client.

multi4

At this stage we finally have some kind of multiplayer thing going on. Most of the key components of the server and client are now in place in some capacity.

We will now add in some collision detection, to do our collision detection we will use the following sat package: https://www.npmjs.com/package/sat

npm install sat

We are going to use this to test polygon against polygon. To do this we need to give our objects a shape polygon.

/src/common/game.json

"itemtypes" : [
  {
   "title": "PTU", "type": 3, "id": 6,
     "name" : "PTU",
     "width" : 2,
     "height" : 4,
     "shape" : [
       {"x": 0, "y": -2 },
       {"x": 1, "y": 1 },
       {"x": -1, "y": 1}
     ]     
  } 
],

So you will see we have defined 3 verticies for our ship polygon (which as is shown on the client is a small triangle shape)

First in /src/server/gameObjects/spaceObject.js

...
import { Polygon, Vector, Circle } from 'sat';
...
  getCurrentCollisionBounds() {
    return this.getCollisionBoundsAtPositionRotation(this.core.x, this.core.y, this.core.angle);
  }

  getCollisionBoundsAtPositionRotation(posX, posY, angle) {
    return {
      bounds: this.getObjectPolygonAtPositionRotation(posX, posY, angle),
      type: 'hull'
    }
  }

  getObjectPolygonAtPositionRotation(posX, posY, angle) {
    return new Polygon(new Vector(0, 0) , this.specification.shape.map((vertex) => {
      return new Vector(vertex.x, vertex.y);
    })).rotate(angle * Math.PI/180).translate(posX, posY);
  }
...

In the above we are imported some shape stuff from sat. We then have three methods. The first is ‘getCurrentCollisionBounds’, this gets our ships current polygon based on its position and rotation. ‘getCollisionBoundsAtPositionRotation’ returns a structure containing a polygon proposed at a proposed x,y,angle. Notice it also returns a type as hull, this is because later we are going to have shields on our ship which it may instead collide against. ‘getObjectPolygonAtPositionRotation’ creates the polygon, using the shapes in our specification (game.json itemtype entry for the ship), and then rotates it given our ships present angle.

Now in /src/server/managers/objectManager.js:

...
  import SAT from 'sat';
...
  objectOverlapCheck(x, y, objectID, newPolygonState) {
    let localObjects = this.gm.getWorldObjectsAroundPoint(x,y);

    for (let i=0; i<localObjects.length; i++) {

      if (localObjects[i].objectID !== objectID) {

        var objBounds = localObjects[i].getCurrentCollisionBounds();
        var response = new SAT.Response();
        var collided = false;

        if (newPolygonState.type == 'hull' && objBounds.type == 'hull') {
          collided = SAT.testPolygonPolygon(objBounds.bounds, newPolygonState.bounds, response);
        }

        if (collided) {
          return true;
        }
      }
    }
    return false;
  }
...

In the above we imported sat, in our overlap check we then got all nearby objects, looped over them and tested against every other object whether the polygons collided.

We now need to integrate in our overlapcheck, it will be added in a few places:

In gridManager we need to update the objectPositionChange method so that when a ship moves it checks to see if it can do the movement:

/src/server/managers/gridManager.js

...
  objectPositionChange(object, nX, nY, proposedPoly) {

    if ((object.registeredGrid !== this.getGridKeyOfPosition(nX, nY))) {

      let collisionResult = this.oms.objectOverlapCheck(nX, nY, object.objectID, proposedPoly);
      if (collisionResult) {
        return false;
      }

      ...

    } else {
      // No grid change - just update position

      let collisionResult = this.oms.objectOverlapCheck(nX, nY, object.objectID, proposedPoly);
      if (collisionResult) {
        return false;
      }
      
      object.core.x = nX;
      object.core.y = nY;
    }

    return true;
  }
...

The changes in the above are that we altered the method header so that it also takes a proposedPoly argument. This is the new shape of object (as it may wish to rotate and potentially be in collision by doing so). We then added the overlap check method call in two places, the first in the section with the gridChange, and again at the bottom of the method whereby no gridChange happens.

We’ll now alter the ‘updateObjectPosition’ in /src/server/managers/objectManager.js

...
  updateObjectPosition(object, sX, sY, poly) {
    return this.gm.objectPositionChange(object, parseFloat(sX), parseFloat(sY), poly);
  }
...

And then alter the ‘setPosition’ in /src/server/gameObjects/spaceObject.js

...
  setPosition(sX, sY, poly) {
    return this.zone.oms.updateObjectPosition(this, sX, sY, poly);
  }
...

Next we need to alter our ship step method to make use of this altered setPosition:

In /src/server/gameObjects/ship.js alter the end of the step method to the following:

...
  doStep(dt) {
    ...

    proposedNewX += this.core.speedX * (dt/1000);
    proposedNewY += this.core.speedY * (dt/1000);

    let proposedRotatedShipPoly = this.getCollisionBoundsAtPositionRotation(proposedNewX, proposedNewY, proposedNewAngle);

    if(this.setPosition(proposedNewX, proposedNewY, proposedRotatedShipPoly)){ 
      this.core.x = proposedNewX;
      this.core.y = proposedNewY;
      this.core.angle = proposedNewAngle;
    }
  }
...

This is all slightly mental but should work (setPosition probably shouldnt be the name). On a movement or rotation we generated the new polygon, call setPosition which tests the new polygon, potentially returns true/false. You might notice that in this the x,y will be set twice, once in gridManager and once when it comes back here. That will be sorted later on.

You could test this now.. but you won’t see much going on, because your client won’t know of the collision.

Before adding client side collision detection we are going to use this as an excuse to now add an important feature into our server-client communications. We will make it so that every x seconds the server sends out an update to the clients on the state of the world around it in order to keep things in sync.

First in /src/client/clientWorldEngine.js add the following new method:

...
  fullUpdate(d){
    for (let b = 0, len = d.length; b < len; b++) {
      for(var i = this.worldObjects.length - 1; i >= 0; i--) {
        if(this.worldObjects[i].objectID === d[b].core.objectID) {
          this.worldObjects[i].core = d[b].core;
        }
      }
    }
  }
...

This will take a block of ‘core’ data’s from many objects, and then update them all, which will keep their positions and rotations in sync.

Then in /src/common/constants.js add the FULLUPDATE constant

...
  FULLUPDATE: 'fullupdate'
...

Now in /src/client/UI/gameWorld.js find the ‘inboundMessageParser’ method and add the following case for our fullupdate method:

...
      case constants.FULLUPDATE:
        this.gameWorldEngine.fullUpdate(data.o);
      break;
...

Now on the server in /src/server/zone.js

...
  run(){
    ...
    this.doFullUpdateCheck(currentServerTime);

    this.lastTimestamp = currentServerTime;
  }

  doFullUpdateCheck(){
    if(!this.updateStamp){
      this.updateStamp = new Date().getTime();
    }

    if(new Date().getTime() > this.updateStamp + 10000){

      let gridCache = {};
      let wO = this.oms.gm.getAllWatchers();

      for (let i = 0, len = wO.length; i < len; i++) {
        let objs = this.oms.gm.getWorldObjectsAroundPoint(wO[i].regX, wO[i].regY);
        let objData = [];

        for (let p = 0, len = objs.length; p < len; p++) {
          objData.push(objs[p].getComposition());
        }
        wO[i].notifyOwner(constants.FULLUPDATE, { o: objData });
      }

      this.updateStamp = new Date().getTime();
    }
  }
...

We added a new method which checks to see if 10 seconds has elapsed, if so it gets all watchers on the zone, gets all the objects around that watcher, packs up their “core’s” and then sends them to the watcher. We also altered the run method so that it calls this fullupdatecheck method at the end.

With this in place we can test our server side collision detection is working by getting two clients loaded. Flying one ship into the other.. on the server they will collide and stop. On the client however it will continue on moving until… the fullupdate comes in, snapping the object back to the collision point.

In the following 3 images you can see the before, 1. lining up to ram the other ship, 2. it appearing to have passed through on the client, 3. the fullupdate coming in snapping it back to a position of collision next to it

multi5

With that working we will now implement collision detection on the client

Some of these methods will look similar to those on the server:

/src/client/entities/objects/spaceObject.js

...
import { Polygon, Vector, Circle } from 'sat';
...
  getCurrentCollisionBounds() {
    return this.getCollisionBoundsAtPositionRotation(this.core.x, this.core.y, this.core.angle);
  }

  getCollisionBoundsAtPositionRotation(posX, posY, angle) {
    return {
      bounds: this.getObjectPolygonAtPositionRotation(posX, posY, angle),
      type: 'hull'
    }
  }

  getObjectPolygonAtPositionRotation(posX, posY, angle) {
    return new Polygon(new Vector(0, 0) , this.typeConfig.shape.map((vertex) => {
      return new Vector(vertex.x, vertex.y);
    })).rotate(angle * Math.PI/180).translate(posX, posY);
  }
...

Next in /src/client/clientWorldEngine.js

...
import SAT from 'sat';
...
  objectOverlapCheck(x, y, objectID, newPolygonState) {
    for (let i=0; i < this.worldObjects.length; i++) {
      if (this.worldObjects[i].objectID !== objectID) {
        var objBounds = this.worldObjects[i].getCurrentCollisionBounds();
        var response = new SAT.Response();
        var collided = false;

        if (newPolygonState.type == 'hull' && objBounds.type == 'hull') {
          collided = SAT.testPolygonPolygon(objBounds.bounds, newPolygonState.bounds, response);
        }

        if (collided) {
          return true;
        }
      }
    }
    return false;
  }  
...

This gives us our collision detection check method similar to on the server (for now only testing hulls).

next /src/client/entities/objects/ship.js

...
  doStep(dt, engine) {
    ...
    proposedNewX += this.core.speedX * (dt/1000);
    proposedNewY += this.core.speedY * (dt/1000);

    let proposedRotatedShipPoly = this.getCollisionBoundsAtPositionRotation(proposedNewX, proposedNewY, proposedNewAngle);

    if(!engine.objectOverlapCheck(proposedNewX, proposedNewY, this.objectID, proposedRotatedShipPoly)){ // Attempt to test
      this.core.x = proposedNewX;
      this.core.y = proposedNewY;
      this.core.angle = proposedNewAngle;
    }
  }
...

We altered the doStep so that it is given a reference to the engine, and then before setting the position and rotation tests using that engine. This arg will be used for more things later on.

With this change we need to make a couple of alterations so that the engine passes itself as an arg to step

/src/common/gameWorldEngine.js

...
  doStepsToTime(stepDt){
    for (let i = 0, len = this.objects.length; i < len; i++) {
      this.objects[i].doStep(stepDt, this);
    }
  }
...

Athough it isnt used yet, in these three places /src/server/gameObjects/ship.js and spaceObject.js, and /src/client/entities/objects/spaceObject.js alter the method header for doStep accordingly to take the engine arg also.

...
  doStep(dt, engine) {
...

If you now test the ship ramming scenario again, you will now see that our client no longer passes through the ship but collides keeping sync’d with the server.

There’s a bit more collision detection we’ll do later but for now that is enough.

Link to source files for Part 7

Part 8 - Command System and interface

Currently we can send move and rotation messages to our ship but next we are going to add in the ability to give our ship and surrounding objects commands. This is going to be a kind of command line interface between the player and the world.

We’ll start by create a sendCMD method on our client in /src/client/actions/actions.js

...
export const sendCMD = (socket, cmddata) => {
  console.log('Emitting command...')
  socket.emit(constants.USERCMD, cmddata);
  return (dispatch) => {
    dispatch(commandLogReceived('> '+cmddata));
  }
}
...

In /src/common/constants.js add the new constant

  USERCMD: 'usercmd'

We’ll now add some basic handling of this on the server, In /src/server/cmdShipServer.js

...
  constructor(io) {
    ...
    io.sockets.on('connection', socketioJwt.authorize({
        secret: process.env.JWTSECRET,
        timeout: 15000
      })).on('authenticated', (socket) => {
      ...
      socket.on(constants.USERCMD, (data) => {
        let user = this.userManager.getUser(socket.decoded_token._id);
        if (user !== undefined && user !== null) {
          this.zoneManager.getZone(user.zoneid).onReceivedInput(user, data);
        }
      });

    });

    return this;
  }
...

As before with move, we added the handler for USERCMD message to the end the socket message handlers.

We’ll now implement the new ‘onReceivedInput’ command in /src/server/zone.js:

...
  onReceivedInput(user, data) {
    return new Promise((resolve, reject) => {
      let inputData = data.trim().split(' ');
      if(inputData.length < 1) {
        console.log('invalid command');
        return;
      }

      let userVehicle = this.oms.getWorldObject(user.vehicleid);

      switch (inputData[0]) {
        case 'test':
          logger.info(`Test message recieved`);
          break;

        default:
          break;
      }
    });
  }
...

The above mechanism will form the basis for a lot of our players interaction with the game world.

Now we will go back to the client (Warning: Things may get even more mental in the following section)

Here we are going to build a draggable window system, one of which will be our command line interface.

To do this we are going to use a package called React-dnd - http://react-dnd.github.io/react-dnd/ How I have done this may not be the best way of doing it with react-dnd, but it works.

npm install --save react-dnd
npm install --save react-dnd-html5-backend

You may remember we made /src/client/UI/windowSystem.js - this will be where our windows will be contained.

We’ll have our store keep track of what windows we have, and have a means by which our WindowSystem can ‘register’ (create) a new window.

Our windows will have an id, a type, a title and positioning done with top and left.

In /src/client/actions/actions.js add the following action:

...
export const registerWindow = (id, wintype, title, top, left) => {
  return {
      type: actionTypes.REGISTER_WINDOW,
      id: id,
      wintype: wintype,
      title: title,
      top: top,
      left: left
  }
}
...

Next add the actiontype for that to /src/client/constants/actionTypes.js

...
  REGISTER_WINDOW: 'register_window'
};
...

Next in the reducer we’ll handle that action:

/src/client/reducers/worldReducer.js

...
let initialState = {
...
  windows: []
}
...
export default (state = initialState, action) => {
  let updated = Object.assign({}, state)

  switch (action.type) {
    ...
    case actionTypes.REGISTER_WINDOW:

      var wins = Object.assign([], updated.windows)
      let foundWindow = false;

      for(let i=0; i< wins.length; i++){
        if(wins[i].id == action.id){
          foundWindow = true;
        }
      }

      if(!foundWindow){
        wins.push({
          id: action.id,
          type: action.wintype,
          title: action.title,
          top: action.top,
          left: action.left
        });
      }

      updated['windows'] = wins;

      return updated;
    ...
  }
}
...

We did two things there, we added windows to initialState, we then added the handling of the REGISTER_WINDOW action which just puts the window into our collection.

We are going to need something here called flow for bunging our components together for export

npm install --save flow

With that done we can go back to /src/client/UI/windowSystem.js and change it to the following:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import SocketClient from 'socket.io-client';
import HTML5Backend from 'react-dnd-html5-backend'
import ReactDnD, {DragDropContext} from 'react-dnd';
import {
    DragSource,
    DropTarget,
    ConnectDropTarget,
    DropTargetMonitor,
    XYCoord,
} from 'react-dnd';
import flow from 'lodash/flow';

import InterfaceWindow from './interfaceWindow';

import GameWorld from './gameWorld';
import Authentication from './authentication';
import constants from '../../common/constants.js';
import { registerWindow } from '../actions/actions.js';

const Types = {
  GameWindow: 'gamewindow'
};

class WindowSystem extends Component{
  constructor(props){
    super(props);

    this.props.dispatch(registerWindow('commandline', 'interface', 'commandline', 400, 10))

    this.socket = SocketClient('http://localhost:3000', { reconnection: false, autoConnect: false });
  }

  render(){
    const styles = {
      top: 0,
      left: 0,
      width: window.innerWidth,
      height: window.innerHeight,
      position: 'absolute',
      color: '#000000',
    }
    const { isOver, canDrop, connectDropTarget } = this.props;

    const inGame = (this.props.world.loggedInStatus && this.props.world.connectionStatus !== constants.DISCONNECTED);
    const gameScreen = (<div style={{zIndex:1}}><GameWorld socket={this.socket} userid={this.props.world.userid}/></div>);
    const screen = (inGame) ? gameScreen : <div style={{zIndex: 2}}><Authentication socket={this.props.socket} /></div>;

    return connectDropTarget(
      <div style={styles} id="wr">
        <div style={{zIndex:3}}>

        {this.props.world.windows.map( (item) => {
          if(item.id == 'commandline' && inGame){
            return (
              <InterfaceWindow key="commandline" id="commandline" width="480px" height="200px"
                left={item.left}
                top={item.top}
                title="commandline">

                CONTENT GOES HERE

              </InterfaceWindow>
            )
          }
        })}
        </div>
        { screen }

      </div>
    );

  }

  moveGameWindow(id, left, top) {
    let newWins = Object.assign([], this.props.world.windows)
    for (let i = 0; i < newWins.length; i++) {
      if(newWins[i].id == id){
        newWins[i].left = left
        newWins[i].top = top
      }
    }
  }

}

const gameWindowTarget = {
  drop: ( props, monitor, component ) => {
    if (!component) {
      return
    }

    const item = monitor.getItem()
    const delta = monitor.getDifferenceFromInitialOffset()
    const left = Math.round(item.left + delta.x)
    const top = Math.round(item.top + delta.y)

    component.moveGameWindow(item.id, left, top);
  }
}

function collectDrop(connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
    isOverCurrent: monitor.isOver({ shallow: true }),
    canDrop: monitor.canDrop(),
    itemType: monitor.getItemType()
  };
}

const mapStateToProps = state => {
    return {
        world: state.world
    }
}

export default flow(
    DropTarget(Types.GameWindow, gameWindowTarget, collectDrop),
    connect(mapStateToProps),
    DragDropContext(HTML5Backend),
)(WindowSystem);

If you want to understand entirely whats going on there with the drag n drop code it is probably best to go through the react-dnd docs. You will see how in the constructor we registered a new window, this will go into our store, and then get handled by the render method, creating a new window of type interface (which for now just has placeholder text but will shortly be our command line interface component). You will see flow in action at the end of the file at the export.

An ‘interface window’ is going to be a window which is used by the app for the user to do interfacey stuff, as opposed to a window such as seeing what cargo is in your ship (which will later be an itemWindow).

In /src/client/UI/ create a new file interfaceWindow.js with the following:

import React, { Component } from 'react';
import HTML5Backend from 'react-dnd-html5-backend'
import ReactDnD, {DragDropContext} from 'react-dnd'
import { Container, Col, Row, Button, Form, FormGroup, Label, Input, FormText } from 'reactstrap';
import {
    DragSource,
    DropTarget,
    ConnectDropTarget,
    DropTargetMonitor,
    XYCoord,
    ConnectDragPreview
} from 'react-dnd'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { registerWindow } from '../actions/actions.js';
import flow from 'lodash/flow'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

const Types = {
  GameWindow: 'gamewindow'
};

class InterfaceWindow extends Component {
  constructor(props){
    super(props)

    this.state = {
      minimize: false,
      height: this.props.height,
      visibleHeight: this.props.height
    }
  }

  minimizeWindow(){
    this.setState({
      minimize: !(this.state.minimize),
      visibleHeight: (this.state.minimize) ? this.state.height : '20px'
    })
  }

  render(){

    const boxStyle = {
      background: '#222222',
      border: '1px solid #999',
      borderRadius: '3px',
      width: this.props.width,
      height: this.state.visibleHeight,
      left: this.props.left,
      top: this.props.top,
      position: 'absolute'
    }

    const handleStyle = {
      cursor: 'move'
    }

    const windowControlButtonStyle = {
      backgroundColor: '#3333BB',
      color: '#aaaaaa'
    }

    const titleStyle = {
      width: "100%",
      backgroundColor: '#3333BB',
      color: '#aaaaaa'
    }

    const closeStyle = {
      backgroundColor: '#999999',
      color: '#ffffff',
      display: 'inline-block',
      cursor: 'move'
    }

    const contentDiv = {
      height: "100%"
    }

    const { isDragging, connectDragSource, connectDragPreview } = this.props

    const content = !this.state.minimize ? this.props.children : <div></div>

    const minMaxButton = !this.state.minimize ? (<div><FontAwesomeIcon icon="window-minimize" /></div>) : (<div><FontAwesomeIcon icon="window-maximize" /></div>)

    return (
      connectDragPreview &&
      connectDragSource &&
      connectDragPreview(
        <div style={boxStyle}>
          <div style={{ height: "26px",display: "flex", flexDirection: "row"}}>
            <div>
              {connectDragSource(<div><button style={windowControlButtonStyle}><FontAwesomeIcon icon="arrows-alt" /></button></div>)}
            </div>
            <div>
              <button style={windowControlButtonStyle} onClick={this.minimizeWindow.bind(this)}>
                {minMaxButton}
              </button>
            </div>
            <div style={titleStyle}>
            {'\u00A0'}{this.props.title}

            </div>
          </div>

           <div id="test" style={{height: 'calc(100% - 26px)'}}>{content}</div>
        </div>
      )
    )
  }
}

const gameWindowSource = {
  beginDrag(props) {
    const item = { id: props.id, top: props.top, left: props.left, locationDetails: props.locationDetails };
    return item;
  },

  endDrag(props, monitor, component) {
    if (!monitor.didDrop()) {
      return;
    }

    const item = monitor.getItem();
    const dropResult = monitor.getDropResult();
    const delta = monitor.getDifferenceFromInitialOffset()
  }
};

function collectDrag(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging(),
    connectDragPreview: connect.dragPreview()
  };
}

const mapStateToProps = state => {
    return {
        world: state.world
    }
}

export default flow(
  connect(mapStateToProps),
  DragSource(Types.GameWindow, gameWindowSource, collectDrag)
)(InterfaceWindow);

Most of that is straight forward, it gives some basic window functionality such as a move drag button and minimise and maximise, and sticks a title on it.

You’ll notice we used some icons in there for our window such as minimize/maximize, we need to now add these in:

npm install --save @fortawesome/fontawesome-svg-core
npm install --save @fortawesome/free-solid-svg-icons
npm install --save @fortawesome/react-fontawesome

To use these in /src/client/app.js we need to add the following:

...
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faWindowMinimize, faWindowMaximize, faWindowClose, faArrowsAlt } from '@fortawesome/free-solid-svg-icons'
...
library.add(faWindowMaximize);
library.add(faWindowMinimize);
library.add(faWindowClose);
library.add(faArrowsAlt);

export default class App extends Component {
...

If you build and run the above you should have the following:

windows1

The first image shows our new interface window, the second after the minimise has been clicked, and third it being dragged.

Lets now build a command line interface to go into this:

In /src/client/UI/ create the new file commandLineInterface.js

import React, { Component } from 'react';
import { connect } from 'react-redux'

import {
  sendCMD
} from '../actions/actions.js';

class CommandLineInterface extends Component {

  constructor () {
    super();

    this.state = {
      cmd: ''
    }
    this.commands = React.createRef();
  }

  handleCommandLineChange(e) {
    this.setState({cmd: e.target.value});
  }

  processCommand(){
    if (this.state.cmd.trim().length > 0) {
      this.props.dispatch(sendCMD(this.props.socket, this.state.cmd.trim()));
      this.setState({cmd: ''});
    }
  }

  handleEnterKeyCheck(e) {
    if (e.key === 'Enter') {
      this.processCommand();
    }
  }

  componentDidUpdate(){
  }

  render () {
    const commandLine = (
      <div style={{ display: "flex", flexDirection: "column", height:'100%'}}>
        <div style={{ height:'25px',display: "flex", flexDirection: "row"}}>
          <input type="text" style={{width: "90%", background: '#000000', color: 'white'}} name="cmd" value={this.state.cmd} onChange={this.handleCommandLineChange.bind(this)}  onKeyPress={this.handleEnterKeyCheck.bind(this)}/>
          <button style={{width: "10%"}} onClick={this.processCommand.bind(this)}>Send</button>
        </div>

        <textarea ref={this.commands}
          style={
            {
            background: '#000000',
            resize: "none",
            overflow: "hidden",
            width: "100%",
            flexGrow: "1",
            lineHeight: "13px",
            fontSize: "13px",
            textColor: 'white',
            color: 'white',
            overflowY: 'scroll'
            }
          } value={this.props.world.commandLog}>
        </textarea>
      </div>
    );

    return commandLine;
  }
}

const mapStateToProps = state => {
    return {
        world: state.world
    }
}

export default connect(mapStateToProps)(CommandLineInterface)

This is just a textarea which displays our earlier created commandLog, an input, the content of which is sent to the server via our sendcmd action we made.

Back in /src/client/UI/windowSystem.js we can now add this in:

...
import CommandLineInterface from './commandLineInterface';
...
              <InterfaceWindow key="commandline" id="commandline" width="480px" height="200px"
                left={item.left}
                top={item.top}
                title="commandline">

                <CommandLineInterface socket={this.socket}/>

              </InterfaceWindow>
...              

You will see we imported the commandline component and then switched our placeholder text for the component, passing in the socket.

Building and running that, gives us our commandlog and and sending the test message will show up recieved on the server.

windows2

Link to source files for Part 8

Part 9 - Camera movement and Starfield

The next change is going to be pinning the camera onto the users vehicle.

In /src/client/UI/gameWorld.js make the following changes:

  update (ts, dt) {
    ...
    this.updateCamera();

    this.clear(cam);
    this.draw(cam);
  }

  updateCamera () {
    if(this.gameWorldEngine.playerShip !== null){
      cam.x = this.gameWorldEngine.playerShip.core.x - (cam.width/2);
      cam.y = this.gameWorldEngine.playerShip.core.y - (cam.height/2);
    }
  }

If you now run this the player will be at the centre of the screen, but you will notice an issue, with no other objects around the ship it becomes impossible to know if it is moving. To resolve this issue we will create a starfield in the background:

In /src/client/UI/gameWorld.js make the following changes:

...
let starField = [];
const STARFIELD_WIDTH = 2000;
const STARFIELD_HEIGHT = 2000;
...
class GameWorld extends Component {
  constructor () {
    ...
    this.createStarField();
  }

  createStarField(){
    var stars = [];

    for(var i=0; i< 1000; i++) {
      stars[i] = {
          x: Math.random()*STARFIELD_WIDTH,
          y: Math.random()*STARFIELD_HEIGHT,
          m: Math.random()*1+1
      }
    }

    starField = stars;
  }

  drawStarfield (observationPoint) {
    let xStart = observationPoint.x % STARFIELD_WIDTH;
    let yStart = observationPoint.y % STARFIELD_HEIGHT;
    let xEnd = (observationPoint.x + observationPoint.width) % STARFIELD_WIDTH;
    let yEnd = (observationPoint.y + observationPoint.height) % STARFIELD_HEIGHT;

    context.fillStyle = '#333333';
    for(var i=0; i<starField.length;i++) {
      var star = starField[i];

      let xP,yP;

      if(xStart>=0){
        xP = star.x - xStart;

        if(xP < 0){
          xP = STARFIELD_WIDTH - Math.abs(xP);
        }

      }else{
        xP = star.x - xStart;

        if(xP > STARFIELD_WIDTH){
          xP = xP - STARFIELD_WIDTH;
        }
      }

      if(yStart>=0){
        yP = star.y - yStart;

        if(yP < 0){
          yP = STARFIELD_HEIGHT - Math.abs(yP);
        }

      }else{
        yP = star.y - yStart;

        if(yP > STARFIELD_HEIGHT){
          yP = yP - STARFIELD_HEIGHT;
        }
      }

      if( (xP > 0) && (yP > 0) && (xP < observationPoint.width) && (yP < observationPoint.height) ){
        context.fillRect(xP, yP, star.m, star.m);
      }
    }
  }
...
  update (ts, dt) {
    ...
    this.clear(cam);
    this.drawStarfield(cam);
    this.draw(cam);
  }
...  

So in the constructor we generated a 2000x2000 ‘starfield’, randomly positioned dots of varying magnitude. In our update method we call drawStarfield which wraps this starfield around the screen as the user moves.

Running that you should see something like the following:

starfield

Link to source files for Part 10

Part 10 - New vehicles, board, unboarding

At this stage we need to now expand the types of objects in our game world.

The first one is going to be a space ship, instead of just the basic PTU. We’ll then do board/unboarding of vehicles.

In /src/common/game.json add the following:

...
"itemcategories": {
 "2" : { "name": "Ships", "parentid": 0, "base": 1},
 "3" : { "name": "PTU", "parentid": 2, "hidden":1 },
 "6" : { "name": "Utility Ship", "parentid": 2 }
},
"itemtypes" : [
  {
   "title": "PTU", "type": 3, "id": 6,
     "name" : "PTU",
     "width" : 2,
     "height" : 4,
     "shape" : [
       {"x": 0, "y": -2 },
       {"x": 1, "y": 1 },
       {"x": -1, "y": 1}
     ]     
  },  
  {
     "title": "Utility Vessel Type A",
     "type": 6,
     "id": 7,
     "size": 500000,
     "name" : "Utility Vessel Type A",
     "width" : 5,
     "height" : 7,
     "shape" : [
       {"x": -4, "y": -3 },
       {"x": 4, "y": -3 },
       {"x": 4, "y": 3},
       {"x": -4, "y": 3}
     ]
  }  
],
...

We will add some temporary code on the client for handling this new ship so we can see the difference:

First add a new constant in /src/common/constants.js

...
  SHIP_PTU: 6,
  SHIP_UTILITY: 7
...

Now in /src/client/entities/objects/ship.js alter the end of the draw method (after the beginPath call) to the following:

...
  draw (context, cam) {
    ...
    if(this.typeID == constants.SHIP_UTILITY){
      context.moveTo(-(w / 2), - (h / 2))
      context.lineTo((w / 2), - (h / 2))
      context.lineTo((w / 2), (h / 2))
      context.lineTo(-(w / 2), (h / 2))
    } else {
      context.moveTo(0, - (h / 2))
      context.lineTo(0 - (w / 2), (h / 2))
      context.lineTo((w / 2), (h / 2))
      context.lineTo(0, 0 - (h / 2))
    }
    context.closePath()
    context.stroke()

    context.restore()
  }
...

Next in /src/client/clientWorldEngine.js find the addObject case statement for type 6 and change to the following:

...
      case constants.SHIP_PTU:
      case constants.SHIP_UTILITY:
        obj = new Ship(
          itemConfig,
          composition );
        break;
...

To test this I will now create a temporary debug method on the server which we can call via our commandline interface to spawn world objects:

in /src/server/managers/objectManager.js we need some new methods - add the following two:

...
  instantiateShip(objectID) {
    return new Promise((resolve, reject) => {
      let qi = this.constructor.loadShipFromID(objectID).then( (qResult)=> {
        return this.instantiateShipFromRecord(qResult);
      }).then((ship) => {
        resolve(ship);
      }).catch((e)=>{
        logger.error(e);
        reject(e);
      });
    });
  }

  spawnShipAtPosition(objectID, x, y) {
    this.instantiateShip(objectID).then((ship) => {
      ship.initShipBasedOnState();  // Sets any initial state
      return this.getSafePositionForObject(x, y, ship, 15, null);
    }).then((result) => {
      if (!result.result) {
        return;
      }
      result.object.core.x = result.x;
      result.object.core.y = result.y;
      this.addObjectToWorld(result.object);
    });
  }
...

The first method uses loadShipFromID and instantiates a new ship object. The second does similar except also sets the objects position and adds it into the world. You’ll notice the second calls methods that don’t yet exist ‘initShipBasedOnState’ and ‘getSafePositionForObject’.

We are going to take a bit of a detour here to add these in. The first is simple and for now will be empty:

/src/server/gameObjects/ship.js

...
initShipBasedOnState(){
  
}
...

For now that empty but we will expand it shortly, its purpose is to setup the ship depending on its last ‘position state’ as it may have been doing something when it removed from the world. This ship state is stored in a property that you may have already seen ‘positionState’ which we added to the schema at he start. Position state may be the wrong word to have called it, it really is just ‘the state of the object’.

Before going further we’ll add a constant for one type of positionState:

/src/common/constants.js

...
  OBJECT_POSITION_STATES: {
    STANDARD_SPACE: 3
  },
...

Standard space means its just in a normal state.

The second method we’ll add is more mental and involves another detour:

/src/server/managers/objectManager.js

...
import { Polygon, Vector, Circle } from 'sat';
...
  getSafePositionForObject(x, y, object, minimumDistance, bounds) {
    return new Promise((resolve, reject) => {
      let tries = 0;
      let maxTries = 3000;
      let diameter = (object.width > object.height) ? object.width : object.height;

      if (diameter < minimumDistance) {
        diameter = minimumDistance;
      }

      let tryDistance = diameter;
      let tryAngle = 0;
      let tryPosition = { x, y };

      // Iterate around to find an area that passes
      while (tries < maxTries) {
        //create a sphere
        let launchArea = new Circle(new Vector(tryPosition.x, tryPosition.y), Math.round(diameter));

        let freeSpace = this.testSphereNonCollidesOnWorldObjects(launchArea, x, y, null);

        let boundsCheck = true;
        if(bounds !== null){
          boundsCheck = false;

          let bResponse = new SAT.Response();
          let bCollided = SAT.testPolygonCircle(bounds, launchArea, bResponse);

          if(bCollided){
            boundsCheck = true;
          }
        }

        if (freeSpace && boundsCheck) {
          return resolve({ result: true, object: object, x: tryPosition.x, y: tryPosition.y });
        }

        tryPosition.x = parseFloat(x) + Math.cos(tryAngle * Math.PI/180) * tryDistance;
        tryPosition.y = parseFloat(y) +  Math.sin(tryAngle * Math.PI/180) * tryDistance;

        tryAngle += 5;

        if (tryAngle > 360) {
          tryDistance += diameter;
          tryAngle = 0;
        }

        tries += 1;
      }

      resolve( {result: false } );
    });
  }

  testSphereNonCollidesOnWorldObjects(sphere, x , y, excludeID) {
    let localObjects = this.gm.getWorldObjectsAroundPoint(x, y);

    for (let i=0; i<localObjects.length; i++) {
      if (excludeID && localObjects[i].objectID == excludeID) {
        continue;
      }
      //Test our polygon/position/state against it
      let objBounds = localObjects[i].getCurrentCollisionBounds();
      let response = new SAT.Response();
      let collided = false;

      collided = SAT.testPolygonCircle(objBounds.bounds, sphere, response);

      if (collided) {
        return false;
      }
    }

    return true;
  }
...

There’s two added methods. The purpose of the ‘getSafePositionForObject’ is that given a point and an object, and it attempts to find the nearest empty space that fits this object (As obviously we don’t want things spawning ontop of other things). There are probably far better ways of doing that than the way i did it using my rogue maths. The second method is a method for testing the sphere shaped area for emptiness.

Here there’s going to be another detour.. we want to implement this in other places - anywhere we are loading an object into the world such as when our ship loads and we’ll also add in the new initState method:

In /src/server/zone.js we will edit the loadUserIntoZone method to use our new methods:

...
  loadUserIntoZone(user) {
    if(this.userIsRegistered(user)){
      logger.error('Attempted to load a user already registered ', user.username);
      return;
    }

    this.registerUserToZone(user);

    this.oms.instantiateShip(user.vehicleid).then((loadedShip) => {
        return this.oms.getSafePositionForObject(loadedShip.core.x, loadedShip.core.y, loadedShip, 15, null);
      }).then((result) => {
        if (!result.result) {
          return;
        }
        result.object.core.x = result.x;
        result.object.core.y = result.y;

        logger.info('A ship has been spawned and loaded... firing initstate');
        result.object.initShipBasedOnState();  // Sets any initial state
    
        // Assign user to vehicle
        result.object.setDockedUser(user);

        // Load the user in at the point we will load in at
        logger.info('User is loaded in as a watcher at ship position');
        this.oms.userWatcherJoin(user, result.object.core.x, result.object.core.y);

        logger.info('Adding object to world');

        this.oms.addObjectToWorld(result.object); // Add to object collection, grids and send adds

    });
  }
...

So now when the users ship attempts to load at the logged off position, it will check its safe and if not find a nearby place that is.

With that done, again in /src/server/zone.js change ‘onReceivedInput’ to the following:

...
  onReceivedInput(user, data) {
    return new Promise((resolve, reject) => {
      let inputData = data.trim().split(' ');
      if(inputData.length < 1) {
        console.log('invalid command');
        return;
      }

      let userVehicle = this.oms.getWorldObject(user.vehicleid);

      switch (inputData[0]) {
        case 'spawn':

          let lX = userVehicle.core.x + Math.floor(Math.random() * 20);
          let lY = userVehicle.core.y + Math.floor(Math.random() * 20);

          let cs = ObjectManager.createShip(
            constants.SHIP_UTILITY,
            {
              positionState: constants.OBJECT_POSITION_STATES.STANDARD_SPACE,
              positionStateParams: {},
              lastX: lX,
              lastY: lY,
              angle: userVehicle.core.angle,
              zoneID: this.zoneid,
              dockedBaseID: '',
              dockedUserGarageID: '',
              dockedUserID: ''
          }).then((shipResult) => {
            let newShip = this.oms.spawnShipAtPosition(shipResult._id, lX, lY);
            return resolve();
          });

        break;
        default:
          break;
      }
    });
  }
...

Building and running that, and then entering ‘spawn’ in our command line should give us this:

spawn

A new ship appearing alongside our PTU.

Shortly we will work out how we can get into that ship.. but we have some things to sort out first:

We are going to expand the state system on the ship, using this we are going to add an important feature into our game. When someone logs off, we don’t want their ship disappearing immediatly. If it does, somebody could escape from being killed when surrounded by enemies, just be closing the browser. Instead we want a ‘logoff timer’, whereby when someone disconnects, the ship stays in space that amount of time before disappearing. If someone logs back on before the timer expires it cancels the timer, else the ship is removed.

Aswell as this we will have a ‘logon timer’, when a person logs on a timer begins during which time they are in a state other than STANDARD. This will allow us to perhaps make them invunerable/unable to move when they first logon.

To begin implementing this we’ll add some settings in our /src/common/game.json

{
"settings": {
  "logoffTimer" : 15000,
  "loadingTimer" : 10000,  
  "gridWidth": 1500,
  "gridHeight": 750
},
...

Next in /src/common/constants.js add some new states into the object_position_states constant:

  OBJECT_POSITION_STATES: {
    LOADING: 1,
    LOGGING_OFF: 2,
    STANDARD_SPACE: 3,
    OFFLINE: 8
  }

/src/server/gameObjects/spaceObject.js

...
  constructor(zone, objectState, objectSpecification) {
    ...
    this.positionState = objectState.positionState;
    this.positionStateParams = (objectState.positionStateParams) ? objectState.positionStateParams : {};
    this.queuedLogoffStateChange = false;
    ...
  }
  ...
  changePositionState(positionState, stateParams) {
    if (positionState === constants.OBJECT_POSITION_STATES.STANDARD_SPACE && this.queuedLogoffStateChange) {
      logger.info('Initiating post transit queued logoff');
      this.queuedLogoffStateChange = false;
      this.changePositionState(constants.OBJECT_POSITION_STATES.LOGGING_OFF,
        { zoneid: this.zone.zoneid, timerStart: new Date().getTime(), duration: 15000 });
      return;
    }

    this.positionState = positionState;
    this.positionStateParams = stateParams;

    this.setPersistanceMark(true);
  }
...

Next in /src/server/gameObjects/ship.js we will change our state init method, and add some new methods:

...
  update(ts, dt) {
    this.handleStateUpdate(ts);
    ...
  }

  handleStateUpdate(st) {
    switch(this.positionState) {
      case constants.OBJECT_POSITION_STATES.LOADING:
        // Continue waiting on timer
        if (st > this.positionStateParams.timerStart + ConfigurationManager.getSetting('loadingTimer')) {
          // Fire off an action to allow normal operations
          logger.info('Loading timer expired. Changing state to STANDARD_SPACE');
          this.changePositionState(constants.OBJECT_POSITION_STATES.STANDARD_SPACE, { zoneid: this.zone.zoneid });
        }
        break;
      case constants.OBJECT_POSITION_STATES.LOGGING_OFF:
        // User has logged off
        if (st >  this.positionStateParams.timerStart + ConfigurationManager.getSetting('logoffTimer')) {
          // Fire an action to unload the vehicle
          logger.info('Log off timer expired. Changing state to OFFLINE. deloading object');
          this.zone.oms.removeObjectFromWorld(this, {});
          this.unload();
          this.changePositionState(constants.OBJECT_POSITION_STATES.OFFLINE, { zoneid: this.zone.zoneid });

        }

        break;
      default:
        break;
    }
  }

  beginLogOff() {
    this.changePositionState(constants.OBJECT_POSITION_STATES.LOGGING_OFF,
      { zoneid: this.zone.zoneid, timerStart: new Date().getTime(), duration: 15000 });
  }

  beginLoading() {
    this.changePositionState(constants.OBJECT_POSITION_STATES.LOADING,
      { zoneid: this.zone.zoneid, timerStart: new Date().getTime(), duration: 10000 });
  }

  initShipBasedOnState(){
    this.queuedLogoffStateChange = false; 

    switch(this.positionState){
      case constants.OBJECT_POSITION_STATES.LOGGING_OFF:
        // User has logged back on before its vehicle vanished
        logger.info('Setting state from LOGGING OFF to STANDARD_SPACE');
        // Switch the state to IN_SPACE
        this.changePositionState(constants.OBJECT_POSITION_STATES.STANDARD_SPACE, { zoneid: this.zone.zoneid })

        break;
      case constants.OBJECT_POSITION_STATES.OFFLINE:
        logger.info('Setting state from OFFLINE to LOADING with timer set');
        this.beginLoading();

        break;
    }
  }
...
  init(){
    return new Promise((resolve, reject) => {
      this.initShipBasedOnState();
      resolve();
    });
  }
...

Don’t worry yet what ‘queuedLogoffStateChange’ does. This is needed shortly for times when we cannot begin the logoffTimer, such as if the vehicle is moving between zones. Instead it will start once it lands at its destination and that code (yet to be written) will make use of this variable.

Now.. we need to alter our zone code so that it uses this new mechanism:

...
  unloadUserFromZone(user) { 
    logger.info('Unloading user from zone... ');
    this.removeUserRegistration(user.userid);

    this.oms.userWatcherLeave(user);

    logger.info(`User vehicle begin unload called: ${user.vehicleid}`);

    let userVehicle = this.oms.getWorldObject(user.vehicleid);
    if(userVehicle !== null){
      userVehicle.beginLogOff();
    }
  }
...

Instead of removing the object from the zone, it now invokes beginLogoff.

There is one other thing we need to do here, if the user logs back on before their ship unload, we need to cancel the unload and put them back in the ship. In the loadUserIntoZone alter it to the following:

...
  loadUserIntoZone(user) {
    if(this.userIsRegistered(user)){
      logger.error('Attempted to load a user already registered ', user.username);
      return;
    }

    this.registerUserToZone(user);

    let existingShip = this.oms.getWorldObject(user.vehicleid);
    if (existingShip == null ) {
      this.oms.instantiateShip(user.vehicleid).then((loadedShip) => {
          return this.oms.getSafePositionForObject(loadedShip.core.x, loadedShip.core.y, loadedShip, 15, null);
        }).then((result) => {
          if (!result.result) {
            return;
          }
          result.object.core.x = result.x;
          result.object.core.y = result.y;

          logger.info('A ship has been spawned and loaded... firing initstate');
          // Flow out
          result.object.initShipBasedOnState();  // Sets any initial state
      
          // Assign user to vehicle
          result.object.setDockedUser(user);

          // Load the user in at the point we will load in at
          logger.info('User is loaded in as a watcher at ship position');
          this.oms.userWatcherJoin(user, result.object.core.x, result.object.core.y);

          logger.info('Adding object to world');

          this.oms.addObjectToWorld(result.object); // Add to object collection, grids and send adds
      });
    } else {
      logger.info('Users vehicle still in zone... firing initstate');
      existingShip.initShipBasedOnState(); 

      logger.info('User is loaded in as a watcher at existing ship position');
      this.oms.userWatcherJoin(user, existingShip.core.x, existingShip.core.y);
    }
  }
...

So you will see that now the method looks to see if the users vehicle is present in the world, if it isn’t it runs the same code as before. However if the ship is still in the world, we don’t make a new one and instead dock the user with the existing.

If we build and start this. Connect a client to the server and wait 10 seconds you should see the following:

info: Initializing game server... {"timestamp":"2018-12-25 10:54:32 AM +0000"}
info: Game Server init began... {"timestamp":"2018-12-25 10:54:32 AM +0000"}
info: Init complete. Starting game server process... {"timestamp":"2018-12-25 10:54:32 AM +0000"}
started...
info: Client established connection and authenticated. zf5r06FVDYeD8USCAAAA test9. Connected clients: 1 {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: test9 invoked handshake. {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: test9 invoked launched. {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Attempting to load user 5c21a79ea694554d1b166f92  {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Game: User loaded 5c21a79ea694554d1b166f92 {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Setting state from OFFLINE to LOADING with timer set {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: A ship has been spawned and loaded... firing initstate {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: User is loaded in as a watcher at ship position {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Adding object to world {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Adding object to world: 5c21a79ea694554d1b166f93 {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Adding object to zone at: 430 83 {"timestamp":"2018-12-25 10:54:35 AM +0000"}
(node:20501) DeprecationWarning: collection.findAndModify is deprecated. Use findOneAndUpdate, findOneAndReplace or findOneAndDelete instead.
info: Ship object persistance complete. {"timestamp":"2018-12-25 10:54:35 AM +0000"}
info: Loading timer expired. Changing state to STANDARD_SPACE {"timestamp":"2018-12-25 10:54:45 AM +0000"}

Initially the state was moved from OFFLINE to LOADING. Then once the timer expired the state was changed to STANDARD_SPACE. I then disconnected the client.. waited until the logoff timer expired and got as follows:

info: Ship object persistance complete. {"timestamp":"2018-12-25 10:54:45 AM +0000"}
info: test9 disconnected. {"timestamp":"2018-12-25 10:54:56 AM +0000"}
info: Game: User exiting 5c21a79ea694554d1b166f92 {"timestamp":"2018-12-25 10:54:56 AM +0000"}
info: Unloading user 5c21a79ea694554d1b166f92 from UserManager {"timestamp":"2018-12-25 10:54:56 AM +0000"}
userzoneid:  1
info: Unloading user from zone...  {"timestamp":"2018-12-25 10:54:56 AM +0000"}
info: User vehicle begin unload called: 5c21a79ea694554d1b166f93 {"timestamp":"2018-12-25 10:54:56 AM +0000"}
info: Ship object persistance complete. {"timestamp":"2018-12-25 10:54:56 AM +0000"}
info: Log off timer expired. Changing state to OFFLINE. deloading object {"timestamp":"2018-12-25 10:55:11 AM +0000"}
info: Removing object from world:: 5c21a79ea694554d1b166f93 {"timestamp":"2018-12-25 10:55:11 AM +0000"}
info: Found and removed {"timestamp":"2018-12-25 10:55:11 AM +0000"}
info: Ship object persistance complete. {"timestamp":"2018-12-25 10:55:11 AM +0000"}

Right.. next part given that we can spawn ships that are empty. We need these to stay in the world and persist. The only ships that should logoff/logon are those that have players in them. Empty ships will always be present if the zone is up. To do this, when a zone loads we want to load in all ships that do not have a player in them.

We’ll alter our init method on zone to fire a loadObjects method, which must complete before the init promise resolves.

/src/server/zone.js

...
  init() {
    return new Promise((resolve, reject) => {
      this.loadObjects().then((result) => {
        resolve();
      }).catch((e) => {
        console.log(e);
        reject();
      });
    });
  }

  loadObjects() { 
    return this.oms.loadEmptyShips();
  }
...  

The loadObjects method will shortly load other things such as structures but for now it will just do empty ships.

/src/server/managers/objectManager.js

...
import WorldObjectModel from '../model/objects/worldObject';
...
  static queryWorldObjects(spec) {
    return new Promise((resolve, reject) => {
      WorldObjectModel.find(spec).lean().then((results) => {
        return resolve(results);
      }).catch((e) => {
        logger.error(e);
        reject(e);
      });
    });
  }
...
  loadEmptyShips() {
    return new Promise((resolve, reject) => {
      this.constructor.queryWorldObjects({ itemtype: 'Ship', dockedUserID: '', dockedBaseID: '', zoneID: this.zone.zoneid, status: 1 }).then( (qResult)=> {
        logger.info(`${qResult.length} empty ships found in zone: ${this.zone.zoneid}`);

        let shipPromises = [];

        for (let i = 0, len = qResult.length; i < len; i++) {
          shipPromises.push(this.instantiateShipFromRecord(qResult[i]).then((aShip) => {
            this.addObjectToWorld(aShip);
            logger.info('Empty ship loaded');
          }));
        }

        return Promise.all(shipPromises).then(() => {
          return resolve();
        }).catch((e) => {
          console.log(e);
          logger.error('Error initializing empty ships');
        });

      }).catch((e) => {
        logger.error(e);
        reject(e);
      });
    });
  }
...  

So in the above I created a new method for querying all world objects ‘queryWorldObjects’. loadEmptyShips then uses this (You may see that this.constructor.queryWorldObjects - that it seems is the best way of calling a static method from within an instance). It queries all ships that have empty ‘dockedUserID’ (no docked user), and empty ‘dockedBaseID’ (not stored within a structure), it then created an empty array which will hold promises, then it iterates over the ships, pushing into the promises array the returned promise from the ship init. Once complete we then do Promises.all, which lets us not resolve until all ship init promises in the array have done their thing.

If you build and run this, if you messed around with the spawn a lot prior to this you will get something like the following when starting up the server:

Listening on 3000
(node:22534) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(node:22534) DeprecationWarning: collection.ensureIndex is deprecated. Use createIndexes instead.
info: Connected to MongoDB server successfully. {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Initializing game server... {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Game Server init began... {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: 5 empty ships found in zone: 1 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to world: 5c21ec3f538c93259b94b705 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to zone at: 137.93499663481882 57.99978098309639 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Empty ship loaded {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to world: 5c21eca50ebef127104955f4 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to zone at: 144.96352553816862 58.99931601420991 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Empty ship loaded {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to world: 5c21ed115073ac28fed5cf8f {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to zone at: 147.04130923866418 61.998859475813155 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Empty ship loaded {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to world: 5c21ed5910246e2a1595d8f6 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to zone at: 147.00584602668002 66.99931574214563 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Empty ship loaded {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to world: 5c21ffe56367a643e8b33b5f {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Adding object to zone at: 433.9710320074374 99.99952035690868 {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Empty ship loaded {"timestamp":"2018-12-25 11:27:13 AM +0000"}
info: Init complete. Starting game server process... {"timestamp":"2018-12-25 11:27:13 AM +0000"}
started...

If you then connect a client, you should see your ships all loaded in from earlier.

emptyships

We are getting closer to being able to write the code for board/unboard vehicle, but we still have more things we need.

Next up is the ship composition/specification data that is sent out.

You will have see we currently send out the ‘public composition’, this is meant for everyone nearby regardless of whos ship/object it is. It contains core information that a client needs to render the vehicle. We are going to add a new type of ‘composition data’ called ‘private composition’. This is going to be data that only some people know, such as the person flying the ship. This might contain information that other players don’t know such as how much fuel your ship has, how badly it is damaged internally etc etc, it is private info for only those who should know it.

Aswell as adding private composition , we are going to add in a ‘state change’ messages for both public or private composition. Using these if the state (public or private) alters, those who should know can be informed. An example of this is our ‘positionState’ variable on whether the user is logging off or loading, we want to inform those around when this happens (fullUpdate does send some but only core - which is for positional/movement data).

First we will add a private composition method to /src/server/gameObjects/ship.js, and also alter the getComposition():

...
  getComposition(){
    return {
      core: this.core,    
      stateSpec: {
        dockedUser: (this.dockedUserID !== null) ? this.dockedUserID : '',
        dockedName: this.dockedName,
        vehicleStatus: this.positionState,
        title: this.title,
        general: {}
      }
    };
  }

  getPrivateComposition(){
    let comp = {
      objectID: this.objectID,
      stateSpec: {
        general: {
          privateTestData: 1
        }
      }
    }
    return comp;
  }
...

You will see we modified the public composition so that positionState is now sent in public, and we have a private composition method (which for now has nothing special).

The above get sent when a client is notified of an object, but there may be updates, we will add the constants for both these ‘composition update’ messages - /src/common/constants.js, and also the initial private composition

...
  OBJECT_SPEC_UPDATE: 'object_spec_update',
  OBJECT_PRIVATE_SPEC_UPDATE: 'object_private_spec_update',
  SHIP_DETAILS: 'ship_details'
...

SHIP_DETAILS is the private composition message which is sent to the occupant. OBJECT_PRIVATE_SPEC_UPDATE is when there is a change in some kind of private ship data OBJECT_SPEC_UPDATE is for public changes to a ship that the client is aware of

On the client we will now handle these by adding the following cases to ‘inboundMessageParser’ in /src/client/UI/gameWorld.js:

...
      case constants.OBJECT_SPEC_UPDATE:
        this.gameWorldEngine.objectSpecUpdate(data.objectID, data.data);
      break;

      case constants.OBJECT_PRIVATE_SPEC_UPDATE:
        this.gameWorldEngine.objectPrivateSpecUpdate(data.objectID, data.data);
      break;

      case constants.SHIP_DETAILS:
        this.gameWorldEngine.setShipDetails(data.objectID, data.stats);
      break;
...

In /src/client/clientWorldEngine.js we’ll add these new methods:

...
  objectSpecUpdate(objectID, data) {
    for (let i = 0; i < this.worldObjects.length; i++) {
      if(this.worldObjects[i].objectID == objectID){
        let nts = this.server_timestamp + (new Date().getTime() - this.client_timestamp);

        switch(data.cmd){
          case 'status':
            console.log('Ship status change: ',data.status)  
          break;
          case 'uc':
            this.worldObjects[i].spec.dockedUser = data.uID;
            this.worldObjects[i].spec.dockedName = data.dN;

            if(data.uID){
              if(data.uID == this.playerUserID){
                this.playerShip = this.worldObjects[i];
                console.log('setting player to new vehicle');
              }
            }
          break;
          case 'props':
            data.m.forEach((element) => {
              this.setDeepVal(this.worldObjects[i].spec, element.name, element.value);
            });
          break;           
        }        
      }
    }
  }

  objectPrivateSpecUpdate(objectID, data) {
  }
  
  setShipDetails(objectID, stats) {
    console.log('Ship details received');
  }

  setDeepVal(obj, path, value){
    obj = typeof obj === 'object' ? obj : {};
    let keys = Array.isArray(path) ? path : path.split('.');
    let curStep = obj;

    for (let i = 0; i < keys.length - 1; i++) {
      let key = keys[i];
      if (!curStep[key] && !Object.prototype.hasOwnProperty.call(curStep, key)){
        let nextKey = keys[i+1];
        let useArray = /^\+?(0|[1-9]\d*)$/.test(nextKey);
        curStep[key] = useArray ? [] : {};
      }
      curStep = curStep[key];
    }
    let finalStep = keys[keys.length - 1];
    curStep[finalStep] = value;
  };
...

For now only the ‘objectSpecUpdate’ method has anything of significance in it. We handle three potential messages, the status change such as ‘LOADING/LOGGING_OFF/STANDARD_SPACE’ and ‘uc’ - which is what we will send to clients when the occupant of a ship changes. Then there is “props”, this is a means by which we can updated a list of object properties such as “ship.thrusters.status” to a value. That method ‘setDeepVal’ is something I digged out of stackoverflow once which will update an object given the dot notation for it.

We don’t yet however have a means for sending out ‘props’ messages on the server, but that will come later.

Now we will switch back to the server, and put in the code for sending out the status change in /src/server/gameObjects/spaceObject.js

...
  changePositionState(positionState, stateParams) {
    ...
    this.positionState = positionState;
    this.positionStateParams = stateParams;

    this.zone.oms.broadcastInRangeOfObject(this,
    constants.OBJECT_SPEC_UPDATE,
    {
      objectID: this.objectID,
      data: {
        cmd: 'status',
        status: positionState,
      }
    });

    this.setPersistanceMark(true);
  }
...

When this is built and run you should see the client logging status changes on vehicles to the console.

Now in /src/server/gameObjects/boardableEntity.js we are going to alter ‘setOccupancyData’ and ‘setDockedUser’, and add a new method for setting a vehicle to vacant (which we will use when unboarding)

...
  setOccupancyData(dockedUserID, dockedName, broadcast){
    this.dockedUserID = dockedUserID;
    this.dockedName = dockedName;
    this.setPersistanceMark(true);

    if(broadcast){
      this.zone.oms.broadcastInRange(
        this.core.x,
        this.core.y,
        constants.OBJECT_SPEC_UPDATE,
        {
          objectID: this.objectID,
          data: {
            cmd: 'uc',
            uID: (this.dockedUserID == null) ? '' : this.dockedUserID,
            dN: this.dockedName          }
      });
    }    
  }

  setDockedUser(user, broadcast) {
    this.setOccupancyData(user.userid, user.username, broadcast);    
    return true;
  }

  setVehicleVacant(broadcast) {
    this.setOccupancyData(null, '', broadcast);
    return true;
  }
...

You will see the method header on setDockedUser has now changed to include the ‘broadcast’ arg, we are going to use this because sometime we want to disable the message going out. In /src/server/zone.js we will alter the setDockedUser call to handle this broadcast arg.

...
  loadUserIntoZone(user) {
    ...    
        ...
        result.object.setDockedUser(user, true);
        ...
    });
  }
...

Next we will have our SHIP_DETAILS message go out when we join and are loaded in our ship:

In /src/server/zone.js - add the following broadcastTouser to the end of ‘loadUserIntoZone’ to send the message.

...
  loadUserIntoZone(user) {
    ...
    let existingShip = this.oms.getWorldObject(user.vehicleid);
    if (existingShip == null ) {
      this.oms.instantiateShip(user.vehicleid).then((loadedShip) => {
          return this.oms.getSafePositionForObject(loadedShip.core.x, loadedShip.core.y, loadedShip, 15, null);
        }).then((result) => {
          ...

          this.broadcastToUser(user, constants.SHIP_DETAILS, {
            objectID: result.object.objectID,
            stats: result.object.getPrivateComposition()
          });
      });
    } else {
      logger.info('Users vehicle still in zone... firing initstate');

      ...
      this.broadcastToUser(user, constants.SHIP_DETAILS, {
        objectID: existingShip.objectID,
        stats: existingShip.getPrivateComposition()
      });
    }
  }
...

You will see the broadcastUsers were added on both potential routes of the method, sending out the SHIP_DETAILS.

Now we can go back to the client and we will use this ‘docked’ data, to display who is in the vehicle.

To do this we are going to add some interface functionality, selecting an object using the mouse pointer.

Our store keeps hold of our UI information, so we’ll create a new actionType in /src/client/constants/actionTypes.js

...
  SELECTED_OBJECT: 'selected_object',
...

Now in /src/client/actions/actions.js

...
export const selectedObject = (objectID) => {
    return {
        type: actionTypes.SELECTED_OBJECT,
        objectID: objectID
    }
}
...

Finally we will add the handling of the action to our reducer at /src/client/reducers/worldReducer.js, and put it into the initial state.

...
let initialState = {
  ...
  selectedObject: null
};
...
    case actionTypes.SELECTED_OBJECT:
      updated['selectedObject'] = action.objectID;
      return updated      
...

Now we need to fire this selectedObject action upon the mouse click in /src/client/UI/gameWorld.js

...
import {
  ...
  selectedObject
} from '../actions/actions.js';

...
  clickCheck(e) {
    if(e.srcElement.id == 'wr'){
      let obj = this.gameWorldEngine.getObjectUnderPoint(e.clientX+cam.x, e.clientY+cam.y);
      if(obj !== null) {
        console.log('Selecting: ',obj.objectID)
        this.props.dispatch(selectedObject(obj.objectID));
      } else {
        console.log('Clearing selection')
        this.props.dispatch(selectedObject(null));
      }
    }
  }
...
        <div><canvas id="wr" onKeyDown={this.onKeyPressed} style={{zIndex:-1}} width={cam.width} height={cam.height} ref={this.canvasref} /></div>
...

In the above we added an id attribute to the canvas, and to test on click and then our click handler asks the gameWorldEngine for what object is at the position and if found dispatches our selectedObject action. If there is nothing under the click we dispatch selectedObject with null to clear the selection.

Now in /src/client/clientWorldEngine.js we’ll add that ‘getObjectUnderPoint’ method

...
import { Vector } from 'sat';
...
  getObjectUnderPoint(x, y) {
    let p = new Vector(x, y);
    let nearestObject = null;

    for (let i=0; i<this.worldObjects.length; i++) {
      var objBounds = this.worldObjects[i].getCurrentCollisionBounds().bounds;
      var response = new SAT.Response();
      var collided = false;

      collided = SAT.pointInPolygon(p, objBounds, response);

      if (collided) {
        if(nearestObject !== null){
          if( Math.sqrt(Math.pow((this.worldObjects[i].core.x-x), 2) + Math.pow((this.worldObjects[i].core.y-y), 2) ) < Math.sqrt( Math.pow((nearestObject.core.x-x), 2) + Math.pow((nearestObject.core.y-y), 2) ) ){
            nearestObject = this.worldObjects[i];
          }
        }else{
          nearestObject = this.worldObjects[i];
        }
      }
    }

    return nearestObject;
  }
...

Now we can use this selectedObject ID

Before this we’ll add a getLabel method to our client side space objects.

/src/client/entities/objects/spaceObject.js

...
  getLabel() {
    return this.title;
  }
...

/src/client/entities/objects/ship.js

...
  getLabel(){
    if (this.spec.dockedName.trim().length == 0) {
      return "[Empty ship]";
    } else {
      return ((this.core.typeID == constants.SHIP_PTU) ? 'PTU' : 'ship')+' ('+this.spec.dockedName+')',w,0);
    }
  }
...

Now back to /src/client/UI/gameWorld.js

...
  update (ts, dt) {
    ...
    this.drawSelectedUI(cam);
  }

  drawSelectedUI(observationPoint) {
    if(this.props.world.selectedObject !== null){
      let selectedWorldObject = this.gameWorldEngine.getObject(this.props.world.selectedObject);
      if(selectedWorldObject !== null && selectedWorldObject !== undefined){
        context.save()
        context.translate((selectedWorldObject.core.x - observationPoint.x), (selectedWorldObject.core.y - observationPoint.y))

        context.font = "10px Arial";
        context.fillStyle = '#FFFFFF';

        context.fillText(selectedWorldObject.getLabel(),selectedWorldObject.typeConfig.width,0);

        context.restore();
      }
    }
  }

...

In the above we created a new method called ‘drawSelectedUI’ which draws our selected object label, we then added a drawSelectedUI call to the end of the update method.

Building and running the above we get this when we click the ships:

clicktest

Now we have enough in place to do board/unboard:

To begin with we will have it so that when the user sends a ‘board [objectid]’ command, the server attempts to put them in that empty ship (if they are close enough). Later we will add some kind of ‘object actions’ so that you click select a ship and somewhere on the interface will be a button ‘Board’. To unboard we will simply send an ‘unboard’ command, which will spawn the user back in their PTU near the vehicle, and set the previously occupied vehicle to empty.

We’ll start by adding some command handlers to the ‘onReceivedInput’ method in zone.js to handle our two commands:

...
  onReceivedInput(user, data) {
    return new Promise((resolve, reject) => {
      ...
      switch (inputData[0]) {
        ...
        case 'board':
          logger.info(`${user.username} trying to board ${inputData[1]}`)
          if (inputData.length == 2) {
            this.boardVehicleRequest(user, inputData[1]);
          }
          break;
        case 'unboard':
          this.unboardVehicleRequest(user);
          break;
        ...
...

Next we will create the two methods referenced above - They will work as follows:

‘board’ will take an id of a target vehicle. It will check that the vehicle exists. After this we will do a check to see if our current ship is a PTU (which it must be). We’ll then check that the target vehicle is boardable (which for now will just be whether it is empty). If all that is okay, we will then remove the users PTU from the world and set it to be located at the target vehicle (as if inside it). We’ll then set the user to be docked with the targetVehicle, then broadcast a new SHIP_DETAILS message so that the user has the private ship data for the new vehicle they are piloting.

‘unboard’ will kind of do the reverse. We’ll check that we are not a PTU (a user can’t unboard their PTU). We’ll then instantiate the PTU next to the users current vehicle (in a safe non colliding position), set the previous vehicle as empty, add the PTU to the world and then set the user docked with the PTU and as before send the SHIP_DETAILS to the user.

The code for this is below:

/src/server/zone.js

...
  boardVehicleRequest(user, targetVehicleID) {
    return new Promise((resolve, reject) => {

      const targetVehicle = this.oms.getWorldObject(targetVehicleID);
      const userVehicle = this.oms.getWorldObject(user.vehicleid);

      if (targetVehicle == null || userVehicle == null) {
        console.log('invalid objects');
        return resolve();
      }

      if (userVehicle.core.typeID != constants.SHIP_PTU) {
        console.log('not ptu')
        // Cannot board while not a PTU
        return resolve();
      }

      if (!targetVehicle.isBoardableBy(user, userVehicle)) { // this checks vacancy
        console.log('not boardable');
        // Cannot board - handle later
        return resolve();
      }

      this.oms.removeObjectFromWorld(userVehicle, {});
      userVehicle.setVehicleVacant(true) // Set Vehicle vacant and broadcast

      userVehicle.setDockedLocation(targetVehicle.objectID, '');

      user.vehicleid = targetVehicle.objectID;
      targetVehicle.setDockedUser(user, true);

      this.broadcastToUser(user, constants.SHIP_DETAILS, {
        objectID: targetVehicle.objectID,
        stats: targetVehicle.getPrivateComposition()
      });

      // flag user for persistance to record they are in a new vehicle
      this.userManager.addUserToPersistanceQueue(user.userid);
    });
  }

  unboardVehicleRequest(user){
    return new Promise((resolve, reject) => {

      const userVehicle = this.oms.getWorldObject(user.vehicleid);
      if (userVehicle == null) {
        return resolve();
      }

      if(!userVehicle.core.typeID === constants.SHIP_PTU){
        // Cannot unboard when we are just a PTU
        return resolve(); 
      }

      let lX = userVehicle.core.x + 30;
      let lY = userVehicle.core.y + 30;

      if(user.ptuVehicleID == null){
        logger.error(`user ${user.userid} has a null ptuVehicleID - critical error`);
        return reject();
      }

      // Spawn the PTU
      this.oms.instantiateShip(user.ptuVehicleID).then((ptu) => {
        return this.oms.getSafePositionForObject(lX, lY, ptu, 15, null);
      }).then((result) => {
        if (!result.result) {
          loffer.info('failed to find safe position for object');
          // TODO - handle this
          return resolve();
        }

        // Set existing vehicle to unoccupied
        userVehicle.setVehicleVacant(true) // Set Vehicle vacant and broadcast

        // TODO position our PTU in safe position
        result.object.core.x = result.x;
        result.object.core.y = result.y;

        // Assign user to vehicle
        result.object.setDockedUser(user, false);
        result.object.setDockedLocation('', '');

        user.vehicleid = result.object.objectID;
        this.userManager.addUserToPersistanceQueue(user.userid);

        logger.info('Spawning PTU..');
        this.oms.addObjectToWorld(result.object);

        this.broadcastToUser(user, constants.SHIP_DETAILS, {
          objectID: result.object.objectID,
          stats: result.object.getPrivateComposition()
        });

        return resolve();
      });
    });
  }
...

You will see we do a check on the target vehicle to see if it is boardable by us, we’ll add a basic version of that to boardableEntity which for now just checks to see if the vehicle is empty.

/src/server/gameObjects/boardableEntity.js

...
  isBoardableBy(user, userVehicle) {
    if(this.hasActiveOccupant()){
      return false;
    }
    return true;
  }
...

Earlier we used console.log to output an object’s ID when it is selected, this will make it easier to test this:

Building and running this.. we should get something like the following:

boarding

You will see in the first image we are in our PTU, and select the empty Ship, in the second image we have boarded the vessel and our PTU loaded into the ship, in the third I have flown around a bit, and then unboarded which ejected my PTU from the vehicle leaving the Ship empty.

Link to source files for Part 11

Part 11 - Ship composition, Hyperdrive and Shields

Now that we can get in the new vehicle we will extend what a ship can do, for this we will add the idea of ship modules.

We will start this by adding more state to a ship. This may end being a bad idea but I will store a ships state in a json object in the DB. This is ‘objectState’ that you may have seen when we initially created the worldObject schema.

Our world objects are going to be composed of 4 elements of data:

We’ll then have modules which can be fitted to the ship, which aswell as adding capabilities to the ship, may alter some of the properties above when active. Such as some kind of special reinforced shield module / reactor or thruster speed improvements. The above data is going to consist of some elements which are public (known by all that the vehicle is visible too) and private (known only to those such as the occupant). We won’t build this all in one go but gradually in parts so don’t worry too much about what all that means. To begin to implement this we will add some basic ‘engineering’ (kind of like in star trek) to the ship.

In this part we will not yet add in modules/fittings. Before we add fittings we need to implement the ingame item system. We can however implement some functionality that all ships have the ability to do in some capacity, regardless of fittings. To start we will have a shield, some hull and armour, a hyperdrive and also build the autopilot system.

(This may all seem a bit mental - a symptom of making all this up as I go along without a plan)

We’ll begin with just hull and armour so that it is clearer what is going on with the composition of a ship. The following will add code which sets u