Rory's Blog
Home
Blog
Projects
Facebook
Google+
Github
RSS

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.