NodeJS Game: browser based Single shard MMO Game + Ethereum based economy

This is going to be an attempt to be build a simple space based MMO JS game in the browser with NodeJS on the backend, and an ingame Ethereum based economy.

The concept

The idea I have is: A simple 2D version of Eve in the browser, with Ethereum.

To this I am going to develop the following:

To begin with I am going to keep the functionality simple.

Each player will have a ship. They can fly about, shoot at each other. Upon death they will respawn at a base of some kind from which they can undock. They will be able to mine asteroids. Haul it back to the base and sell the minerals and in return recieve the Ethereum based token.

The client

My first step is going to be building a client which draws a ship, in space and is controllable by the user.

I am going to begin with building a basic React application, with a Component SpaceNode

import React, { Component } from 'react'

export default class SpaceNode extends Component {
  constructor () {
    super()

    this.state = {
      context: null
    }
  }

  componentDidMount () {
    let context = this.refs.canvas.getContext('2d')

    this.setState({ context: context })

    let lastTime = null

    this._frameCallback = (ms) => {
      if (lastTime !== null) {
        const diff = ms - lastTime
        this.update(diff / 1000)
      }
      lastTime = ms
      requestAnimationFrame(this._frameCallback)
    }
    requestAnimationFrame(this._frameCallback)
  }

  update (dt) {
    this.clear()

    this.draw()
  }

  draw () {
    let w = 2
    let h = 4

    let x = 320
    let y = 240

    this.state.context.strokeStyle = '#FFFFFF'
    this.state.context.lineWidth = 1

    this.state.context.beginPath()
    this.state.context.moveTo(x, y - (h / 2))
    this.state.context.lineTo(x - (w / 2), y + (h / 2))
    this.state.context.lineTo(x + (w / 2), y + (h / 2))
    this.state.context.lineTo(x, y - (h / 2))
    this.state.context.closePath()
    this.state.context.stroke()
    this.state.context.restore()

    this.state.context.fillStyle = '#FF0000'
    this.state.context.fillRect(x, y, 1, 1)
  }

  clear () {
    this.state.context.fillStyle = '#000000'
    this.state.context.fillRect(0, 0, 640, 480)
  }

  render () {
    return (
      <div>
        <canvas width='640' height='480' ref='canvas' />
      </div>
    )
  }
}

This places a 640 x 480 canvas on the screen and on load will setup our update method to redraw the canvas.

The update method to begin with just clears the canvas with a black fill and then draws a rectangle representing the players ship

Creating the ship and the camera

Next I am going to move the ship class out into a seperate class, implement the concept of a camera point, and have our players ship move to wherever the user clicks on the screen.

It is possible I will change how this is done but for now I will have a SpaceObject class which the worlds various objects will extend

export default class SpaceObject {
  constructor ( x, y) {
    this.x = x
    this.y = y
    this.rotation = 0
  }
}

In SpaceNode.js I will expand our state to include our camera settings, the x,y we are looking at in the world and the width and height of our screen. I will then add a ships array, a destination object to store where the user has clicked and also a reference to the players ship.

...
    this.state = {	
      cam: { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight },
      context: null,
      ships: [],
      destination: { x: 320, y: 240, angle: 0 },
      player: null      
    }
...

I will implement a basic click method which work out the position in the world the user has clicked, and also the angle from the players current position

...
  handleClick(e){

    let angle = Math.round( Math.atan2(e.clientY - (this.state.cam.height/2), e.clientX - (this.state.cam.width/2)) * 180 / Math.PI )

    if(angle > -90) {
      angle += 90
    } else {
      angle = 450 + angle
    }    

    this.setState({destination: { x: this.state.cam.x+(e.clientX), y: this.state.cam.y+(e.clientY), angle: angle }})
  }
...

I’ll then update the draw method so that it repositions the camera based on any user movements and then iterates over the ship collection, drawing them.

  draw () {
    if(this.state.cam.x !== this.state.player.x || this.state.cam.y !== this.state.player.y) {
      let newCamPosition = Object.assign(this.state.cam, {})
      newCamPosition.x = this.state.ships[0].x - (this.state.cam.width/2)
      newCamPosition.y = this.state.ships[0].y - (this.state.cam.height/2)

      this.setState({cam: newCamPosition})
    }

    for (let ship of this.state.ships) {
      ship.draw(this.state)
    }
  }

I’ll then create Ship.js, drawing it on the screen and (temporily), if it is the players ship I will move towards the click point and also rotate in the direction of the click

import SpaceObject from './SpaceObject'

