Palagpat Coding

Fun with JavaScript, HTML5 game design, and the occasional outbreak of seriousness

Summertime Cleanup

Thursday, June 27, 2013

Some of my readers may recall that a few months ago I left my former position to take a leadership role with a Silicon Valley startup. Now, after several months of cross-country flights and a ton of frequent-flier miles, the kids are out of school for the summer. That means it's time to make a cross-country move: cleaning and renting out our Maryland house, finding a home for our chickens and superfluous furniture, and moving ourselves and the rest of our worldly possessions nearly 3000 miles west.

Which adds up to my blogging and side-projects taking a backseat for a few weeks. Maybe you've noticed.

That said, I was looking at the landing page for my TangleJS game library this afternoon, and realized it was kinda hideous. So I set aside an hour, rolled up my sleeves, and cleaned it up a bit. It's not going to win any design awards, but I'm a lot happier with it now. YMMV.

Hmm, on second thought...

Monday, June 17, 2013

I think instead of making Saturday my weekly update here, we might as well say it'll be Monday, since the last several have all fallen on Monday anyway. :-P

Last week, I wrestled with the idea of doing a fork of the Breakouts project, given that I pretty publicly said on Twitter that I was going to do so in June. Well, reality sunk in shortly thereafter, when I realized that it's already the middle of the month, and my Tangle game engine isn't really capable enough yet to really tackle a Breakout clone. Soo...

Tangle needs a more robust answer for handling user inputs. Right now, the InputManager class can handle keyboard events of various stripes, and that seems to be working pretty well. But there's no mouse or touch support yet, and there's a whole class of target browsers (i.e. mobile ones) that I can't reach until that's in place. So the game I choose to tackle this month is going to force me to finish that capability, which will incidentally force me to finish the next entry in my Let's Make a Canvas Library blog series:

a simple snake game
Yeah, Snake.

Snake is simple, yet enables me to play with all kinds of different input vectors: keyboard, mouse clicks & gestures, touchscreen tap & swipe, et cetera. Seems like the perfect choice.

One Game Down

Monday, June 3, 2013

So, that was May.

I actually shipped something, even though I didn't get everything done I would've liked. Actually, I spent all my spare cycles last week trying to get touch events added to Tangle's InputManager, so Quilt would have mobile support, but I ran out of time without getting it fully functional. Sigh.

That also meant I didn't get many more levels added to Quilt (it currently sports 8, and the difficulty curve is a little too exponential... level 7 should be around 10 or so, and level 8 shouldn't come in until 15 or 20), nor did it get the level-reset command I wanted to add, even though that would have been like 10 minutes of work. At most.

I may still sneak that one in, in fact.

The Importance of Moving On

I read Jeff Atwood's blog, Coding Horror, pretty regularly, and even though I don't always agree with everything he has to say, this post left a pretty strong impact on the way I approach software development:

At the end of the development cycle, you end up with software that is a pale shadow of the shining, glorious monument to software engineering that you envisioned when you started.

It's tempting, at this point, to throw in the towel -- to add more time to the schedule so you can get it right before shipping your software. Because, after all, real developers ship.

I'm here to tell you that this is a mistake.

Yes, you did a ton of things wrong on this project. But you also did a ton of things wrong that you don't know about yet. And there's no other way to find out what those things are until you ship this version and get it in front of users and customers.

So, yeah. Quilt's out in the wild now, and if anyone wants to submit feedback on Github, Twitter, or G+, I'll accept it.

I'll also keep tinkering, because that's what I do. But it's time now to turn my attention to June's game for #1GAM.

More on that soon.

Quilt, the De-Tangler

Monday, May 27, 2013

Quilt and TangleJS symbols intertwined I'd said I was going to post something new here every Friday night. Then I went out of town on a house-hunting trip, and things got kind of busy. So... I missed blogging this past Friday, but what I did do, is get Quilt to the Minimum Viable Product stage, meaning it's online and you can play it, even if all the mechanics have yet to be ironed out.

Play Quilt, my #1GAM entry for May

The cool thing about working on something new that's not a blog post or Tangle tutorial is that my Tangle game library actually grew as a result of something I needed for this project, instead of me having to deliberately grow it by design. That was kind of a "duh" moment for me, actually: for the past year or so, my blogging here has gotten a little wrapped around the axle of the Let's Make a Canvas Library car, so to speak, and I didn't have the spare attention to work on anything else, or really to even work on Tangle as much as I would like. Now, in the past 2 weeks, I've put together a game (albeit a simple one), and Tangle has organically grown in the process! So, yeah. I should've been doing it this way all along, methinks.

