Building my Own Craft(y)
11/15/15
So, of the various, admittedly-few JavaScript game-libraries I've tried, I quite like Crafty. I'm thinking I might want to make my own at some point, and to me that means I should dig into Crafty's construction and get an idea of how it ticks. From there, I may have some ideas on the game library I want to make.
So, I've forked the main Crafty repository so that I had my own, and from there started digging around.
For some reason, I thought that Crafty was made in CoffeeScript, but it turns out only a few files are in the language. I guess I'll figure out what that's all about at some point.
In any case, I figured if there's some coffee in there anyways, I
might as well make that rather ugly
Gruntfile.js
a not-so-ugly
Gruntfile.coffee.
So this:
require("coffee-script");
var fs = require('fs');
var open = require("open");
module.exports = function (grunt) {
var pkg = grunt.file.readJSON('package.json');
var version = pkg.version;
var banner = '/**\n' +
' * <%= pkg.name %> <%= pkg.version %>\n' +
' * <%= pkg.author.url %>\n *\n' +
' * Copyright <%= grunt.template.today("yyyy") %>, <%= pkg.author.name %>\n' +
' * Dual licensed under the MIT or GPL licenses.\n' +
' */\n\n';
var docGen = function() {
var outputFile = "./build/api.json";
var sourceParser = require("./build/parseSourceDocs");
var apiGenerator = require("./build/parseNodes");
var sourceFiles = grunt.file.expand('src/**/*.js');
var blocks = sourceParser.parse(sourceFiles);
var jsonObject = apiGenerator.structureBlocks(blocks);
var apiJSON = JSON.stringify(jsonObject, null, 4);
grunt.file.write(outputFile, apiJSON);
grunt.log.writeln("Wrote api data to " + outputFile);
};
var apiServer = require("./build/api-gen/dynamic-server.js");
function runApiServer() {
var done = this.async();
apiServer(grunt, "./build/api.json");
setTimeout(function(){
open("http://localhost:8080");
}, 100);
}
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
usebanner: {
dist: {
options: {
position: 'top',
banner: banner
},
files: {
src: ['crafty.js']
}
}
},
browserify: {
dist: {
files: {
'crafty.js': ['src/crafty.js']
},
options: {
transform: ['brfs']
}
},
debug: {
files: {
'crafty.js': ['src/crafty.js']
},
options: {
debug: true,
transform: ['brfs']
}
}
},
watch: {
files: ['src/*.js'],
tasks: ['build:dev']
},
uglify: {
options: {
banner: banner
},
build: {
src: 'crafty.js',
dest: 'crafty-min.js'
}
},
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'tests/**/*.js'],
options: {
trailing: true,
ignores: ['tests/lib/*.js'],
globals: {
}
}
},
qunit: {
all: ['tests/index.html']
},
'node-qunit': {
all: {
deps: 'tests/lib/helperFunctions.js',
code: 'tests/index_headless.js',
tests: [
'tests/common.js',
'tests/core.js',
'tests/2d.js',
'tests/logging.js',
'tests/controls.js',
'tests/events.js',
//TODO add these once isometric adapted:
//'tests/isometric.js',
'tests/math.js',
'tests/model.js',
'tests/storage.js',
'tests/systems.js',
'tests/time.js',
'tests/tween.js',
'tests/issue746/mbr.js',
'tests/issue746/pos.js',
'tests/2D/collision/collision.js',
'tests/2D/collision/sat.js'
],
setup: {
log: {
errors: true,
//tests: true,
globalSummary: true
}
},
done: function(err, res) {
if (!err)
grunt.log.ok("NODE TESTS SUCCESSFUL");
else
grunt.log.error("NODE TESTS FAILED");
}
}
},
jsvalidate: {
files: ['crafty.js', 'tests/**/*.js']
},
connect: {
server: {
options: {
keepalive: true
}
}
},
open: {
api : {
path: 'http://localhost:8080/',
},
}
});
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-jsvalidate');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-banner');
grunt.loadNpmTasks('grunt-node-qunit');
grunt.registerTask('version', 'Takes the version into src/version.js', function() {
fs.writeFileSync('src/version.js', 'module.exports = "' + version + '";');
});
// Build development
grunt.registerTask('build:dev', ['browserify:debug', 'usebanner']);
// Build release
grunt.registerTask('build:release', ['browserify:dist', 'usebanner']);
// Building the documentation
grunt.registerTask('api', "Generate api documentation", docGen);
// Default task.
grunt.registerTask('default', ['build:dev', 'jsvalidate']);
// Run the test suite
grunt.registerTask('check', ['build:dev', 'jsvalidate', 'qunit', 'node-qunit', 'jshint']);
// Make crafty.js ready for release - minified version
grunt.registerTask('release', ['version', 'build:release', 'uglify', 'api']);
// Run only tests
grunt.registerTask('validate', ['qunit', 'node-qunit']);
grunt.registerTask('api-server', "View dynamically generated docs", runApiServer);
grunt.registerTask('view-api', ['api', 'api-server'] );
};
Became this:
require "coffee-script"
fs = require "fs"
open = require "open"
module.exports = (grunt) ->
pkg = grunt.file.readJSON "package.json"
version = pkg.version
banner = """
/**
* <%= pkg.name %> <%= pkg.version %>
* <%= pkg.author.url %> *
* Copyright <%= grunt.template.today("yyyy") %>, <%= pkg.author.name %>
* Dual licensed under the MIT or GPL licenses.
*/
"""
docGen = ->
outputFile = "./build/api.json"
sourceParser = require "./build/parseSourceDocs"
apiGenerator = require "./build/parseNodes"
sourceFiles = grunt.file.expand "src/**/*.js"
blocks = sourceParser.parse sourceFiles
jsonObject = apiGenerator.structureBlocks blocks
apiJSON = JSON.stringify jsonObject, null, 4
grunt.file.write outputFile, apiJSON
grunt.log.writeln "Wrote api data to #{outputFile}"
apiServer = require "./build/api-gen/dynamic-server"
runApiServer = ->
done = @async()
apiServer grunt, "./build/api.json"
setTimeout (-> open "http://localhost:8080"), 100
grunt.initConfig
pkg: pkg
usebanner:
dist:
options:
position: "top"
banner: banner
files:
src: ["crafty.js"]
browserify:
dist:
files:
"crafty.js": ["src/crafty.js"]
options:
transform: ["brfs"]
debug:
files:
"crafty.js": ["src/crafty.js"]
options:
debug: true
transform: ["brfs"]
watch:
files: ["src/*.js"]
tasks: ["build:dev"]
uglify:
options:
banner: banner
build:
src: "crafty.js"
dest: "crafty-min.js"
jshint:
files: ["Gruntfile.js", "src/**/*.js", "tests/**/*.js"]
options:
trailing: true
ignores: ["tests/lib/*.js"]
qunit:
all: ["tests/index.html"]
"node-qunit":
all:
deps: "tests/lib/helperFunctions.js"
code: "tests/index_headless.js"
tests: [
"tests/common.js"
"tests/core.js"
"tests/2d.js"
"tests/logging.js"
"tests/controls.js"
"tests/events.js"
#TODO add these once isometric adapted:
#"tests/isometric.js"
"tests/math.js"
"tests/model.js"
"tests/storage.js"
"tests/systems.js"
"tests/time.js"
"tests/tween.js"
"tests/issue746/mbr.js"
"tests/issue746/pos.js"
"tests/2D/collision/collision.js"
"tests/2D/collision/sat.js"
]
setup:
log:
errors: true
#tests: true
globalSummary: true
done: (err, res) ->
if not err
grunt.log.ok "NODE TESTS SUCCESSFUL"
else
grunt.log.error "NODE TESTS FAILED"
jsvalidate:
files: ["crafty.js", "tests/**/*.js"]
connect:
server:
options:
keepalive: true
open:
api :
path: "http://localhost:8080/"
# Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks "grunt-contrib-uglify"
grunt.loadNpmTasks "grunt-contrib-jshint"
grunt.loadNpmTasks "grunt-contrib-qunit"
grunt.loadNpmTasks "grunt-contrib-watch"
grunt.loadNpmTasks "grunt-contrib-connect"
grunt.loadNpmTasks "grunt-jsvalidate"
grunt.loadNpmTasks "grunt-browserify"
grunt.loadNpmTasks "grunt-banner"
grunt.loadNpmTasks "grunt-node-qunit"
grunt.registerTask "version", "Takes the version into src/version.js", ->
fs.writeFileSync "src/version.js", "module.exports = \"#{version}\";"
# Build development
grunt.registerTask "build:dev", ["browserify:debug", "usebanner"]
# Build release
grunt.registerTask "build:release", ["browserify:dist", "usebanner"]
# Building the documentation
grunt.registerTask "api", "Generate api documentation", docGen
# Default task.
grunt.registerTask "default", ["build:dev", "jsvalidate"]
# Run the test suite
grunt.registerTask "check", ["build:dev", "jsvalidate", "qunit", "node-qunit", "jshint"]
# Make crafty.js ready for release - minified version
grunt.registerTask "release", ["version", "build:release", "uglify", "api"]
# Run only tests
grunt.registerTask "validate", ["qunit", "node-qunit"]
grunt.registerTask "api-server", "View dynamically generated docs", runApiServer
grunt.registerTask "view-api", ["api", "api-server"]
I think it looks better and is quite a bit more readable. I think, even if I don't stick with CoffeeScript (more on those thoughts in a later post), I'd probably keep it around for Gruntfile configs, as it just looks more like a config should, I think.
A Quick Tangent on Arrow Functions
I also found out that there's a difference between "=>" and "->" in CoffeeScript. Best shown with a quick example of the compile differences:
# CoffeeScript
x = ->
return this
y = =>
return this
// JavaScript
(function() {
var x, y;
x = function() {
return this;
};
y = (function(_this) {
return function() {
return _this;
};
})(this);
}).call(this);
The skinny arrow (actual CoffeeScript terminology) actually returns whatever "this" the function in JavaScript gets fed (there are various ways for a function to have a proper this, and various ways for it to have an improper or undefined one).
The fat-arrow function definition creates a function that has a clear clojure reference to the this of the area where the function is created.
Interestingly enough, this is similar to ES6's implementation of the arrow function vs the original function functionality.
Here I've made a file called
es6.js:
var thing = {
foo: 'bar'
};
this.bar = 'foo';
var this_thing = function () {
console.log(this);
};
this_thing.call(thing);
var not_this_thing = () => {
console.log(this);
};
not_this_thing.call(thing);
$ node es6.js
{ foo: 'bar' }
{ bar: 'foo' }
Using "function" to create the function made it possible to feed in a this with the call command. Trying the same thing with "=>" did nothing, as this was the same this was a clojure of the this the function was made in.
Back to Crafty
So, once I realized that Crafty wasn't in CoffeeScript, I wondered, "Why the Gruntfile?"
Well, as it turns out, Crafty isn't one large JavaScript file, it's a smartly laid out bunch of files that are consolidated into a single file using browserify, and then from there, if you want, you can minimize it using uglify.
There's also tests! Written in a unit-test framework called QUnit. Another testing framework to add to the pile.
There's also stuff in the Gruntfile to generate the API documentation, and start a localhost server to look at it. Which is decently neat!
Generating my Own Crafty
So, I have a copy of crafty.js from a download a while ago. Now, this can be the crafty.js that I build, and because I'll have the regular crafty.js when I need it, I can serve the minified one instead (for the sake of it).
And I'll also make sure it's built properly when I push to my server repo by updating my site update script:
git pull origin master
git submodule update --init --recursive
npm install
node scripts/htmlGenerate.js
node scripts/rssGenerate.js
node scripts/indexGenerate.js
cd Crafty
npm install
grunt version build:release uglify
And now I symbolically link my site's copy of crafty.js to point to my build of crafty.js, and I managed to not break anything.
Woo!
But... Why Do This At All?
....
Because I can?
Well, there are other reasons, but I'll share those in another post.