CC Open Source Blog

Dissecting the Liberated Pixel Cup Demo

author's gravatar

by lunpa on 2012-07-10

ABSTRACT

The Liberated Pixel Cup Demo (LPCD) was written by yours truly over the course of two weeks, prior to the art phase of the Liberated Pixel Cup contest. The demo had several intended purposes. First, to test the usability of the base tile set for building levels. Second, to show character sprites interacting with environments and to demonstrate animations. And third, to inspire. As there has been some interest in the construction of the demo, this article is an overview to how the demo was constructed. Before I go into any detail, it is worth noting that this demo was put together without really knowing how much time would have been available to work on it. Because of this, the demo progressed through several stages - each playable and a plausible endpoint - before arriving to what it is today. This is reflected in a few places in the source code, either in code that was written with the best of intentions or in code that was written to be the foundation for something that never came to be.

STRUCTURE

Complex JavaScript programs get messy pretty fast. This is largely because it is impractical to split a JavaScript program across several files. Lack of namespaces and overly verbose language features (like Object.defineGetter) probably don't help the matter. There is a ridiculous amount of information on how to organize your code and keep sane. I've yet to fall madly in love with any of these solutions.

Here's what I usually do:
I start by defining a dummy module using the object notation (I call this the 'header'). Then I monkey patch all of my functions into it. As I add function definitions and the like, I update the module to reflect the expected structure. Function stubs have comments next to them outlining the expected arguments. I don't use a closure to fake a private scope for the module. Instead, the module is organized to keep calls, callbacks, and different sorts of data separate. It makes testing your code much easier. If you want to scare people from touching something, throw some underscores in front of its name.

The program itself is split into several files, grouping code more or less by purpose. Header.js contains the module object definition, and the starting point of execution for the game engine. All of the remaining files are appended to the end of this file (the order doesn't really matter). Assembly of the program (as well as minification) is automated via a make file.

The advantages of using this organizational scheme are:

The only disadvantage I can think of is that the header must be maintained as the program is written. It isn't easy to tell if the header is maintained well, since the program can still run if function stubs are missing or some of the variables aren't defined.

GRAPHICS ENGINE

Levels are built using the program Tiled, with the level data exported to json. The levels are tiled on a 32x32 grid, which turned out to be a mistake. If I wrote this again, I would go with a 16x16 grid instead, to simplify the conversion of world coordinates to and from screen space coordinates. This is explained further in the section about the physics engine.

Tile boards are rendered upon two html5 canvas elements; an iframe between the two is where the actors are drawn. Level data may contain more than two layers, but will be automatically flattened into two layers when the level is rendered. Actors are represented with div elements; css is used to crop and position them. For actors inheriting from VisibleKind, Z-index is used to do depth sorting, which is why the actors are in an iframe. Depth sorting behavior is done on the actor's _dirty method, which may be overridden.

Art assets are fetched in the background by creating a new Image object in JavaScript. The onload callback is used to inform the engine when the resource is ready for use. When the json file for a level is being parsed, the number of pending downloads is incremented when an image download is started, and decremented on its callback. This allows for the program to wait far all of the images to finish downloading before drawing the tile boards. A similar technique is used with art assets for actors, but this is unnecessary because the asset is displayed using css. This is a throwback from when a third canvas element was used to draw the actors.

The redraw event is scheduled when the focused character's coordinates change (it might still be when any actor's coordinates change, which would be a throwback from when all actors were drawn on a canvas). Because a bunch of functions may request a redraw at once (some might do this multiple times), the first request is honored and the rest are ignored. This simplifies things quite a bit, because the request itself is inexpensive, it can be used when-in-doubt without worrying about a significant performance cost. I'm thinking of generalizing this for another JavaScript game engine I am planning, where there are various engine functions that would make sense to schedule like this. I'm thinking in that version, I'll have the scheduling function be named "please". Eg, please("redraw scene"), etc.

PHYSICS ENGINE

Physics information is stored on a 16x16 conceptual grid. Originally, this was to be 32x32, but proved to be a mistake: in some cases, this would prevent the character from walking right up to the edge of something. Because many hours of work already spent building levels would be lost by making the whole engine use a 16x16 grid, I opted for a flimsy workaround. Physics info for tiles is now one of A, N, NE, E, SE, S, SW, W, NW; which describes the wall coverage in a given graphical tile's conceptual subtiles.