I'm not finished with Quilt yet; there are a few more features I think it really needs to be "done" (mobile support, more levels, and level-reset command top the list), but to honor the spirit of One Game a Month, when May's over, I'm going to stop tinkering with it and move on to the next game on my backlog. :)

In my next post, I'll try to talk in more detail about Quilt, and the changes it prompted me to make in Tangle.

js13kGames: On Polish

Tuesday, September 11, 2012

In my last post, I said that I had the basic tile mechanics (swapping, sliding, and group matching) in place for my js13kGames entry, Blak & Bloo. At the time, I felt like the next thing I needed to tackle was polish:

To have a satisfying experience, though, requires more polish... it's going to require custom tile graphics, animations, and tweening of tile movements. So next, I'm going to need to write a Sprite class.

Me, last time

So with this in mind, I spent a few hours scouring the Internet for suitable (and usable) fruit & berry images, drawing a custom "picnic basket" style background to replace the plain black frame in the prototype, and modified the codebase to use these images in the render loop. There's a definite difference in the "before" and "after" screenshots of this change:

Blak & Bloo gameboard with basic, colored-box tiles
BEFORE

This colored-boxes version kind of looks like one of those optical illusions, doesn't it?

Blak & Bloo with improved, image-based tiles
AFTER

Overall, I'm pretty happy with the direction the art's going, although I'll want to tweak it somewhat as we go along, so it doesn't get too "busy".

Once I'd made these changes, I decided to check the js13kGames contest's Facebook page and saw that several entries had already been posted. I went over to check out what kinds of things other people were doing, and was blown away by the diversity and quality on display!

Holy Miyamoto, I've got to step up!

Hitting the 13KB Wall

Unfortunately, I got trapped in a mental cul-de-sac of sorts — I really want the polish that comes with the better graphics, but they made the payload much bigger than the contest's 13-kilobyte limit!

I tried a few things to shrink my footprint, with varying degrees of success:

  1. I base64-encoded the images as data URIs, hard coded at the head of my JavaScript code... no size savings there, because I'm using PNG images, which are already pretty efficiently stored
  2. I opened up my images in The GIMP again, and meticulously removed all the extraneous fields of color I could, if they could conceivably be drawn in by canvas fillRect() calls at runtime. This helped a bit, but not nearly enough to get my under the size limit.
  3. Finally, I realized what should have been an obvious avenue for size-reduction: I dropped the color depth of the images from 24-bit (RGB / true color) to 8-bit (limited to an optimized 256-color palette). This last avenue proved to be the most fruitful: I'm now back under the 13kb limit, with room to spare.

Along the same lines, I'm beginning to realize that my reliance on my minimal game framework, TangleJS, is making things bigger than they need to be. Case in point: the state machine code. But that deserves an entire blog post, so I'll save that for next time, probably in the post-mortem.

Two days to go!

js13kGames: Library Design and Core Mechanics

Friday, August 31, 2012

js13kGames logo Last week I started working on a game for the js13kGames competition, to see if I could do something fun and cool with open web technologies in less than 13 kilobytes. It's been an interesting week, I've learned a couple of things about my workflow, and I've almost got something playable to show off (maybe next week).

Need-Driven Library Design

The first thing I've run into is how incomplete my in-development HTML5 game library, TangleJS, still is. I've made a couple of decent games from scratch in the past, and started abstracting ideas and lessons learned there to make a better, more deliberate platform for my future efforts. I'm chronicling that process in my blog series, Let's Make a Canvas Library. The mistake I made was in thinking that it was in any way ready for me to start actually, you know, using.

The game I'm building for js13kGames, Blak & Bloo, is my first real effort to build something using the barely-just-enough-library-and-no-more philosophy that's driving Tangle's development. At every step, when I found some new feature I needed (e.g. sprite animations, state machine transition notifications, etc), rather than just grabbing something that worked in a previous game and hacking it down to fit my immediate need, I've tried to take a step back and deliberately design something more general-purpose that can be easily reused as part of the overall Tangle library. It's... well, it's slowed me down during what should have been a quick week of prototyping.