export default class Ship extends SpaceObject {
  constructor ( x, y) {
    super(x, y)
  }

  draw (state) {
    let w = 2
    let h = 4

    state.context.save()

    state.context.strokeStyle = '#FFFFFF'
    state.context.lineWidth = 1

    state.context.translate((this.x - state.cam.x), (this.y - state.cam.y))
    state.context.rotate(this.rotation * Math.PI / 180)

    state.context.beginPath()
    state.context.moveTo(0, - (h / 2))
    state.context.lineTo(0 - (w / 2), (h / 2))
    state.context.lineTo((w / 2), (h / 2))
    state.context.lineTo(0, 0 - (h / 2))
    state.context.closePath()
    state.context.stroke()

    state.context.fillStyle = '#FF0000'
    state.context.fillRect(0, 0, 1, 1)    

    state.context.restore()

    if(this == state.player) {

      let angle = Math.atan2((state.destination.y - this.y), (state.destination.x - this.x) ) * 180 / Math.PI

      this.x += Math.cos(angle * Math.PI/180) * 1
      this.y += Math.sin(angle * Math.PI/180) * 1

      if(state.destination.angle !== this.rotation){

        if( (this.rotation-state.destination.angle+360)%360>180 ) {
          this.rotation++
          if(this.rotation > 359 ) this.rotation = 0
        } else {
          this.rotation--
          if(this.rotation < 0 ) this.rotation = 359
        }

      }
    }
  }
}

Finally in componentDidMount I will create our ship, a second ship, and then add them into the ships array and set the state and bind the width and height of our canvas to the camera width and height

...
    let sh = new Ship(320,240) 
    let sh1 = new Ship(500,400)

    let newShips = Object.assign([], this.state.ships)
    newShips.push(sh)
    newShips.push(sh1)

    this.setState({ships: newShips, player: sh})
...   
    <canvas width={this.state.cam.width} height={this.state.cam.height} ref='canvas' /> 
...    

Clicking on the screen we should now see our ship moving in the world relative to the static ship

shipmoverel

The game world

To begin with the game world is going to be around 10,000 pixels by 10,000 pixels. I’ll now quickly make sure the camera point will stay within the bounds of the world.

Adding in the width and height of the current world to the state

