Design of Processed Tower Defense
For the last four days I've been working on a tower defense style game using Processing.js, and today riding a train into Nagoya I put my last hour of work into it. At this point it is a full featured although not particularly fun game. This article is going to briefly overview the state of the project, and then discuss some of the more interesting implementation details.
Current Status
At the moment there are only two types of towers, and one type of creep. There is a user-aimable missile attack, and implementing similar types of user attacks would be quite doable. There is a creep wave controller, which periodically spawns creep waves, which periodically spawn creeps. Gold per creep goes up with each level (linearly), and creep hp goes up exponentially with each level.
All together including blank lines and comments it is just below 800 lines of Javascript, and--as I'll examine below--its a fairly flexible setup. The most important unimplemented feature would be a way to upgrade and configure towers. Since you can already display a tower's radius by clicking on it, this would just involve writing some upgrade paths, and some kind of UI for the user to interact with1.
Anyway, lets break down the code a bit.
Main Game Setup & Loop
The setup and game loop are deceptively simple. The draw loop (which handles all the game logic as well) is a pleasant five lines long.
var start_tower_defense = function() {
setup = function() {
set_canvas("tower_defense");
reset_game();
size(SET.width, SET.height);
frameRate(SET.framerate);
mouseMoved(on_mouse_moved);
mousePressed(on_mouse_press);
}
draw = function() {
if (SET.state != SET.game_over_state && SET.state != SET.pause_state) {
background(SET.bg_color);
update_groups(SET.rendering_groups);
}
}
setup();
}
Of course, what we really want to know is "What the hell does update_groups do?" The answer to that is: everything. Everything except for managing user interface state (which answers questions like "Is the player placing a tower?" "Are they aiming a missile?" etc) is handled by this update_groups function, but it is rather simply implemented.
update_groups takes one argument, an array of arrays of objects which satisfy a simple protocol. That protocol consists of three methods:
- update is used to update any values.
- is_dead is used to remove objects that are done being used.
- draw explains how to render the object.
The array of arrays is traversed backwards, because the index of a contained array represents the depth at which it is rendered. Thus something rendered at a depth of twelve should render before something at a depth of seven. Within a depth's array, objects are rendered in standard order (i.e. 0,1,2,n-1,n).
var update_groups = function(groups) {
var obj_update = function(x) {
if (x != undefined) x.update();
};
var obj_is_alive = function(x) {
if ( x == undefined || x.is_dead()) return false;
return true;
};
var obj_draw = function(x) { x.draw(); };
for (var i=groups.length-1;i>=0;i--) {
var group = groups[i];
if (group != undefined) {
group.forEach(obj_update);
var alive = group.filter(obj_is_alive);
alive.forEach(obj_draw);
groups[i] = alive;
}
}
}
First update is called on all objects, then is_dead is used to filter out objects that are no longer needed, and then draw is used to render the objects for the player's viewing pleasure. This simple pattern seems to apply well to any scenario involving large numbers of (somewhat) concurrent processes3.
Really, understanding this simple function is almost all you need to know to understand the game's implementation. Now we'll look at one quick example of implementing this simple api.
The game stores the current time in a global variable, so that all events in one execution of update_groups are calculated using the same value for the current time3. The function that handles that is defined as follows:
var SettingUpdater = function() {
var su = new Object();
su.is_dead = function() { return false; };
su.draw = function() {};
su.update = function() { SET.now = millis(); }
assign_to_depth(su, SET.system_render_level);
return su;
};
var setup = function() {
// ...
SettingUpdater(); // initialize SU
// ...
}
The one potentially unclear bit is that the assign_to_depth function is used to place an object into a rendering group, and SET.system_render_level is the lowest of all rendering groups (meaning that it is processed first). It is a rather simple pattern, and a common one in the code.
As a final example of this approach, lets take a look at how the game's grid is implemented.
var Grid = function() {
var grid = new Object();
grid.update = function() {};
grid.is_dead = function() { return false; };
grid.draw = function() {
stroke(SET.grid_color);
var p = SET.pixels_per_square;
var w = SET.width;
var h = SET.height;
for (i = 0; i<w; i+=p) {
line(i, 0, i, h);
}
for (i = 0; i<h; i+=p) {
line(0,i,w,i);
}
};
assign_to_depth(grid, SET.grid_render_level);
return grid;
};
Again, this simple three method api is implemented, and then the object is placed in a rendering group, and thats all there is to it.
User Interface
Other than the three method api for updating and rendering, the rest of the code deals with a three function api for the user interface. This time the three functions (zero or more may be undefined as convenient) are:
- state_legal is a function to determine if the selected action can be performed where the mouse currently is.
- state_action is the function to call if mouse is clicked (and the action is legal).
- state_draw is a function to draw any ui feedback necessary from the moving mouse (for example the radius surrounding the mouse when placing a tower or aiming a missile).
Lets look at the code using this api first to get a feel for what exactly its doing, and then we'll take a look at some code that implements this api afterwards.
This api's implementation resides in two methods, on_mouse_moved, and on_mouse_press, which--respectively--are invoked when the mouse is moved, and when the mouse is pressed. Together they look like this:
var on_mouse_moved = function() {
var pos = mouse_pos();
if (SET.state != SET.normal_state && SET.state_legal) {
if (SET.state_legal(pos.x,pos.y) == true)
SET.bg_color = SET.bg_colors.positive;
else SET.bg_color = SET.bg_colors.negative;
}
else {
SET.bg_color = SET.bg_colors.neutral;
}
if (SET.state_draw) SET.state_draw(pos.x,pos.y);
};
var on_mouse_press = function() {
var pos = mouse_pos();
if (SET.state == SET.normal_state) {
var gpos = pixel_to_grid(pos.x,pos.y);
var tower = get_tower_at(gpos.gx,gpos.gy);
if (tower != false) select_tower(tower);
}
else {
if (SET.state_legal && SET.state_legal(pos.x,pos.y) == true)
SET.state_action(pos.x,pos.y);
set_state_normal();
}
};
Basically the game is either in normal state, or in a special state. If it is in a special state, then that three function api dictates the user interface. If it is in normal state then buttons (defined in the html) can change it into a special state, or clicking on a tower can change it into another state (really any click or mouse press could theoretically change it into another state, if the listening code was added).
Next lets look at the code that allows implements placing a missile towers:
var build_missile_tower = function() {
var cost = 100;
if (SET.gold >= cost) {
SET.state = SET.placing_tower_state;
SET.state_legal = function(x,y) {
var gpos = pixel_to_grid(x,y);
return can_build_here(gpos.gx,gpos.gy);
};
SET.state_draw = function(x,y) {
var gpos = pixel_to_grid(x,y);
var mid = center_of_square(gpos);
var radius = SET.pixels_per_square;
SET.rendering_groups[SET.killzone_render_level] = [];
BuildRadius(mid.x,mid.y,radius);
}
build_tower_mode();
SET.state_action = function(x,y) {
var gpos = pixel_to_grid(x,y);
MissileTower(gpos.gx,gpos.gy);
SET.gold -= cost;
set_state_normal();
}
}
};
The state is activated by pressing the html button labeled "Build Missile Tower". For this ui state, any location is legal unless there is already a square or tower in the current grid square. The state_draw method draws a small circle to let the player know where they are pointing, and the state_action method creates a tower in the selected square and then takes the player's gold.
All the other user interface modes are implemented in the same way.
Summary
Those two patterns are used to implement all of Processed Tower Defense, and have turned out to be pretty helpful in keeping the game simple to understand and extend. In certain places the way I use the rendering loops borders on hackery, and the being a bit more explicit would help readability (for example, I assume that the tower rendering layer will only have towers, and the creep rendering layer will only have creeps, a reasonable assumption, but one that should be more explicitly stated).
Thats really all there is to it, hope that this gives some ideas to those interesting in game development.
The UI would either be some html construct on the sides, or I was thinking of having upgrade options popup in the grid squares surrounding the currently selected tower, this would be visually pretty neat.↩
Part of my efforts to make the game play consistently under varying framerates and remain sane even if it starts lagging. To finish framerate independence all the draw functions would need to be rewritten, but it wouldn't be too hard to do. The non-drawing logic is already framerate independent (spawning waves, spawning creeps, firing weapons, etc). It would be kind of fun to complete the framerate independence (and would make gameplay more consistent between browsers. Games really are a sinkhole for time, because there is always something else you can do to make the experience more interesting or perform better.↩