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
- javascripting: Javascript workshop
- learnyounode: Async I/O workshop
- async-you: Advanced async workshop
- browserify-adventure: Browserify workshop
- stream-adventure: Stream workshop