...
    this.state = {	
      world: { width: 10000, height: 10000},
...

I’ll then refactor things slightly, creating an updateCamera method which keeps the camera within the bounds of the world. I’ll then create an updateObjects method for handling actions of the objects.

  update (dt) {
    this.clear()

    this.updateObjects()

    this.updateCamera()

    this.draw()
  }

  updateCamera () {
    if(this.state.cam.x !== this.state.player.x || this.state.cam.y !== this.state.player.y) {
      let newCamPosition = Object.assign(this.state.cam, {})
      newCamPosition.x = this.state.player.x - (this.state.cam.width/2)
      newCamPosition.y = this.state.player.y - (this.state.cam.height/2)

      //Keep it within bounds

      if(newCamPosition.x < 0 ) newCamPosition.x = 0
      if(newCamPosition.x > (this.state.world.width - this.state.cam.width) ) newCamPosition.x = (this.state.world.width - this.state.cam.width)
      if(newCamPosition.y < 0 ) newCamPosition.y = 0
      if(newCamPosition.y > (this.state.world.height - this.state.cam.height) ) newCamPosition.y = (this.state.world.height - this.state.cam.height)

      this.setState({cam: newCamPosition})
    }
  } 

  updateObjects () {
    for (let ship of this.state.ships) {
      ship.update(this.state)
    }
  }

  draw () {
    for (let ship of this.state.ships) {
      ship.draw(this.state)
    }
  }

Then within the Ship class I’ll seperate the movement into an update method

  update (state) {
    if(this == state.player) {

      let angle = Math.atan2((state.destination.y - this.y), (state.destination.x - this.x) ) * 180 / Math.PI

      this.x += Math.cos(angle * Math.PI/180) * 1
      this.y += Math.sin(angle * Math.PI/180) * 1

      if(state.destination.angle !== this.rotation){

        if( (this.rotation-state.destination.angle+360)%360>180 ) {
          this.rotation++
          if(this.rotation > 359 ) this.rotation = 0
        } else {
          this.rotation--
          if(this.rotation < 0 ) this.rotation = 359
        }

      }
    }    
  }

  draw (state) {
    let w = 2
    let h = 4

    state.context.save()

    state.context.strokeStyle = '#FFFFFF'
    state.context.lineWidth = 1

    state.context.translate((this.x - state.cam.x), (this.y - state.cam.y))
    state.context.rotate(this.rotation * Math.PI / 180)

    state.context.beginPath()
    state.context.moveTo(0, - (h / 2))
    state.context.lineTo(0 - (w / 2), (h / 2))
    state.context.lineTo((w / 2), (h / 2))
    state.context.lineTo(0, 0 - (h / 2))
    state.context.closePath()
    state.context.stroke()

    state.context.fillStyle = '#FF0000'
    state.context.fillRect(0, 0, 1, 1)    

    state.context.restore()
  }

The camera should now stay within the confines of the world. This breaks the code which points the ship in the direction of the mouse click, but that is okay as soon that functionality will be changed.

I’ll now refactor the code so that instead of a ships array, we have a worldObjects array and change the references accordingly

    this.state = {	
      ....
      worldObjects: [],
      ...
    }

Now I will create an asteroid class. There is probably a better way to do this, but for now I will randomly build a number of vertices, at a random angle around a point at a random distance. The update method will handle the movement and the rotation and the draw method will then draw the vertices.

import SpaceObject from './SpaceObject'

export default class Asteroid extends SpaceObject {

  constructor ( x, y) {
    super(x, y)
    
    this.vertices = []

    let size = Math.round(20 + Math.random() * 20)   
    let numVertices = Math.round(5 + Math.random() * 4)   
    
    let range = 360 / numVertices 

    for(let v = 0; v < numVertices; v++ ){
      let randomAngle = (range*v) + Math.round(Math.random() * range)
      let randomDistance = 10 + Math.round(Math.random() * size)
      
      let vX = Math.cos(randomAngle * Math.PI/180) * randomDistance
      let vY = Math.sin(randomAngle * Math.PI/180) * randomDistance     

      this.vertices.push({ x: vX, y: vY })
    }

    this.velocityX = Math.round(Math.random() * 0.1)
    this.velocityY = Math.round(Math.random() * 0.1)

    this.rotationSpeed = Math.random() * 0.06
    
  }
  
  update (state) {
      this.rotation += this.rotationSpeed

      if(this.rotation > 360) this.rotation -= 360
      if(this.rotation < 0) this.rotation += 360

      this.x += Math.cos(this.velocityX * Math.PI/180) * 0.1
      this.y += Math.sin(this.velocityY * Math.PI/180) * 0.1
  }

  draw (state) {

    state.context.save()

    state.context.strokeStyle = '#FFFFFF'
    state.context.lineWidth = 1

    state.context.translate((this.x - state.cam.x), (this.y - state.cam.y))
    state.context.rotate(this.rotation * Math.PI / 180)

    state.context.beginPath()

    state.context.moveTo(this.vertices[0].x, this.vertices[0].y)    
    
    for (let vertex of this.vertices) {
       state.context.lineTo(vertex.x, vertex.y)    
    }

    state.context.closePath()
    state.context.stroke()
    state.context.restore()
  }
}

To test this, In componentDidMount in SpaceNode I will create 4 asteroid objects and add them to the worldObjects array

asteroids

Next up is to create a simple space station which consists of a few shapes, and in the centre of the station, a beacon, which is an animation drawing a circle which fades further to black on each update

import SpaceObject from './SpaceObject'

export default class Base extends SpaceObject {

  constructor ( x, y) {
    super(x, y)

    this.beaconLast = Date.now()
    this.beaconState = 0
    this.beaconInterval = 25
    this.beaconFade = 255
    this.beaconRange = 50
  }
  
  update (state) {
    if(this.beaconLast+this.beaconInterval < Date.now() ){
      this.beaconState++   
      this.beaconLast = Date.now()
      this.beaconFade-=10

      if(this.beaconState>this.beaconRange){
        this.beaconState = 0
        this.beaconFade = 255
      }
    }
  }

  draw (state) {
    let w = 100
    let h = 100
    let bevel = 10 

    state.context.save()

    state.context.strokeStyle = '#FFFFFF'
    state.context.lineWidth = 1

    state.context.translate((this.x - state.cam.x), (this.y - state.cam.y))

    state.context.beginPath()
    state.context.moveTo(0-w/2, (bevel-h/2) )
    state.context.lineTo(0-w/2, (h/2)-bevel )
    state.context.lineTo(bevel-w/2, (h/2) )
    state.context.lineTo(w/2-bevel, (h/2) )
    state.context.lineTo(w/2, (h/2)-bevel )
    state.context.lineTo(w/2, bevel-(h/2) )
    state.context.lineTo(w/2-bevel, 0-(h/2) )
    state.context.lineTo(bevel-w/2, 0-(h/2) )
    state.context.closePath()
    state.context.stroke()

    w = 90
    h = 90
    bevel = 9 

    state.context.beginPath()
    state.context.moveTo(0-w/2, (bevel-h/2) )
    state.context.lineTo(0-w/2, (h/2)-bevel )
    state.context.lineTo(bevel-w/2, (h/2) )
    state.context.lineTo(w/2-bevel, (h/2) )
    state.context.lineTo(w/2, (h/2)-bevel )
    state.context.lineTo(w/2, bevel-(h/2) )
    state.context.lineTo(w/2-bevel, 0-(h/2) )
    state.context.lineTo(bevel-w/2, 0-(h/2) )
    state.context.closePath()
    state.context.stroke()

    state.context.beginPath()
    state.context.moveTo(0, (h/2) )
    state.context.lineTo(0-w/2, 0 )
    state.context.lineTo(0, 0-(h/2) )
    state.context.lineTo(w/2, 0 )
    state.context.closePath()
    state.context.stroke()

    state.context.fillStyle = '#FF0000'
    state.context.fillRect(0, 0, 1, 1)    

    if(this.beaconFade > 0){
      state.context.lineWidth = 2
      state.context.strokeStyle =  '#'+("00000"+(0<<16|0<<8|this.beaconFade).toString(16)).slice(-6)
      state.context.beginPath()
      state.context.arc(0,0,this.beaconState,0,2*Math.PI)
      state.context.stroke()
    }

    state.context.restore()
  }
}

base

The UI

That now gives enough objects to begin working on a UI. This UI will not be a finished product but instead just simple elements to enable basic functionality. To begin with it will consist of 5 elements. Two buttons (tools) which alter the function of the mouse.

Then there will be a panel/window which displays the current selected item

I’ll have a ‘targeting’ element, which makes it visible what object is currently selected

Finally there will be a panel which lists actions which can be carried out on the selected object.

To make the buttons I will add two button html tags using zIndex to overlay them on the canvas. I’ll also modify the canvas click action so that it responds to a click on the canvas instead of the window and also change the name of the method which handles the click. The selected item panel will for now just be the title of the object (which will be added shortly).

...
    this.refs.canvas.addEventListener('click',  this.handleCanvasClick.bind(this))
...
  render () {
    const selectedText = (this.state.selectedObject !== null) ? this.state.selectedObject.title : 'None'

    return (
      <div>
        <canvas style= width={this.state.cam.width} height={this.state.cam.height} ref='canvas' />
        <input type="button" onClick={this.handleSelectButtonClick.bind(this)} style= value="Select"/>
        <input type="button" onClick={this.handleMoveButtonClick.bind(this)} style= value="Move"/>
        <span style=>Selected: {selectedText}</span>
      </div>
    )
  }
...    

I’ll then add two constants for the different types of click state, an action element in the state , and then our two click handlers

const ACTION_SELECT = "select"
const ACTION_MOVE = "move"
...
this.state = {	
...
  action: ACTION_SELECT  
...
}
...
  handleSelectButtonClick(e) {
    this.setState({action: ACTION_SELECT})   
  }

  handleMoveButtonClick(e) {
    this.setState({action: ACTION_MOVE})   
  }
...  

Then to complete this I will alter our canvas click handler as follows:

  handleCanvasClick(e){

    if(this.state.action == ACTION_MOVE ){
      let angle = Math.round( Math.atan2(e.clientY - (this.state.cam.height/2), e.clientX - (this.state.cam.width/2)) * 180 / Math.PI )

      if(angle > -90) {
        angle += 90
      } else {
        angle = 360 + 90 + angle
      }    

      this.setState({destination: { x: this.state.cam.x+(e.clientX), y: this.state.cam.y+(e.clientY), angle: angle }})
    } else {
    

    }
  }

Now our ship should only move if ‘move’ is the currently selected action

I’ll now create the code for selecting the targeted element ( I’ll refactor this later )

To do this I will iterate over all the objects, if the click is within the bounds of the object, then it will be selected. Whilst iterating further, if we detect that the click is within the bounds of another object, we’ll check to see if it is closer to the centre of that object than the previous.

  determineClickedObject (clickX, clickY) {
    let selectedItem = null

    for (let object of this.state.worldObjects) {
      let objectScreenX = (object.x-this.state.cam.x)
      if(clickX > ( (object.x-this.state.cam.x) - object.width/2) && clickX < ( (object.x-this.state.cam.x) + object.width/2) && clickY > ( (object.y-this.state.cam.y) - object.height/2) && clickY < ( (object.y-this.state.cam.y) + object.height/2) ){
        if(selectedItem !== null){
          if( Math.sqrt(Math.pow(((object.x-this.state.cam.x)-clickX), 2) + Math.pow(((object.y-this.state.cam.y)-clickY), 2) ) < Math.sqrt( Math.pow(((selectedItem.x-this.state.cam.x)-clickX), 2) + Math.pow(((selectedItem.y-this.state.cam.y)-clickY), 2) ) ){
            selectedItem = object	
          }
        }else{
          selectedItem = object
        }
      }
    }

    return selectedItem
  }

Later on this will be improved to check to see whether the click is within a polygon but for now this will do in order to build the UI.

Here I also add in width, height and title to SpaceObject and have it passed in to the object constructors like follows

Ship.js

...
  constructor ( x, y, width, height, objectTitle) {
    super(x, y, width, height, objectTitle)
  }
...    

When I currently instantiate the objects in componentDidMount I will for now just pass in the data hardcoded, although later this will come from the server

    let sh = new Ship(320,240,2,4,'Ship 1') 
    let sh1 = new Ship(500,400,2,4,'Ship 2')

    let as1 = new Asteroid(1000,500,20,20, 'Asteroid 1')
    let as2 = new Asteroid(900,450,20,20, 'Asteroid 2')

    let base1 = new Base(100,100,100,100, 'Space Station')

In order to store the currently selectedObject, I will add ‘selectedObject: null’ to the state and then add an else if clause to the click handler

    } else if() {
      // Target

      this.setState({selectedObject: this.determineClickedObject(e.clientX, e.clientY)})   
    }