That said, I believe the extra effort I've spent doing this hasn't been wasted; on the contrary, Tangle won't ever be really useful to me unless I extend it organically by growing it on demand in this way. It's just made progress slower and less interesting than I would've liked it to be. Thus, I haven't really had much to blog about. At least, not until yesterday.

Game Mechanics

As a reminder, my vision for Blak & Bloo is a 2-player, cooperative/competitive Match-3 puzzle game. As in all games of this style, the goal will be to line up groups of 3 or more matching tiles (in this case fruits and berries) to score points. The “hook” I alluded to last time, the thing that I hope will make the game fun to play, is that Player 1 and Player 2 have totally different ways of interacting with the game... to make tile matches, Player 1 can only swap adjacent tiles, and Player 2 can only shift rows and columns along their axes:

tile-swapping vs tile column shifting

So far, I've got the code written to make these swaps and shifts (and any resulting group matching) happen instantly, and the tiles are rendering themselves as simple swatches of color, like in the image above. To have a satisfying experience, though, requires more polish... it's going to require custom tile graphics, animations, and tweening of tile movements. So next, I'm going to need to write a Sprite class.

js13kGames: Kickoff

Sunday, August 19, 2012

js13kGames logo This past Monday marked the beginning of a cool new 1-month competition called js13kGames, where the aim is to use open web technologies (i.e. JavaScript, HTML5, and CSS3 — no plugins) to build an online game. I've had a couple of ideas simmering on the back burners of my brain for the past few months, as I've been working on my Let's Make a Canvas Library blog series, and I've decided to take this contest as an opportunity to grab one of those ideas and front-burner it for the next 4 weeks. Should be a fun exercise, and when the month's over, I hope to have something neat to show for it.

Design Brainstorming

So the original kernel of my idea was to make a simple puzzle game in the Match-3 vein, something like Bejewelled maybe, but to somehow change the dynamic. The trick is, the genre is pretty crowded with innovations, so it's a little hard to come up with something that's never been done before. A less-than-innovative game could be saved with awesome artwork or music, but sadly, my artistic skills aren't quite at the level where they could redeem an otherwise-boring game, so that's not really an option.

About the same time I started trying to think of clever Match-3 game hooks, I needed to create some fake game assets for my AssetCache demo page. I hit upon the fake game name "Black & Baloo" for a title screen, and kinda liked it:

Blak & Bloo title screen prototype

The name started out as just a simple play on words, sparked by something in my gray matter about a bear named Baloo in an old favorite story, and was never intended to be any more than a throwaway for that TangleJS tutorial page. But the more I thought about it, the more I realized I could use it as the hook I was needing!

What if, I thought, I based my game around the interactions of two player characters, named Black and Baloo respectively? (I've since decided to change the names to Blak and Bloo, since the pun still works that way, and it's a little more unique). The idea of collaborative / competitive Match-3 hasn't been explored very well, I don't think, so there's a novelty factor. Plus, the two-player mechanic provides some interesting technical challenges for me to overcome, which I'll probably talk about soon.

Once I'd hit upon this core idea, my ideas began to take shape. Next time, I'll talk about another one of the game mechanics I'll be playing with to make the game (and the development process) more interesting. It's gonna be a fun month!

Let's Make a Canvas Library: Assets

Monday, July 16, 2012

Welcome back to Let's Make a Canvas Library, our tutorial series focused on the principles of building a good personal code library for writing games and other cool stuff with HTML5 Canvas. Today we're going to focus on assets: images and sounds.

Like the blog series it's modeled on, my aim is primarily educational, not to come up with an awesome library that every person on the planet is going to want to use. That said, I'll be committing each improvement to a new Github repo I'm calling Tangle, for anyone who wants to follow along.

Asset Management

preloading splash message

Unless you're really embracing your retro muse, just about any game you set out to build is going to have assets: the images and sounds that give it its unique flavor. For many games, these assets will take a while to load, especially over low-bandwidth connections like those usually found on mobile devices. That being the case, you're going to want to preload those files before starting the main game loop. And since this is something you're going to do over and over (and over) again, on basically every game you build, it makes sense to make it a part of your library.

