Introduction to ES6 Modules for Game Programmers

As an application (be it a game or anything else) grows, so does the need for some sort of organization. Also, all but the simplest programs will require the use of some sort of additional libraries. For both of these reasons, just about every programming language meant for serious use has a way to separate code into logical, self-contained blocks. Even lowly C has its header files, but more modern languages can do more.

JavaScript, however, used to be sorely lacking in this department. Sure, you can load additional scripts in a web page, and jQuery (to name one example) managed an entire plugin architecture using DOM onload events. But everyone recognized that this wasn’t enough, and thus modules were born.

The problem is, they weren’t a part of the JavaScript “core”, so there was no standard way of making or using a module. Node made its own module system based off the CommonJS spec (later used by Browserify), while Require.js championed a slightly different style called AMD. In general, server-side and other “app”-style JS used Node’s modules, since they were already using Node itself, while purely browser-based libraries went with AMD. As with any case where there are two competing standards, developers are left having to learn both of them.

Now, with ES6, all that can change. Modules are now a core part of the language. Once browsers fully support them, you can use them anywhere. (Babel can convert ES6 modules into AMD or CommonJS, though, if you want to use modules right now.) And this is a case where you’ll definitely want to, no matter what you’re writing.

Using Modules

Most of the discussions out there focus on writing modules, rather than using them. But game developers, in particular, are going to be more likely to use somebody else’s module first, so I’m starting there.

The import keyword is your gateway to modularity. If you’ve ever used require() in Node, you’re halfway there, but you’ve still got more to learn. The idea is pretty simple, though. Let’s say that we’re using some made-up game library that’s fully modularized, so that all its classes (because it’s all ES6, see?) are in their own module files. Well, we can do this:

import Vec2d from "lib/vector2d";

Now Vec2d is available for us to use in the rest of that script, and it will be whatever the vector2d module exported. Presumably, that would be a class or a function that created a 2D vector.

That’s the most basic import. For modules that export a single value (class, function, or even a constant), it’s all you have to do. In fact, the way the standard was made, it’s actually the preferred way of doing things. But it’s not the only way. What if we have a module that gives us multiple exports?

import {normalize, dotProduct} from "lib/vector2d";

That gives us names for two functions that might be defined (and exported) in our hypothetical vector2d module. Of course, we can combine these two:

import Vec2d, {normalize, dotProduct} from "lib/vector2d";

Here, the default export (see below) is assigned to the name Vec2d, while the braces indicate exports that we’re “picking” from the module. If we don’t like the names the module gives us, no problem:

import Vec2d, {normalize, dotProduct as dotP} from "lib/vector2d";

Finally, if we have a module that has a lot of exports (maybe something like jQuery), we can use a “wildcard” import that brings in the whole module as an object:

import * as $ from "lib/jquery";

We do have to name the object we’re importing into. (I used the traditional $ for jQuery.) After we use this import, anything the module exported with a name will be available as a property on the $ object.

Note that all importing is done statically, so you can’t “conditionally” import like this. (This is one downside to ES6 modules compared to earlier attempts like Node’s.) For that, you need to use the new Module Loader API, which would look something like AMD:

System.import("lib/jquery")
    .then(function($) {
        // Use jquery module as $, just like always
    });

Creating Modules

Eventually, you’ll want to make your own modules. For a game, you might be using a classical OO approach, where all your game entities (enemies, powerups, etc.) are ES6 classes, each in their own file. Like we did above, we’ll start with the simplest case, where your module only has one value, particularly a class.

/* enemy.js */
export default class {
    // Class definition...
};

That’s all there is to it. export default is the magic phrase that tells your JS interpreter that you’re defining a module and that you want it to export a single value. That value could be anything: class, function, constant, or even another module. And, when you want to use your enemy, all you have to do is import it like we did earlier.

If you want a module with more than one export (maybe a library of independent functions), then you can use a syntax that’s almost the reverse of that for importing multiple values:

/* utils.js */
export function log(msg) {
    console.log(msg);
}

export function foo(bar) {
    // Some other function
}

export const MAX_FPS = 60;  // TODO: Lower to 30 for console builds

All of these will be exported, and they can be imported using the “braces” or “wildcard” versions of import. (Since there’s no default export, these are, in fact, the only two ways to use the module. A simple import utils from "utils"; wouldn’t help us here.)

If you don’t like writing export everywhere in a module like this, you have an alternative. Instead, you can write the module as you normally would, then add an export declaration at the end. This declaration consists of export followed by the name of each expression you’re exporting, all of them put in braces, like an object literal. In other words, like this:

/* utils2.js */
function log(msg) {
    console.log(msg);
}

function foo(bar) {
    // Some other function
}

const MAX_FPS = 60; // TODO: Lower to 30 for console builds

export {log, foo, MAX_FPS};

The main advantage of this style is that you can export a function (or whatever) under a different name, using as like we can when importing. So, for example, we could instead write the last line as export {log as debug, foo, MAX_FPS};.

In Closing

So modules are good, and ES6 modules make JavaScript even better. Combined with classes, we now have a language that makes writing programs (including games) much easier. Once support for modules becomes widespread in browsers, all JS game developers will reap the benefits that “native” devs already enjoy.

Leave a Reply

Your email address will not be published. Required fields are marked *