The physics grid is populated during level load. Several helper functions exist to check if a given coordinate is blocked by a wall, an actor, or a warp point.

Actors that prototype AnimateKind (which also happens to be the actors which can be the focused player) have a _move_to function that initiates the walk cycle. The walk cycle function is probably the most complex singular part of the game engine. This is in part due to the fact that the character's coordinates are floating point values, not array indices. A good chunk of this code is used to make sure the character doesn't appear to be walking through walls when cutting around a corner; this had the added side effect of the movement trajectory appearing to be adaptive to obstructions despite the lack of a real path finding algorithm. Part of the complexity of this function also comes from the fact that it is possible to call events on other actors when colliding into them.

The player character is an actor. Any actor that prototypes AnimateKind can be focused as the main character. This is used in the demo a bit, allowing you to play as Alice (by default), Bobby Tables, and a secret character. Using a JavaScript debugger and a little know-how, you can take control of many other actors; such as any of the students or any of the monsters.

ACTOR MODEL

Each entity in gameplay is represented by a javascript object that contains data describing the actor, and event handler functions. Actor objects are stored in LPCD.ACTORS.registry, and there exists several helper functions to be used to manage them. If you use the api functions to create your actors, this process is entirely automatic.

There is an inheritance chain used in creating an actor, allowing different engine features to be implemented on the actors themselves while keeping the code isolated. This means that the code for things like human characters, monsters, treasure boxes, and etc are all responsible for rendering themselves in the graphics engine. These actor type constructors can be found on the header object in LPCD.ACTORS, and defined in the file actor_model.js. For the most part, these constructors are fairly concise, with the exceptions of VisibleKind and AnimateKind.

All actors inherit from AbstractKind. The most important aspect of this actor is the variable "_binding", which determines if an actor is cleared from memory or not when a new level is loaded. This allows focused actors to travel from level to level. There was going to be a feature for persistent actors, allowing for things like items and treasure, though this was never implemented. Thus, PersistentKind exists, though I don't believe anything actually uses it.

VisibleKind inherits from AbstractKind and is used to provide a presence for the actor in the graphics engine by creating a div element and inserting it into the iframe used to display actors. This object also provides world coordinates (since they're needed for drawing) to the actor. This object does not make an actor responsive to collision detection.

ObjectKind inherits from VisibleKind, and is used for inanimate objects. It provides the _blocking function, so that the actor can be used in the physics system.

AnimateKind inherits from ObjectKind. It provides the _gain_input_focus function, directional facing information, a _look_at function, and the walk cycle via the _move_to function. This does not implement any animation features, but is simply for animate objects. CritterKind and HumonKind both inherit from AnimateKind and implement animation specific features.

LEVEL SCRIPTING AND CHARACTER DEFINITIONS

Level scripts are found in the dynamics folder, and have the file name of the level they correspond to + ".js". So for example, the starting level's file name is "start1.json" (level data is found in the levels folder. I do not recommend viewing it via web browser), the corresponding dynamics script is "start1.json.js". To make it easy to clean things up when the level changes; when the level is loaded, an iframe is created and the level dynamics script is loaded within that iframe. It is given access to LPCD.API via a global variable named API; but is left blind to the rest of the engine. This allows us to dispose of the script easily by deleting the iframe.

An amusing side effect of this is if you define within a dynamics script an actor that inherits from AnimateKind, and change your input focus to this new actor and leave the level; the object for the actor remains, but none of its member functions may be called anymore. However, anything in the prototype chain still works fine provided that it was defined in the engine itself. Because of this, characters.js is used to define game-specific characters and useful objects outside of the levels and instance them from the level dynamics script via the API.instance function. Because the code was defined outside of the level, the object remains functional after the level has been flushed.

Conveniently, this behavior is consistent between Firefox and Chrome. If this behavior for scripts in iframes is standardized, I imagine this was never an intended use case.

CLOSING THOUGHTS

Overall, I'm quite pleased with how the demo turned out. There are some rough spots where it isn't clear where things are happening (eg, flushing the level actors by changing the innerHTML property of a DOM element), which I had forgotten about prior to writing this article. Despite that, I think the code is pretty usable as a game engine, and should still be fairly easy to extend. Hopefully this article serves as a guide for others to tinker with the engine, to use the code in their own projects, or even to study in building something entirely new.