View Source

The Art of I/O (4th Session of 4)

A magical introduction to input and output signals

First Session | Previous Session | Course Home | Toggle Notes Mode | http://ioschool.is

Course Recap

In our last three sessions, we've touched on

  • functions (accept input, return output, maybe do effects)
  • closures (functions that 'close over' variables)
  • callbacks (functions that are 'called back' later)
  • modules (exported code we can require in other code)
  • events (when things happen)
  • streams (chunks of data over time)

In this session, we'll complete our magical introduction to these spells by crafting a real-time multiplayer game. \(=^‥^)/’``

Basic Server

npm install ecstatic

// server.js

var http = require('http')
var ecstatic = require('ecstatic')

var httpServer = http.createServer(ecstatic)

httpServer.listen(5000)

Basic HTML

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Space Cat!</title>
    <link href="./styles.css" rel="stylesheet" />
  </head>
  <body>
    <svg class="space" height="100%" width="100%">
      <image class="cat" xlink:href="./cat.svg"
        height="50px" width="50px" x="50%" y="50%" />
    </svg>
    <script src="./client.js"></script>
  </body>
</html>

Basic Assets

# twemoji cat emoji
curl https://upload.wikimedia.org/wikipedia/commons/a/a8/Twemoji_1f63a.svg > cat.svg
/* styles.css */
html, body, svg {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
// client.js
console.log("welcome to space.");

Add Some Browserify

npm install browserify

// server.js

var http = require('http')
var ecstatic = require('ecstatic')
var browserify = require('browserify')

function httpHandler (req, res) {
  if (req.url === "/client.js") {
    browserify("./client.js").bundle().pipe(res)
  } else {
    ecstatic(req, res)
  }
}

var httpServer = http.createServer(httpHandler)

httpServer.listen(5000)

Connect Server And Client Over Streams

// server.js
// ...
var websocket = require('websocket-stream')

function wsHandler (stream) {
  console.log("new connection!")
}

websocket.createServer({
  server: httpServer,
}, wsHandler)
// client.js

var websocket = require('websocket-stream')

var stream = websocket("ws://" + location.host + "/")

Server - Broadcast State

// server.js
// ...

var position = [50, 50]

function wsHandler (stream) {
  // send initial position to client
  stream.write(JSON.stringify(position))
}

// ...

Client - Listen For State

// client.js
// ...

var through = require('through2')

stream.pipe(through(function (state, enc, next) {
  // get current position from server
  var position = JSON.parse(state)
  console.log(position)

  next()
}))

Client - Render State

// client.js

// ...
var dom = require('domquery')

var cat = dom('.cat')[0]

// ...
stream.pipe(through(function (state, enc, next) {
  // ...

  // set cat x and y coordinates based on position
  cat.setAttribute('x', position[0] + "%")
  cat.setAttribute('y', (100 - position[1]) + "%")

  next()
}))

Client - Capture Events

// client.js

// ...

var codeToKey = require('keycode')

dom(document).on('keydown', function (ev) {
  // get key name from keydown event
  var key = codeToKey(ev)
  console.log("key", key)
})

Client - Send Actions

// client.js

// ...

dom(document).on('keydown', function (ev) {
  var key = codeToKey(ev)

  // send key name to server
  stream.write(key)
})

Server - Receive Actions

// server.js

// ...

function wsHandler (stream) {
  // ...

  // pipe data from client to stdout
  stream.pipe(process.stdout)
}

// ...

Server - Handle Actions

// server.js ----------------------------------------
// ...

function wsHandler (stream) {

  // change position based on incoming keys
  stream.pipe(handleActions())
}

function handleActions () {
  return through(function (buf, enc, next) {
    // get incoming key
    var key = buf.toString()
    console.log("key", key)

    // get current position
    var pos = position;
    console.log("position", pos)

    next()
  })
}

Server - Update State

// server.js ----------------------------------------
// ...
var mod = require('mod-op')
// ...
    // ...
    // update position based on key
    switch (key) {
      case "right":
        pos[0] = mod((pos[0] + 1), 100)
        break
      case "left":
        pos[0] = mod((pos[0] - 1), 100)
        break
      case "up":
        pos[1] = mod((pos[1] + 1), 100)
        break
      case "down":
        pos[1] = mod((pos[1] - 1), 100)
        break
      default:
        return next()
    }
    // ...

Server - Send State Updates

// server.js ----------------------------------------
// ...

function wsHandler (stream) {
  // ...
  // change position based on incoming keys
  stream.pipe(handleActions(stream))
  // ...
}

function handleActions (stream) {
  return through(function (buf, enc, next) {
    // ...

    // write new position
    stream.write(pos)

    next()
  })
}

Server - Observable State

// server.js ----------------------------------------

// ...

var Observ = require('observ')
var ObservStream = require('observ-stream')

var initialPosition = [50, 50]
var positionObserv = Observ(initialPosition)
var positionStream = ObservStream(
  positionObserv, { objectMode: false }
)

function wsHandler (stream) {

  // write current position
  var position = positionObserv()
  stream.write(JSON.stringify(position))

  // pipe position state to client
  positionStream.pipe(stream)

  stream.pipe(handleActions(positionObserv))
}

function handleActions (positionObserv) {
  return through(function (buf, enc, next) {
    // ...

    // get current position
    var pos = positionObserv()

    // ...

    // set new position
    positionObserv.set(pos)

    next()
  })
}

Make It Magical

/* styles.css */

/* ... */

/* below derived from http://codepen.io/simurai/pen/kgsce */
.space {
  background-position: center;
  background-size: 120px 120px;
  background-color: hsla(320,80%,60%,1);
  background-image: repeating-radial-gradient( hsla(320,100%,60%,.6) 0px, hsla(220,100%,60%,0) 60%),
    repeating-radial-gradient( hsla(330,100%,40%,1) 12%, hsla(320,80%,60%,1) 48px);
  animation: space 10s cubic-bezier(.1,.4,.9,.6) infinite;
}

@keyframes space {
  from { background-size:  120px  120px,  48px  48px; }
  50%  { background-size: 240px 240px, 200px 200px; }
  to   { background-size:  48px  48px, 280px 280px; }
}

Life Of Magic

NodeSchool

Event Magic

Events allow us to do something when something else happens.

We can listen to event emitters (objects that "emit events") with emitter.on(eventName, callback).