In my first few games, my preloading routine was limited to just images and javascript modules (when I needed sounds, I did those separately, using SoundManager2 or something similar). For a while, my preload process had two phases: first, I'd pass a list of images and modules to the Preloader class, which would then call dojo.require() on the modules, and pass the list of images along to a global ImageCache object to preload separately. When all the code and images were loaded by the Dojo loader and ImageCache, then the Preloader class would signal completion back to the calling routine. In retrospect, it probably involved a few too many steps of indirection:

//
// (in main.js)
//
dojo.addOnLoad(function init_loader(){
    dojo.require("loc.Preloader");
    dojo.addOnLoad(preload);  // when loc.Preloader is loaded, run preload()
}); // end of init_loader()

function preload() {
    window.loader = new loc.Preloader({
      images: {
        title: "res/title.png",
        map:   "res/map.png",
        floor: "res/floor.png",
        tiles: "res/underworld_tiles.png",
        items: "res/items.png",
        link:  "res/link.png"
      },
      modules: ["id.MapQuest"]
    })

    if (loader.ready()) {
        // if everything was cached (or we're on a really fast network),
        // then we're ready to proceed.
        init_game();
        delete window.loader;
    } else {
        // still waiting for things to preload; listen for onReady
        var listener = dojo.subscribe("loc.Preloader.onReady", function() {
            dojo.unsubscribe(listener);
            init_game();
            delete window.loader;
        });
    }
} // end of preload()

function init_game() {
    window.game = new id.MapQuest({
        ...
    });
}

// ------------------------------------------------
// (in Preloader.js)
//
dojo.provide("loc.Preloader");
dojo.require("loc.ImageCache");
dojo.addOnLoad(function() {
    if (!window.imageCache) { window.imageCache = new loc.ImageCache(); }
});

dojo.declare("loc.Preloader", null, {
    images: {},
    modules: [],
    _isReady: false,
    constructor: function preloader_constructor(args){
        dojo.mixin(this, args);

        for (var i in this.images) {
            // treat item as a key:value pair
            window.imageCache.addImage( i, this.images[i] );
        }
        for (i in this.modules) {
            // load each requested item and then register an OnLoad handler
            //   to be notified when they're all done
            dojo.require(this.modules[i]);
        }
        dojo.addOnLoad(function() {
            if (window.imageCache.ready()) {
                dojo.publish("loc.Preloader.onReady",[]);
            } else {
                this.listener = dojo.subscribe(
                    "loc.ImageCache.onReady",
                    function() {
                        dojo.unsubscribe(this.listener);
                        delete this.listener;
                        this._isReady = true;
                        dojo.publish("loc.Preloader.onReady",[]);
                    }
                );
            }
        });
    },
    ready: function preloader_ready() {
        return this._isReady;
    }
});

// ------------------------------------------------
// (in ImageCache.js)
//
dojo.provide("loc.ImageCache");
dojo.declare("loc.ImageCache", null, {
    constructor: function ImageCache_constructor(args) {
        this._sprites = {};
        this._loadStatus = {};
    },
    addImage: function addImage(name,src) {
        if (name in this._sprites) {
            return;
        } else {
            this._sprites[name] = new Image();
            this._loadStatus[name] = false;
            this._sprites[name].src = src;
            this._sprites[name].onload = dojo.hitch(this, function() {
                this._loadStatus[name] = true;
                if (this.ready()) {
                    dojo.publish("loc.ImageCache.onReady",[]);
                }
            });
        }
    },
    hasImage: function hasImage(name) { return (name in this._sprites); },
    getImage: function getImage(name) {
        if (name in this._sprites) {
            return this._sprites[name];
        } else {
            console.error("Image name '", name, "' not found");
            return null;
        }
    },
    ready: function ready() {
        var retVal = true;
        for (var id in this._sprites) {
            if (id in this._loadStatus) {
                retVal &= this._loadStatus[id];
            } else {
                return false;
            }
        }
        return retVal;
    }
});

Making it Better

