Making JavaScript Games

From the Browser to the App Store

Created by Eric Lathrop / @ericlathrop

Here's the video of this presentation at Nodevember 2014

The <canvas> Element



						

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#f00";
ctx.fillRect(270, 190, 100, 100);
						

ctx.fillStyle = "yellow";
ctx.font = "60px sans-serif";
ctx.fillText("Hello World!", 150, 250);
						

var logo = new Image();
logo.addEventListener("load", function() {
    ctx.drawImage(logo, 70, 100);
});
logo.src = "two-scoop-games-logo.png";
						

The Render Loop


function render(time) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "yellow";
    ctx.font = "60px sans-serif";
    ctx.fillText(time, 150, 150);
    window.requestAnimationFrame(render);
}
window.requestAnimationFrame(render);
						

var lastTime = -1;
function elapsedTime(time) {
    var elapsed = time - lastTime;
    if (lastTime === -1) {
        elapsed = 0;
    }
    lastTime = time;
    return elapsed;
}
						

var fps = Math.floor((1 / elapsed) * 1000) + " FPS";
						

Animation


var frame = 0;
var frames = 22;
var frameWidth = img.width / frames;
var frameX = frame * frameWidth;

ctx.drawImage(img, frameX, 0, frameWidth, img.height, 260, 25, frameWidth, img.height);
						

var animation = {
    img: new Image(),
    time: 0,
    frames: 22,
    msPerFrame: 40,
    advance: function(elapsed) {
        this.time += elapsed;
    },
    draw: function(ctx, x, y) {
        var width = this.img.width / this.frames;
        var frame = Math.floor(this.time / this.msPerFrame) % this.frames;
        var frameX = frame * width;
        ctx.drawImage(this.img,
            frameX, 0, width, this.img.height,
            x, y, width, this.img.height);
    }
};
animation.img.src = "hamster-run-right-f22.png";
						

Keyboard Input & Motion


var keys = {};
var keyMap = { 87: 'w', 65: 'a', 83: 's', 68: 'd' };

window.addEventListener("keydown", function(e) {
    keys[keyMap[e.keyCode]] = true;
});

window.addEventListener("keyup", function(e) {
    keys[keyMap[e.keyCode]] = false;
});
						

hamsterSpeedX = 0, hamsterSpeedY = 0;
if (keys["w"]) { hamsterSpeedY = -0.4; }
if (keys["a"]) { hamsterSpeedX = -0.4; }
if (keys["s"]) { hamsterSpeedY = 0.4; }
if (keys["d"]) { hamsterSpeedX = 0.4; }

hamsterX += hamsterSpeedX * elapsed;
hamsterY += hamsterSpeedY * elapsed;
						

if (keys["a"]) {
    hamsterSpeedX = -1;
}
if (keys["d"]) {
    hamsterSpeedX = 1;
}
hamsterSpeedX *= 0.9; // friction
hamsterX += hamsterSpeedX * elapsed;
						

if (keys["w"]) {
    hamsterSpeedY = -1;
}
hamsterSpeedY += 0.1; // gravity
hamsterY += hamsterSpeedY * elapsed;
if (hamsterY > 150) { // floor
    hamsterY = 150;
    hamsterSpeedY = 0;
}
						

Sound


var audioContext = new AudioContext();
var jumpSound;

var request = new XMLHttpRequest();
request.open("GET", "jump.mp3", true);
request.responseType = "arraybuffer";

request.addEventListener("readystatechange", function() {
    if (request.readyState !== 4) {
        return;
    }
    audioContext.decodeAudioData(request.response, function(buffer) {
        jumpSound = buffer;
    });
});

request.send();
						

var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination)
source.start(0);
						

Putting It All Together


if (keys["w"]) {
    if (hamsterY === 150) { // on floor
        playSound(jumpSound);
        hamsterSpeedY = -1.5;
    }
}
						
Stanley Squeaks and the Emerald Burrito

Other APIs

  • Gamepads
  • Vibration
  • Ambient Light
  • Location
  • Accelerometer
  • Full Screen
  • Camera
  • Microphones
  • Websockets / WebRTC data channel
  • Local Storage
  • Application Cache
  • Page Visibility API

Chrome Web Store

manifest.json


{
    "name": "Scurry",
    "description": "Play as a cockroach...",
    "version": "1.0.2",
    "manifest_version": 2,
    "app": {
        "background": {
            "scripts": ["background.js"]
        }
    },
    "icons": {
        "16": "images/icons/Icon-16.png",
        "128": "images/icons/Icon-128.png"
    },
    "permissions": [
        "storage"
    ]
}
						

background.js


chrome.app.runtime.onLaunched.addListener(function() {
    chrome.app.window.create('index.html', {
        'bounds': {
            'width': 1136,
            'height': 640
        }
    });
});
						

Storage API


chrome.storage.sync.get([ 'key1', 'key2' ], function(data) {
	console.log(data); // { 'key1': value1, 'key2': value2 }
});

chrome.storage.sync.set({ 'key1': value1, 'key2': value2 }, function() {
	console.log("saved!");
});
						
Chrome Web Store Developer Dashboard

iOS App Store

Don't use Apache Cordova
Use Ejecta
Overview of Ejecta

ejecta.include("game.js");

ejecta.openURL("http://twoscoopgames.com/", "Open the Two Scoop Games website?");

ejecta.getText("Highscore", "Please enter your name", function(text) {
    console.log(text);
});

ejecta.loadFont("path/to/my/font.ttf");
						

HTML5 Supported Features

  • <canvas>
  • <audio>
  • <image>
  • localStorage
  • WebSocket
  • XMLHttpRequest
  • Page Visibility
  • Multitouch
  • Device Orientation / Motion
  • Geolocation

Other Supported Features

  • Game Center
  • In-app Purchases
  • iAd
SyRUSH on the iOS App Store

Other Deployment Targets

  • Windows/Mac/Linux Desktop (and Steam!) via node-webkit
  • Android via Ejecta-X (needs some love!)
  • Wii-U eShop
  • Windows RT / Windows Phone 8
  • Firefox OS

THE END

BY Eric Lathrop