SURVIVOR: Remaking A Commodore 64 Game In HTML
TL;DR version: I played this game as a kid, and during a flight to Hawai'i in December 2011, was finally inspired to start a browser-based remake of it. Completion time was 2 months, between the occasional spare evening and weekend. schillmania.com/survivor. Should work in IE 8, Firefox, Opera and Safari. Related: Development process, screenshots etc.
You know what never gets old? ... Nostalgia.
8-bit Memories
Many people have fond recollections of the 8-bit, arcade-centric era of video gaming between the seventies and eighties, the so-called "Golden Age." 1978's Space Invaders was the first big arcade hit that paved way for countless imitators, and a number of successful spins on the genre. Arcades ruled the roost for some time, but eventually the home gaming market took off (and subsequently almost died) in the early 1980s.
Enter The Commodore
A typical Commodore 64 computer. (Credit)
In 1982, the Commodore 64 computer was released and proved not only to be a business-friendly appliance, but also a quite capable gaming machine. It had fast graphics, hardware-level sprite collision detection and high-quality 8-bit sound, which made it popular with developers and pimply-faced teenage customers alike. Its potential as an educational, not-strictly-for-gaming machine made it also appeal to parents, who may have been concerned about their kids spending too much time in the arcades. Having sold 17 million units since 1993, the C64 may still hold the title of best-selling personal computer to date.
Perhaps the educational angle (or my incessant pestering) helped to convince my own parents, who bought a complete C64 package second-hand from a newspaper listing for around $500 in 1986. It included the famed CBM monitor, a 1541 floppy drive, printer, modem (which I did not realize or understand until many years later) and a whole lot of hand-labeled floppies.
Piracy, Hand-Me-Down Style
It was the box of floppies included with the purchase that turned out to be a veritable treasure trove - yes, in the "arr, matey" sense - of games that had been cracked, copied and shared amongst friends, and it was through this box that I eventually found and loaded up SURVIVOR. From what I recall, the crackers in this case did not make their own intro screen, but did modify the game's title screen to mention their names.
I was only vaguely familiar, at best, with the concept of "cracked" software when I first experienced it; I was six years old at the time. It later occurred to me that these fancy intro screens, scrolling letters and music seen with some games were in fact made by people in a "scene" whose hobbies included removing copy protection, writing code with funny messages and occasionally modifying games to include strings that I was pretty sure were not part of the original game.
While I didn't have a clue how these people were able to modify and effectively "remix" games, I found it fascinating. While I eventually learned BASIC by - well, POKE-ing around (boo, hiss), the results of my efforts were limited to silly games of the text-based kind. Nonetheless, I had a blast learning and playing, and as you might guess, still have fond memories of the C64.
SURVIVOR (C64 version)
When I first loaded this game, my expectations were the same as with the dozens of other games on the collection of floppies: I frankly had no idea what to expect. The intro screen with the "Ride Of The Valkyries" was fairly minimal, but felt familiar. The "window blinds"-style text was neat. Starting the game, it quickly became apparent that this was a space-based shoot-'em'-up; it was 1982, after all, and that was a hugely popular theme at the time.
Studying The Original: "Research"
Of course, in order to understand one's destination, one must know one's origin. Alternately, as comedian George Carlin once put it: "If you haven't gotten where you're going, you aren't there yet." This meant that I was relying on memory, and a Commodore 64 emulator for the most part, in order to observe the patterns of the original game enough to be able to recreate them in HTML + CSS + JavaScript.
Here is what the gameplay was like on the original Commodore 64 version:
Survivor - C64 Longplay (YouTube)
Details: A Great Use Of Sound
When playing the game, the first thing that I noticed was the attention to detail on the sound effects. There was a fairly raucous, comical 8-bit BLAPP noise (Listen to the BLAPP noise) when beginning the level. Perhaps it was intended as a "warp" sound.
There was also an intermittent, low-frequency "heartbeat" sound (Listen to the heartbeat sound) (note: quiet) that was heard at a regular interval, and it appeared that the blinking blocks or walls of the world were pulsing in sync with this sound. The heartbeat was effectively driving the visuals of the game, and I thought that was pretty creative.
Audio As A Pressure Tactic
It was interesting to note that as I progressed through the level, the heartbeat increased in frequency and the whole game sped up with it; a subtle, and effective strategy to make the player feel a bit of tension as they each "milestone" was reached. In this case, there were four milestones, and the heart rate increased with each one. (Later favourite examples of this include Super Mario Brothers' "running out of time" effect, and Tetris' change in music/pace when your incomplete block pile-up hits a "dangerous" threshold.)
Incremental Pitching
In addition to syncing sound and gameplay, there were also little nods toward progress throughout the game. For each quiet "pop" or loud explosion when you shot something (there were two distinct sounds for a hit versus something being destroyed), the frequency of the sound would increase slightly. There were perhaps twenty semi-tones of increasing pitch before a ceiling would be reached, and the sound would reset to the lowest pitch. If nothing else, this made the sound effects less-repetitive, similar to how Wolfenstein 3D would later randomize the pitch of the "grunt" played when your character attempts to open a door.
Comic Relief Via Bad Guys
On occasion, one of three different "bad guy" characters would spawn in order to home in on your character (the ship) within the world. If you collide, you're toast, so the goal was to initially avoid and inevitably shoot them out.
What made these characters entertaining, ultimately, was the sound effects; they are best described as a sequence of randomly-generated notes, along with a bit of bit crunching applied on-the-fly. (Listen to "Bad Guy Loop 1")
This sound would play as long as a bad guy was alive and made it clear that even if they weren't visible on-screen, someone was out to get you. Visually-speaking, the characters themselves were goofy; an "X", a bird, and a smiley face - so the sound seemed to fit the visuals.
Base Explosions
My favourite sound effect in all of SURVIVOR has to be the base explosion. When the last turret on a base was shot out, the whole base would make a brief, buzzing 60-Hz sound that grew in intensity followed by a loud, crunchy 8-bit explosion sound (also keyed off of the aforementioned pitch effect, I might add.) (Listen to "Base Explosion Sound")
The sound, including the matching visuals and the resulting speed-up in the game's heartbeat, made destroying a base a very rewarding goal - something you get to do up to four times before passing the level.
SURVIVOR (HTML + CSS + JS Prototype)
Thirty years after the original game's release, here is the result of my efforts:
It should be mentioned that my take on this is an interpretation of the original game, rather than a direct emulation, port or conversion; the intent here was to recreate many of the original gameplay features, look and feel, and behaviours in a modern browser environment, strictly for the fun and challenge of doing it.
Some aspects of the original game were omitted or modified during development of this "tribute" to the C64 version, but overall the goal was to produce something that would have the effect of "taking back" people who had played the original, invoking a bit of that nostalgic factor.
Inspiration at 30,000 Feet
Code on the back of a chewing gum package: "A Mai Tai (was) my code co-pilot." Via Flickr.
They say necessity is the mother of invention; for better or worse, I have always remembered that idiom as being the other way around. In the case of the SURVIVOR remake project, it happened to be inconvenience being the mother of inspiration: The circumstances that planted the seed for the project included a five-hour flight to Kaua'i, a Mai Tai, a fully-charged laptop battery and no in-flight WiFi.
I was idly poking around on my desktop, wondering what to do without a wireless connection, when I recalled that I had the VICE C64 emulator installed and had plenty of time to play some old games. Ultimately, that led to doing just that: Playing some old games.
Eventually, I loaded up SURVIVOR and remembered that I'd been meaning to try a HTML-based prototype/remake of it for some time. Given I still had several hours to kill and suspected HTML was capable of doing the job, it felt like a perfect opportunity to start prototyping.
Reverting To Old Habits
To get started I created a .js file in Notepad, my legacy Windows editor of choice, and began writing a large comment block describing the game. My laptop's battery eventually gave way, but I felt I was on a creative roll and and didn't want to stop - so I carefully unrolled and flattened a chewing gum package and continued working on paper. From there, I was able to hash out the overall object structure of the game, outlining the major objects, methods, properties, events, core logic and so forth which I could later factor into actual code.
Writing and editing code on paper - whether high-level ideas, pseudocode or actual instructions - is something I have done since I was a kid. It used to be habit to print reams of code for road trips and summer vacations; while away from home, I would make "offline edits" to programs that I had been working on, often adding entirely new blocks or segments of code which I would later type in after returning. It was fun to revert to this method for prototyping, after many years of being accustomed to having a laptop readily available.
Pen and paper were used again under the same circumstances a week later on the return flight, but by this time I had a working prototype and was hand-writing nearly-functional code with assumptions about naming conventions, object structures and so forth in some cases.
Game Structure
Objects
These are some of the major elements identified in the game, using my own naming conventions.
- Ship: Your character. Your goal: Fly around, shoot everything, and don't crash into anything (or you die.)
- Blocks: Basic, static elements used to shield Bases. Can be shot.
- Walls: Elements that connect to form a Base. Immune to gunfire.
- Turrets: An "armed" wall (also a Base component) which fires a single shot at regular intervals. Can be shot.
- Bad Guys: These sporadically respawn and chase the player. Turret gunfire can also kill them.
- Spaceballs: Green slime-ball-like asteroids that float idly around the world. Immune to everything.
Special "Meta"-objects in this case are Bases, which are defined by contiguous groups of Walls and Turrets. They represent the targets which must be destroyed in order to pass the level.
Logic, Rules + Behaviours
Some observations made while playing the original, used to define gameplay logic:
- Blocks, Walls and Turrets collectively form a Base.
- Blocks animate in sync with the world's heartbeat.
- Bases are logically made up of contiguous Walls and Turrets that form enclosures.
- A base explodes once all of its Turrets have been destroyed. (Blocks do not count toward destruction, but merely serve as a deterrent.)
- Turret gunfire dies before hitting the first Block in its path. The gunfire end-point is fixed and does not change even if the Block is destroyed during gameplay.
- Turret gunfire will stop and continue unaffected through Ship gunfire.
- The ship has some friction despite being in space, and can move in tiny increments if thrust is applied quickly enough.
- Ship gunfire dies when it hits the edge of the screen.
- Ship gunfire is small enough to pass through the small space between adjacent Blocks.
- Bad Guys have varying chase / follow algorithms that give them unique behaviours.
- Bad Guys can fly untouched over everything, except for gunfire.
- Ship and Turret gunfire both kill Bad Guys on collision and continue passing through, unaffected.
- Spaceballs can reverse direction on random axes when "bouncing" off of Walls, Blocks or Bases.
- Spaceballs will stop Ship gunfire.
Gameplay
The objective of the game is to take out the four Bases by destroying their respective Turrets whilst killing Bad Guys, avoiding Turret gunfire and Spaceballs at the same time.
When the game begins, the player warps in at a fixed (safe) location. From there, they are free to fly around the world and shoot targets at will. The bases can be destroyed in any order, but it is wise to consider taking out the more heavily-armed, or difficult ones first.
Other Bits
The ship is also armed with three Smartbombs, whose sole purpose is to kill active Bad Guys. They are convenient when the ship is in a tight spot or when evasion is difficult.
Recreating The Game: A Technical Overview
The Return Of DHTML (or, "Look Ma, No Canvas!")
Firstly, an interesting note: The choice was made to go old-skool "Dynamic HTML" style in recreating this game using native DOM elements, rather than using <canvas>
for rendering the UI.
The choice was made primarily because I'm more familiar with using plain old HTML elements vs. canvas for rendering. It was also a fun challenge to try to write a game that would run at smooth frame rates without using canvas. Furthermore, I knew that the native DOM approach would get the same advantage of hardware acceleration in many cases in addition to the usual native layout, pattern-matching and styling of individual objects via CSS.
General Approach: Objects, State + DOM Updates via CSS
function Ship() {
// Pseudo-code structure example
var css, data, dom;
css = {
thrust: {
up: 'thrust-up',
right: 'thrust-right',
down: 'thrust-down',
left: 'thrust-left'
}
};
data = {
dying: false,
dead: false,
};
dom = {
o: null
};
function thrust(xDirection, yDirection) {
if (data.dying || data.dead) {
return false;
}
}
function init() {
if (!dom.o) {
dom.o = badGuyTemplate.cloneNode(true);
}
}
function destruct() {
// remove from the dom, etc.
if (dom.o) {
if (dom.o.parentNode) {
dom.o.parentNode.removeChild(dom.o);
}
dom.o = null;
}
}
return {
'init': init,
'destruct': destruct,
'thrust': thrust
};
}
Speaking from a high level, each major object in the game needs its own events, methods and properties in order to track and update its state. The ship representing the player is a good example as it has position, firing and thrusting states, all of which change dynamically according to user input.
From the UI perspective, objects also create and append some elements to the DOM for their visual representation on-screen. In addition to tracking state as an internal property, CSS classes and class name changes are used to set and directly affect the render state of DOM elements in many cases. When applied efficiently, JS-driven CSS class name changes can be a powerful thing.
Javascript-To-HTML Object Mapping
Regarding code patterns, one I find myself repeating includes defining collections underneath objects such as css (for class names), data (local object state), dom (element references, collections and so on), events, and in some cases, objects (related or "child" instances, eg., gunfire objects on a ship.)
These collections are flexible for most use cases, and remain distinct enough that they group separate things together nicely. With this pattern I know where to quickly look up things in the DOM, reference CSS class names, call event handlers and access child objects without much effort. Object-specific methods are typically defined on the object top level, and can be called directly.
game.objects.ship.thrust('right'); ->
#ship.thrust-right { ... }
Thus, for example: When the Ship()
object instance has its thrust()
method called, it can easily check its own data.direction
property to figure out whether it needs to change directions. If changing it can update its internal direction state, and then apply CSS changes to the dom.o
DOM node reference according to the value of data.css.directions[data.direction]
. Thus, a method call applies changes to the local object state as well as the DOM via CSS in a clean and consistent fashion.
The ship can thrust on one or two axes at once, and it's rather elegant to apply both directions to the class name for the DOM element and define a CSS rule for that case, e.g., #ship.thrust-down.thrust-right { transform: rotate(135deg); }
vs. the single-direction case, #ship.thrust-right { transform: rotate(90deg); }
. This pattern is used throughout the game to help separate the logical state from the UI state, simultaneously strengthening the use of CSS in terms of driving the UI.
Explicitly Create And Destroy (or, "Collect Your Own Garbage")
It can also be helpful to have init()
and destruct()
methods. In the constructive case, you may wish to create a number of objects at once but also defer their initialization when they may do "expensive" things like appending or attaching events to the DOM, generally regarded as being very slow compared to native script execution.
Depending on your use case, creating a number of objects up-front and later calling the init function within a second loop may be more efficient. You can also minimize layout/reflow costs by appending to a document fragment or other "offline" node within your init functions, before "finally" appending the fragment to the live DOM in one fell swoop.
The same logic can apply to destruction methods which may remove event listeners, DOM nodes and ultimately null out DOM and/or internal object references. If you can, it's generally good to clean up after yourself when an object has reached the end of its life; make the browser's work easier for it whenever possible for make benefit of glorious state of performance win.
In many cases, objects within SURVIVOR (eg., gunfire, bad guys, blocks, turrets, base walls) will "self-destruct" when they die, removing their elements and their references from the DOM - ideally, lightening the script engine and DOM parsing/rendering load.
Recreating The Game: Getting Started
"Perhaps you'd better start from the beginning."
#world
, World Map, Tiles, Window Dimensions And Scrolling
SURVIVOR map, V1: ASCII JavaScript array-style. The top and bottom bases (walls and turrets) are defined.
Among the first things built were the "world" container element, the background tile and the concept of a ship object that would represent the player.
Next up, some window dimension logic to track viewport sizes, updates and scrolling was added.
Keypress listeners were added for applying ship thrust, so I could move around the space (one 32-pixel grid square at a time) and use window.scrollTo()
as needed to keep the ship in view.
At some point I needed to create a map that would define the world, so I spent a lot of time studying the original game map, flying around, pausing and taking screenshots in order to accurately recreate it in ASCII form. It took a lot of counting rows and columns to line everything up, but eventually it all seemed to work.
The original game tiles were 16x16, from what I recall; I went with 32x32 for the remake, so the game wouldn't look like live microfiche in the modern world of HD-size displays. Even at 2x scale, much more of the world is visible in a browser when compared to a CRT (or an emulator window) in the original game.
The Game Loop
At the heart of every game is a main loop which drives object motion, animation, game state and UI updates at a regular interval, often targeting 30 frames per second for the UI "thread." To get things started, a basic loop function was written which handled the motion of the ship and window scrolling, with an eye toward collision detection.
Once the ship could be flown around, the next step was to make it fire.
Pew, Pew, Pew!
A game prototype isn't really much fun until you can shoot at things. In this case, when the fire key was held, new ship gunfire objects would be created and their elements would be appended to the DOM in order to be drawn and animated at a fixed interval.
In terms of motion, most objects have an animate()
method which updates the object's x/y coordinates, and/or cycles its CSS-based background sprite image in order to show the correct frame of an animation sequence depending on its state. The ship and its child gunfire objects, in both cases, simply move on x/y axes in regards to animation as they have no "explosion" or other states which require spriting.
As with many other objects in the world, ship motion needed to be constrained to the dimensions of the world, which was ultimately determined by the size of the 2D array that defined it - so min / max rules were added to the ship's positioning method.
Target Practice: Bad Guys
With ship gunfire working, the next objective was to add some enemies and rudimentary collision detection to enable things to go "boom." This meant recreating "bad guys" from the original game.
In my implementation, BadGuy()
instances would have a 25% chance of respawning "every n seconds", a random number between 10 and 25 which was newly randomized with each iteration. I wasn't sure what the original game did, so I played around with the numbers a bit until it felt about right.
As observed in the original game, there were three types of bad guys and each had their own follow / chase behaviour. I took the simplest, dumbest-thing-possible approach; all three make a bee-line for your ship, adjusting their X/Y coordinates to target your ship's centroid with each iteration of the game loop.
Each bad guy has a random amount of "jitter" applied per frame, so they appear slightly unstable while iterating through their animation frames. While moving, their coordinates are compared to those of the ship. If an intersect is found, then a collision condition is met and both objects explode()
and die()
, triggering a death animation sequence and eventual removal of nodes from the DOM once applicable destruct()
methods run.
Ten Days In
Before long, I had a functional prototype with the basics of ship navigation, blocks and bad guys, and the ability to shoot both - including the requisite sound effects, via SoundManager 2, naturally. Sound "sprites" were used for the two explosion types having ten distinct sounds each, reducing the number of potential HTTP requests from 20 to 2.
Creating The World
Parsing The Map, Creating The Objects
SURVIVOR map, V2: UTF-8 box drawing characters are now being used for walls and turrets, making the mapping much more literal. (View full-size for detail.)
At some point, objects would need to be created for the static blocks, walls and turrets at the coordinates defined by the 2D map array. Iterating through the array, a simple character-to-JS-constructor map was used to create the appropriate object by type, sub-type and/or direction.
Naturally following the visual mapping, the array [row][col]
offsets dictated the x/y
position of each object.
Digging through character sets, it seemed all too appropriate to use UTF-8 box drawing characters to literally draw the four types of walls and turrets. A "type 1" vertical turret, for example, could be represented by a ┻
character.
When creating a Turret()
instance, parameters for type, sub-type and direction would also be passed to the constructor. The parameters mapped to CSS class names applied to the DOM node, which dictated the UI for the given object.
Following the type / sub-type / direction pattern, the applied CSS would match the rule div.turret.type-2.up { ... }
for the previously-mentioned Turret()
instance.
Performance Considerations: Less DOM, More Speed
Creating a lot of these objects in a loop can be potentially expensive, particularly on the DOM side, so a few simple optimizations were made.
An old favourite trick involves creating and caching "template" DOM element structures which can then be duplicated via cloneNode()
within each object constructor. At the very least, structures including child nodes don't have to be incrementally created from scratch with each new instance using this approach.
Additionally, appending to the live DOM from a loop is expensive as it can cause massive amounts of reflow and layout. In the map case, all object elements are applied to an "offline" documentFragment
, which is then added to the live DOM with a single appendChild()
call after the loop completes.
Parsing The Map: Results
With the ship, bad guys, and static objects being created and appended to the DOM based on the map data, the game was starting to look and feel more like the original.
Survivor Prototype: Bases V1. Via Flickr.
Breathing Life Into Map Objects
Once map items were being created and present in the DOM, the next step was to add their animation effects and other "live" components. Blocks remain stationary, but animate through several frames; turrets regularly create gunfire (laser fire?) objects, which move at a fixed rate until hitting the nearest block.
The Game Loop: Now With 100% More Heartbeat
The "heartbeat" feature of the game loop is responsible for increasing the speed and intensity of gameplay while the player progresses through the level, and runs separately from the animation loop. Based on the timing of the sound effects in the original game, the heartbeat interval cycles through values of 500, 366, 233 and 100 msec as each base is destroyed. Specifically, the heartbeat "pulse" event drives audio, block animations and turret gunfire.
Turret Gunfire
With each pulse event, turrets check to see if they are actively firing. If inactive, a TurretGunFire()
instance is created which inherits the turret's coordinates and direction. The gunfire object's "endpoint", where it dies, is determined based on the coordinates of the nearest block sitting in the path of the gunfire. Despite that endpoints may differ, the pulse event has the interesting side effect of synchronizing turret fire.
Once a turret is actively firing, its animate()
method (called regularly via the game loop) moves the related TurretGunFire()
child object element through space at a base velocity, plus a multiplier assigned by the game loop depending on the level of "intensity."
Animating 554 block elements simultaneously with ... CSS (!?)
Blocks also animate with each pulse event, as the game loop cycles through each of its four "phases." In this case, CSS updates the backgroundPosition
of a sprite to show the correct frame for each phase.
Performance-wise, the worst approach would involve looping through a collection of all block elements and updating their class names. A more efficient option may have been finding and changing only the elements on-screen; however, it would still include a loop and then tracking on vs. off-screen elements.
Rather than risk premature optimization, I opted for simple and dumb, hoping browsers would be efficient - and it seems to work well in most desktop cases: A single class name change on #world
to "phase-2", despite matching 554 block elements and updating their background position, appears to be quite smooth. It may be that some browsers are smart and don't do the work of rendering or updating off-screen elements, which should reduce the load significantly.
Snapshot: Turret Gunfire (with boundaries)
Block animation is shown here, as well as turret gunfire. At this point, the concept of a logical "base" is nearly ready to implement.
Collision Detection
"Writing collision detection algorithms makes me want to smash things."
Pixel collision map (0s omitted for legibility)
My first attempts at a collision detection function were very simplistic, and were usable for some but not all situations within the game. In some cases, near-pixel-level detection was needed for the game to do the right thing.
Again, the simplest/dumbest argument was a factor here; a function was written that would compare two bounding boxes for overlap, and if found, would compare the overlapping regions at the pixel level. If a 1 and a 1 were found at the same location, a collision had occurred.
The tightest spaces in the game were some pathways into bases, where the ship had to fly between walls that occupy part of a grid space, leaving only a pixel or two of room on either side for the ship to pass through.
Other interesting collision cases included enabling the ship gunfire to fly through (between) turret gunfire uninterrupted, and allowing ship gunfire to pass through the small natural gaps between blocks - behaviours which were both observed in the original game.
Collision Checks: Macro (Object Maps), Before Micro (Pixel Maps)
While math is fast in JS, it's always preferable to do less work. As part of the game loop, objects compare themselves to other objects they're "interested" in, to determine whether there's been a collision or not as of the current frame. The ship ends up being the comparison point for a lot of these checks, since it can hit and be killed by just about everything.
To stay fast, things are compared first at the grid level: An xyToRowCol()
method determines the row and column for an object's location and does a quick lookup on a few "object maps" for other items that it may be interested in.
With each iteration of the game loop, the ship location will be compared against the world map to see if an active block, wall or turret exists at that location. If none is found, the "Bad Guy" map, turret gunfire map and so on are checked in similar fashion. If an object is present in any of the relevant maps at the ship's location, then a more precise check is performed within that location to see if there is an actual overlap. This can involve comparing the overlapping slices of the two objects at the pixel level in some cases.
In terms of tracking moving objects like bad guys and turret gunfire, each object simply registers itself with its appropriate map at its present [row][col]
as it moves around the world. As a new location is registered, the previous one is deleted. Lookups are quite fast this way, and the pattern is similar to the world map.
Performance: Profiling + Testing
GPU FTW
Earlier prototypes of the game were able to run at 50 fps with an aggressive setInterval()
-based timer in Safari, Chrome and to a lesser extent, Firefox in many cases, thanks mostly to hardware-accelerated rendering. When present, GPU acceleration can shift a lot of rendering load off of the CPU.
The difference in performance can be night and day for UI or graphics-heavy applications, and it's pretty great to have GPU-accelerated compositing for common HTML elements above and beyond just <canvas>
.
The DOM, Again (or, "Treat The DOM As Read-Only")
It's worth stating again: DOM reads and writes should be considered as expensive, and minimized whenever possible. I'm quoting Stephen Woods in saying, Treat The DOM As Read-Only. I agree.
Given you are typically controlling the logic within the JS of your application, you should know where everything should be in terms of state and position; don't use data-
or "expando"-type attributes if you don't need them, and don't read from native properties like offsetHeight
or className
unless you really need to. From what I recall, just reading offsetHeight
can cause layout and/or reflow to happen in the browser, which is terrible. If you must read these values, cache them if you know they are not going to change.
Furthermore, as previously mentioned, minimize the number of live DOM writes. Generate and cache DOM element structures "offline" whenever possible, cloning, modifying and appending structures to a documentFragment
before appending the fragment to the live document element.
Work Aversion (or, "Out Of Sight, Out Of Mind")
A simple performance test panel. (Flickr)
Given there can be a lot of animated elements between turret gunfire, bad guys and spaceballs, it may be worthwhile to hide (via display: none
) or remove these nodes entirely from the DOM when they go off-screen, restoring them only when needed. I tried this as an experiment in performance tuning, and it did seem to help the frame rate under Firefox. With desktop Safari, the difference was negligible as it was already hitting the maximum 50 fps target I had set.
In theory, display: none
may help to lighten rendering / layout as there is less to be displayed. Removing the element from the live DOM was a further optimization attempt following this logic, the theory being that perhaps CSS matching and other operations would be marginally faster with fewer elements.
In both cases, the game engine would track the objects' position, state and so forth, but would not write to the DOM; operations like setting style.left
and style.top
were omitted to help avoid further reflow / layout.
During prototyping, I added a small test panel that would allow disabling of CSS effects and sprites, and display toggling of all major game elements. It was interesting to see which features affected the frame rate the most. It turned out that disabling things like the world "pulse" effect, hiding turret gunfire elements and avoiding window scrolling brought the frame rate up a lot in Firefox, much closer to the 50 fps I originally had as a maximum frame rate.
You can try this feature out for yourself, via #profile=1.
Of course, reducing the size of the browser window should cut down the amount of rendering work, giving the CPU a break if it's pegged and affecting frame rate. This is evident for me in Firefox, my browser of choice, on OS X; for reasons unknown, window scrolling seems to affect the frame rate in Firefox the most.
Nitpicks And Miscellaneous Bits I Didn't Get Quite Right
A short list of annoyances, unsolved bugs and other places where I gave in:
- When respawning after being killed, sometimes the ship will be adjacent to a turret about to fire. (The fix: Check for and avoid turret gunfire paths.)
- Detection of the inner areas of contiguous walls - where the ship, and spaceballs should never appear as they would be stuck - is not automated, and can break down in user-generated levels. The ship's initial spawn point is fixed, in part, for the same reason.
- On occasion (and when the ship is moving?), diagonal ship gunfire can pass through walls.
- I'm pretty sure I didn't catch all of the audio semi-tones that are cycled through as things are hit, and explode.
- Ditto for the pitching of the larger explosion sounds.
- The on-screen / visibility logic is a bit flawed; sometimes ship gunfire dies before the true end of the screen, and off-screen things appear part-way into view.
- I tried out
requestAnimationFrame()
, but initial attempts to use it didn't seem to deliver as high framerates as the oldsetInterval()
method. Presumed I was doing something wrong; would like to revisit, etc.
Other Stuff: The Level Editor
Once you write the engine, you need to feed it different data in order to find and work out the bugs. ;) In this case, that meant making a level editor which would spit out map data in a format the game could parse.
The fun part was making the "palette" of blocks, walls and turrets, and then painting the grid with blocks. Basically this boiled down to watching mousedown()
, mousemove()
and mouseup()
events for drag-and-drop-style interactions in the most complex cases.
After playing around a bit, I realized I wanted conveniences like keyboard shortcuts for selecting from the palette, the ability to fill a space with blocks or a perimeter wall using click and shift + click, and finally the ability to draw walls following the direction of my mouse, like the "snake" game.
After a few days, I had a more-or-less working level editor; the next step was to get the resulting huge array serialized and run through encodeURI()
for passing to the game as a potentially-massive URL parameter. It's ugly, but it works.
I also threw in a few fun CSS3 transform and transition effects so items "zoom in" when being drawn.
Survivor Prototype: Level Editor Test (YouTube)
Because the editor is free-form, you can break the design mold implied by the original game and put together some pretty challenging levels that are completely different; a level full of blocks, for example, with a few long tunnels with turrets firing through the only open spaces. Alternately, a super-base completely lined with turrets at slightly different distances, which means a steady and intermittent wall of deadly gunfire you have to carefully dodge.
Fin.
That, I think, is just about everything. Thanks are due to Richard Carr (Synapse Software, 1982) who wrote the original Atari 2600 version of the game, and Ewing Soft, who made the Commodore 64 version I know and love from 1983.
For the record, I wrote all of the code for this project myself. Image assets (with the exception of the explosions and spaceballs) were created using The GIMP, based on reference screenshots taken of the original game running in the VICE C64 emulator. The sound effects are sampled from the original game, and as such are not included in the GitHub repo.