While this approach works, there are several ways we can go about improving it:

  1. If you read my previous post, you know that crafting our modules in an AMD-compatible format takes care of code preloading, so that's one less thing we need our Preloader to do.
  2. Notice that both Preloader and ImageCache have an onReady event (implemented using Dojo's publish/subscribe methods). That's a clue that we can probably collapse their functionality into a single class.
  3. While we're making changes, and given the broad browser support for the HTML5 <audio> tag, it's probably worth broadening our cache support from just images to include audio assets as well (ultimately we may want to include other types of media like video or fonts too, but that's not necessary just yet).

Bearing these ideas in mind, here's what a new, streamlined, asset-agnostic, AMD-compatible Preloader might look like (note that I've omitted a few details for the sake of brevity; you can review the whole thing over on GitHub):

define(
    ['atto/core','atto/event'],
    function(atto,CustomEvent) {
        function constructor(args) {
            var _assets     = {},
                _loadStatus = {},
                _events = {
                    error:  new CustomEvent('tangle.assetCache.error'),
                    ready:  new CustomEvent('tangle.assetCache.ready'),
                    loaded: new CustomEvent('tangle.assetCache.loaded'),
                },
                _types = {
                    IMAGE: 1,
                    AUDIO: 2
                },
                _extension_to_type = {
                    png: 1, bmp: 1,  gif: 1, jpg: 1, jpeg: 1,
                    wav: 2, webm: 2, ogg: 2, mp3: 2, aac: 2
                };

            function _assetTypeFromFilename(fileName) { ... }

            function _createLoadCallback(assetName) { ... }

            function _createErrorCallback(assetName) { ... }

            function _addAsset(name, src) {
                if (name in _assets) {
                    _events.error.dispatch({name:name,
                      details:"Asset '"+name+"' is already in the cache"});
                    return;
                } else {
                    var assetType = _assetTypeFromFilename(src);
                    switch (assetType) {
                        case _types.IMAGE:
                            _assets[name] = new Image();
                            atto.addEvent(_assets[name], 'load',
                              _createLoadCallback(name), false);
                            atto.addEvent(_assets[name], 'error',
                              _createErrorCallback(name), false);
                            break;

                        case _types.AUDIO:
                            _assets[name] = new Audio();
                            atto.addEvent(_assets[name], 'canplaythrough',
                              _createLoadCallback(name), false);
                            atto.addEvent(_assets[name], 'error',
                              _createErrorCallback(name), false);
                            break;

                        default:
                            // unsupported asset type
                            _events.error.dispatch({name:name,
                              details:"Asset type '"+assetType+"' not supported."});
                            return false;
                            break;
                    }

                    _loadStatus[name] = false;
                    _assets[name].src = src;
                }
            }

            function _hasAsset(name) { ... }
            function _getAsset(name) { ... }
            function _getAssetType(name) { ... }

            function _ready() {
                var retVal = true;
                for (var id in _assets) {
                    if (id in _loadStatus) {
                        retVal &= _loadStatus[id];
                    } else {
                        return false;
                    }
                }
                return retVal;
            }

            return {
                TYPES        : _types,
                addAsset     : _addAsset,
                hasAsset     : _hasAsset,
                getAsset     : _getAsset,
                getAssetType : _getAssetType,
                ready        : _ready,
                events       : _events
            } // end of public interface
        } // end of constructor

        return constructor;
    } // end AMD callback function
);

Let's take a look at a few of the more interesting pieces here.

AMD Wrapper

define(
    ['atto/core','atto/event'],
    function(atto,CustomEvent) {
        function constructor(args) {

            ...

            return {
                TYPES        : _types,
                addAsset     : _addAsset,
                hasAsset     : _hasAsset,
                getAsset     : _getAsset,
                getAssetType : _getAssetType,
                ready        : _ready,
                events       : _events
            } // end of public interface
        } // end of constructor

        return constructor;
    } // end AMD callback function
);

We've seen this before, it's the same basic AMD-flavored version of the Revealing Module pattern that I showed off in the previous post in this series. As in that case, I'm using AMD's define method to specify my module's dependencies, here a couple of modules from my minimal JavaScript library Atto, and return a factory function that can be used by downstream code to create an instance of my class. So far, so good.

Communicating Events

_events = {
    error:  new CustomEvent('tangle.assetCache.error'),
    ready:  new CustomEvent('tangle.assetCache.ready'),
    loaded: new CustomEvent('tangle.assetCache.loaded'),
}
...
_events.error.dispatch({name:name, details:"Asset '"+name+"' is already in the cache"});
_events.ready.dispatch({});

This is new, but not really worth going into too much detail about here. You can think of Atto's Event class as a slightly more robust implementation of the basic pub/sub pattern, but using object literals instead of strings for the topics. As the browser progresses through the preload process, AssetCache dispatches events that can be watched by the main game code to show a loading progress bar, display a status message, or to know when the assets are available to be used:

var progressBar = new ProgressBar({min: 0, max: assetCount});

cache.events.error.watch(function(data) {
   _log(data.details);
});

cache.events.loaded.watch(function(data) {
   _log('Loaded asset ' + data.name + '.');
   progressBar.setValue(progressBar.getValue() + 1);
});

cache.events.ready.watch(function(data) {
   _log('All assets loaded.');
   progressBar.setValue(progressBar.getMax());
   game.setState(game.states.TITLE);
});

Determining Load Status

That leads us to AssetCache's bread and butter, the code that actually does the asset preloading:

function _createLoadCallback(assetName) {
    return function() {
        _loadStatus[assetName] = true;
        _events.loaded.dispatch({name:assetName});
        if (_ready()) _events.ready.dispatch({});
    }
}
function _createErrorCallback(assetName) {
    return function() {
        _loadStatus[assetName] = false;
        _events.error.dispatch({name:assetName,
            details:"Asset '"+assetType+"' failed to load."});
    }
}
...
var assetType = _assetTypeFromFilename(src);
switch (assetType) {
    case _types.IMAGE:
        // image: this is by far the simple case
        _assets[name] = new Image();
        atto.addEvent(_assets[name], 'load', _createLoadCallback(name));
        atto.addEvent(_assets[name], 'error', _createErrorCallback(name));
        break;

    case _types.AUDIO:
        // audio; this will take a little more magic, due to browser codec issues
        _assets[name] = new Audio();
        atto.addEvent(_assets[name], 'canplaythrough', _createLoadCallback(name));
        atto.addEvent(_assets[name], 'error', _createErrorCallback(name));
        break;

    default:
        // unsupported asset type
        _events.error.dispatch({name:name,
            details:"Asset type '"+assetType+"' not supported."});
        return false;
        break;
}
_loadStatus[name] = false;
_assets[name].src = src;

We have some very basic code to determine file type based on its extension, then create the appropriate DOM element, Image or Audio (we should probably wrap the Audio case in a try/catch block, since we're trying to retain as much cross-browser compatibility as possible, and not all browsers support the Audio tag). Then, we attach a couple of event handlers to the element to keep track of its loading progress, and assign its src attribute to the appropriate URL to initiate the load. The Image element has pretty straightforward load and error events, but Audio, in contrast, doesn't fire a load event when it's finished loading. Instead, you get the canplaythrough event, which fires when the browser decides it has buffered enough data from the file so that if you start playing immediately, it will have the whole thing soon enough to play without stuttering. That's close enough for our purposes.

Audio Support Still Stinks

One note on preloding audio assets: AssetCache's support for audio files will depend on your browser and operating system. On my Windows 7 laptop, Google Chrome (v20) can load and play back both the mp3 and ogg audio files, but Firefox (v13) only supports the ogg file, and IE9 only supports the mp3! Neither audio file is loadable on my locked-down work computer, where I'm stuck with IE8, and I couldn't get either one to load on my stock Android 2.3 browser either. I haven't delved any deeper into the mobile issue, but I suspect that the audio object may have actually loaded, but not fired the canplaythrough event. Or maybe it couldn't load either format, I'm not entirely sure.

We'll revisit this crazy audio situation in a later tutorial to find a better cross-browser solution. You'd think that this would be an ideal use case for a library: it would be so much nicer if your app code could just specify a single, named audio asset, and let the library figure out the right file format to load based on browser and platform capabilities. In theory, the audio element exposes a method that reports on your browser's ability to play various file types, but it's notoriously vague and generally unhelpful, so relying on it to determine a caching strategy may not be very helpful.

Demo Time!

I've set up a simple demo page that shows off AssetCache's basic functionality, albeit not yet in the context of an actual game.

AssetCache should work in all modern browsers, and as I said, it even has limited support for Internet Explorer 8. Even though Tangle is just a reference implementation and not a truly production-ready library, as a general principle I try to build things that are as broadly usable as possible, so in theory parts of Tangle could be used to build a game that gracefully degrades on older, less-capable browsers. After all, the whole principle of modular dependency loading that AMD enables means that if you want to cherry-pick certain pieces that you find useful and not use the rest of the library, you're free to do so.

Next time we'll cover the basics of building a game loop, and actually use AssetCache to build something interesting.

Resources