Now that we have the selectedObject, I will add in the crosshairs which will identify the selected item on the screen

  draw () {
    for (let object of this.state.worldObjects) {
      object.draw(this.state)
    }

  	this.drawInterface()
  }

  drawInterface () {
    this.drawTarget()
  }

  drawTarget () {
    if(this.state.selectedObject!==null){
      let targetX = this.state.selectedObject.x - this.state.cam.x
      let targetY = this.state.selectedObject.y - this.state.cam.y

      let targetWidth = this.state.selectedObject.width
      let targetHeight = this.state.selectedObject.height
      
      targetWidth+=5
      targetHeight+=5

      if(targetWidth < 20) targetWidth = 20
      if(targetHeight < 20) targetHeight = 20

      //Draw left top corner

      this.state.context.save()

      this.state.context.strokeStyle = '#FFFF00'
      this.state.context.lineWidth = 2

      this.state.context.beginPath()
      this.state.context.moveTo(targetX - (targetWidth/2) , targetY - (targetHeight/2))
      this.state.context.lineTo(targetX - (targetWidth/2) , targetY - (targetHeight/2) + 5 )
      this.state.context.moveTo(targetX - (targetWidth/2) , targetY - (targetHeight/2))
      this.state.context.lineTo(targetX - (targetWidth/2) +5 , targetY - (targetHeight/2))

      this.state.context.moveTo(targetX + (targetWidth/2) , targetY - (targetHeight/2))
      this.state.context.lineTo(targetX + (targetWidth/2) , targetY - (targetHeight/2) + 5 )
      this.state.context.moveTo(targetX + (targetWidth/2) , targetY - (targetHeight/2))
      this.state.context.lineTo(targetX + (targetWidth/2) -5 , targetY - (targetHeight/2))

      this.state.context.moveTo(targetX + (targetWidth/2) , targetY + (targetHeight/2))
      this.state.context.lineTo(targetX + (targetWidth/2) , targetY + (targetHeight/2) - 5 )
      this.state.context.moveTo(targetX + (targetWidth/2) , targetY + (targetHeight/2))
      this.state.context.lineTo(targetX + (targetWidth/2) -5 , targetY + (targetHeight/2))

      this.state.context.moveTo(targetX - (targetWidth/2) , targetY + (targetHeight/2))
      this.state.context.lineTo(targetX - (targetWidth/2) , targetY + (targetHeight/2) - 5 )
      this.state.context.moveTo(targetX - (targetWidth/2) , targetY + (targetHeight/2))
      this.state.context.lineTo(targetX - (targetWidth/2) +5 , targetY + (targetHeight/2))

      this.state.context.closePath()
      this.state.context.stroke()

      this.state.context.restore()
        
    }
  }

We should now get something like the following

selectedItem

…To be continued. Last updated: 06/12/2017