Armor Alley: Web Prototype
TL;DR version: I played this in the early 1990s on an old 8088-based IBM-PC compatible; this is my interpretation of it. Play now at /armor-alley. Related: Development process, screenshots etc. Also on GitHub.
A browser-based interpretation of the classic 1990 game.
Armor Alley is a side-scrolling simulation involving strategy. The primary goal is to get a van across the battlefield to the enemy base. Of course, it is not that simple; the van is completely unarmed, and incredibly vulnerable. You must defend the van with a convoy of tanks, missile launchers and other units on the ground, in addition to providing air cover with your helicopter against identical enemy forces.
The Armor Alley Web Prototype home screen. Play it now or read more about the original game, units and strategy on Wikipedia.
A quick fly through of the AA Web Prototype in tutorial mode. Basic mechanics, defensive and offensive tactics are explained.
Why do this?
... Because.
Any application that can be written in JavaScript, will eventually be written in JavaScript. -- Atwood's Law (2007)
Writing a browser-based game using HTML, CSS and JavaScript is a great way to learn, re-learn and experiment with approaches to client-side programming. Games require a depth of logical thought, planning and effort to build, and encourage experimentation in architecture and development style.
Performance and scalability are both important factors in building games, and both present learning opportunities in regards to writing performant code that is compatible with a number of platforms and devices. Regardless of fidelity, a working game prototype can be both educational and fun.
The reference: Armor Alley, PC-DOS version (1990)
Premise
Build and defend convoys of Tanks, Missile Launchers, Infantry, Vans and Engineers as they cross the battlefield, using your helicopter for both offense and defense. The goal is to get your van to the enemy base on the other side of the battlefield.
Studying the original
PC-DOS, 1990 (port from original Macintosh version)
Side-scrolling, fixed-height, 4-bit colour
Reusable patterns (vehicles, shrapnel, gun fire, smoke, explosions)
Low-fi animations
Numerous interactions / behaviours between vehicles in the environment. Numerous, but somewhat shallow and limited complexity; relatively straightforward implementation.
Scope
Entirely client-side technologies: HTML, JavaScript, CSS.
First "level", standard vehicles and terrain elements including anti-aircraft turrets / "guns", but no armored bunkers.
Basic enemy "AI", automated convoy building / ordering + enemy chopper actions / defense - likely difficult to truly emulate original behaviour.
No network / multi-player.
Process
Initial prototype: Basic landscape, terrain, vehicles, bunkers w/balloons and vehicle movement. Player helicopter can fly over terrain.
Enemy vehicles added. Basics of enemy detection, gunfire, bombs & collision detection added.
Infantry, bunkers, vehicle-specific interactions.
Status bar, fuel line landing pads + repair / refueling / reloading actions.
Smart missiles and radar system.
Inventory / ordering system.
Nearby object finding / detection (smart missiles, AI)
Troubleshooting & debugging
Chrome DevTools: Frames, memory, JS / CPU profiling
CSS transforms + JS feature detection
Hot loops, object creation / memory use / garbage collection
"Architecture"
A tightly-packed sprite containing the majority of the in-game graphics, made for the Web Prototype.
Raw (vanilla) JS, SoundManager 2 for audio
Good old-fashioned DOM elements for rendering UI vs. <canvas>
or WebGL, etc. Benefits: Natural DOM createElement()
for making game objects, CSS to style them, className
-based manipulation, transitions and animations.
JavaScript: utils
helper for CSS class name manipulation, DOM events, node tree removal, object mixins, cloning etc.
Controllers, i.e., gameLoop
iterate over collections of game objects, calling each object's animate()
method.
If animate()
returns true
, object is dead and controller can remove it from the array. Pattern is repeated for collections of vehicles, gunfire, buildings etc.
MVC-like theme: css
, data
, dom
, objects
interface is defined for major game objects.
Some objects have child and/or parent objects, i.e., bunker
<- chain
-> balloon
.
frameCount
and modulus determine interval of behaviours - movement, firing, animation rate, enemy detection and so on - i.e., if (frameCount % 10 === 0) { fire(); }
Object names + types map between array names, constructor pattern, CSS class names (generally.) e.g., a van has data.type
of 'van', CSS of .van
, stored in game.objects.vans
and so forth.
Each object has a predictable DOM pattern, CSS class name and data structure.
isEnemy
applies to JS, cascades to .enemy
in CSS. UI + collision logic, otherwise, is mostly the same.
Collision detection / enemy + object interaction
nearbyOptions
- "who gets fired at?"
nearbyObject()
- "is an X (i.e., helicopter) in range?"
Object targeting - "move toward the helicopter"
If there is an object overlap, call target.hit()
and provide "source" object interface. Target determines interaction - i.e., target may die, but may also kill source.
Animations
Combination of style.left/top
, some backgroundPosition
-based sprite animation, and CSS animations and transitions.
CSS step-based animations allow convenient className
-triggered transitions, e.g., tank explosion: .tank.dying {}
-> .tank.dead {}
animate()
method applies vX
+ vY
to x
+ y
, updates style.top/left
(traditional) or transform (GPU-accelerated) properties to reposition DOM node.
"Inheritance"
Mixin-based inheritance of data
, css
structures etc. Common CSS class names (states), data attributes like x
, y
, dead
, isEnemy
etc.
Common operations: Move sprite (DOM x/y), object hit, die are in left in a top-level common
helper, similar to utils
.
Performance
Use transform: translate3d()
where supported for GPU-based motion of elements on x/y axis, vs. traditional left/top style changes. Translate avoids expensive repaints, instead using GPU-based compositing for motion.
JS: Avoid creating excessive garbage (e.g., cloning objects mixin-style) in hot/expensive loops; reduce GC, RAM use and overall churn. Pass objects directly / by reference, avoid creating new objects or modifying original object values in loops.
Object destruction / clean-up: Remove node tree, JS/DOM references and parent array reference in the object collection case.
Minimize DOM "I/O": Cache node references and coordinates to reduce reflow due to read operations (e.g., offsetWidth
.) Update client coordinates only on critical events like init and window.onresize()
.
Web Prototype with GPU-accelerated transform: translate3d()
, and room for improvement. Note that most frames are < 16 ms, 60+ fps.
Web Prototype without GPU-accelerated transform: translate3d()
- note lots of red (expensive repaint), and very slow frames.
This is why you don't create temporary cloned objects inside hot loops; that's a lot of garbage. Screenshot shows "spikey" RAM use and garbage collection events before the expensive function is shunted via return false
. The proper fix involved changes to avoid object creation.
Memory, DOM node count and JS/DOM garbage collection
Memory and DOM node count rising and falling over time, with ideal case of DOM nodes (green line) being properly GCed as JS objects and DOM nodes they reference are cleaned up.
Normal gameplay and memory allocation is illustrated above in blue, along with a growing number of DOM Node references (green.) In this case, the helicopter's machine gun fires 64 rounds and nodes are added in linear fashion. When the gunfire objects are destroyed, things settle down and eventually a garbage collection event happens.
When JS/DOM nodes are dereferenced via JS object destruction / clean-up, the remaining DOM nodes can be properly garbage collected. A natural GC event reflects this at the mid-point, followed by the remainder of the new nodes with a manual GC event invoked toward the end.
Chrome DevTools' "Detached Dom Trees" under the Heap Snapshot Profile section can also come in handy for finding leaked DOM nodes; the detached DOM is included under the Containment View.
At time of writing (Nov 1, 2013) there might be a bug related to GPU-accelerated DOM nodes not being GCed, or simply not being reflected in the Chrome DevTools graphs. See #304689 for details.
"AI"
Actually, quite dumb. "Rule-based logic" is a more appropriate description of this implementation.
Smart Missiles: Make a beeline, plus minor deviation with acceleration changes, toward target.
Enemy helicopter: Target nearby cloud, balloon, tanks or player's helicopter if in range. Fixed acceleration rate, normalizes to 0 when "close enough" to target. Returns to base when out of ammo + bombs, fuel, or heavily damaged. Does not dodge targets nor obstacles.
Enemy can hide in clouds, will bomb passing tanks within range if applicable.
"Dogfight mode": Aim to align with player helicopter. Fire gun when within range. If player is directly underneath, try bombing. Disourages direct fly-over / fly-under.
"Targeting" flags: Clouds, tanks, helicopter, balloons. If multiple target options, logic determines priority. (Rough preference, low to high: Clouds, balloons, helicopters, tanks.)
Enemy convoy building / ordering: "MTVIE" sequence at fixed intervals - one every few minutes, depending on available funds.
Enemy helicopter has slight speed advantage, making it harder to chase or run from.
transform:translate
origin considerations
While using GPU transform: translate, on Chrome: Odd/occasional redraw issues found if style.left/top
or transformOrigin
not initially assigned. Logical; otherwise browser says, "transform this element relative to what?" ... Recommendation: Apply initial top/left 0px and/or transform-origin
values in CSS.
Sound effects
Sound greatly enhances the game experience.
Original 8-bit sounds could not be re-licensed; modern replacements (and new sounds) were mixed in from numerous Creative Commons and free sources on freesound.org. The hi-fi sounds made it more fun to blow up things, in particular.
Distance affects volume, and ideally, panning effects on sounds (off-screen sounds are more quiet, and so on.)
Miscellaneous efficiency & performance notes
Collision detection is largely just math. Caching / invalidation would probably be more expensive, not worth the effort.
Ditto for other simple coordinate checks, e.g., object nearby / on-screen / targeting.
Most time is spent in GPU/hardware, performing draw / layout / render operations.
Things that worked
Consistent naming convention within objects, public interface via exports = { css, data, dom }; return exports;
Common methods: animate()
, hit()
, die()
etc.
Object arrays (vans, tanks, bunkers) + single top-level controller, loop which calls animate()
on each item and removes "dead" items accordingly.
Collision accepts object exports
(interface), standard properties like data
and hit()
.
hit()
accepts optional point value, and source/target object. In some cases, both objects can be damaged or destroyed.
JS swapping CSS class names based on state: .enemy
, .dying
, .dead
and so forth.
Common object "create" methods, optional configuration option/param eg., isEnemy
, x
, y
, vX
, vY
frameCount
-based intervals setting animation + behaviour rate, e.g., move()
every frame, fire()
only twice a second, detect enemies once a second etc.
Nearby & "collision" configuration - easily determine "who gets fired at", eg., tanks -> infantry. Default "lookahead" affects vehicle's ability to "see" in front of it.
"utils" for basic DOM events, CSS class name manipulation, node tree removal.
Batch DOM changes, particularly queueing and removal of nodes as the result of object destructors (i.e., a bunch of GunFire
object instances dying.)
In retrospect: things I would change or revisit
Revisit object, data & function inheritance - could most all game objects inherit from a "sprite" base of some sort?
Smarter collision detection algorithms could be researched and implemented.
Event broadcasting? Would this be smart to use in terms of abstraction? (Still not sold.)
Different "exports" / API per-object? More abstraction, less assumption about css
, data
, dom
?
Better "sprite" abstraction per-object. Easier DOM manipulation?
Sprites in CSS earlier on / SASS / compass for automatic optimization
Avoid writing any setInterval()
/ setTimeout()
calls. Currently used for post-explosion delays before object destruction (DOM node removal, object clean-up.) Smarter: Use existing animation loop to apply an action after a given number of frames, and destroy object that way. This has been partially implemented.
TODO: tweaks and other bits to tidy up
Remove setTimeout()
calls used for destruction of objects, move toward using animate()
+ frameCount
for timing instead (some of this is implemented - see FrameTimeout()
in code for reference.)
Re-review object creation, memory allocation and garbage collection. Currently not too bad, but always room for improvement. Object pooling could be used for common objects like gunfire, etc.
Further optimization considerations: Image sprites and sound sprites, where applicable. Remove animated .GIFs in favour of sprites + CSS animations, if it is faster / smoother.
Features not in the original game
Tutorial Mode: Guided introduction to game mechanics, tasks and basic strategies.
Cloud hiding / cloaking from radar (perhaps implemented in original multiplayer mode - not sure.)
Enemy helicopter can hide in clouds, and can bomb passing tanks ("Stealth cloud bombing mode".) Alternately, player can pass over enemy anti-aircraft guns, missile launchers and the enemy base, as well as the enemy helicopter, undetected. Adds a fun element to the game. Sneaky.
Additional sound effects for helicopter, parachute infantry, infantry gunfire, van jamming and shrapnel hit.
Stuff I'll never get to
Multiplayer. Not for lack of interest; I think it'd be great, just don't have the time.