In 1980, Atari released a first-person tank game for video arcades, called Battlezone. It used a vector graphics display like their earlier Lunar Lander and Asteroids games, and featured wireframe rendering of 3D objects on a virtual battlefield. It has spawned numerous sequels and spin-offs, official and un-official, including one released more than 35 years later.
The game has multiple hardware and software components, including:
This disassembly covers the 6502 code and the vector graphics commands in ROM. It includes the math box interface but does not disassemble the code that runs inside it. I used "Battle Zone (rev 2)" ROMs for MAME (which you can find easily on the web), but you can also use data extracted from the 2KB ROM chips.
Battlezone is copyright 1980 Atari, Inc.
Related resources:
The easiest way to play is via emulation on a computer. MAME works very well for this. Once you have the Battlezone ROM set (probably called "bzone.zip"), save it in the MAME "roms" folder. When you start the MAME UI front-end, it will find the game automatically. Select it from the list and hit Enter. When the system summary displays, hit Enter again. (If this doesn't work for you, please visit one of the many MAME help sites.)
You can get a list of keys by hitting Tab, selecting "Input (this Machine)" with the arrow keys, and hitting Enter. The most important are:
The Tab menu has a "Slider Controls" section that lets you configure the display, e.g. you can disable the flickering if it annoys you. The "DIP Switches" menu will let you set the machine for free play so you can stop inserting virtual quarters.
The goal of the game is to shoot the enemy before they hit you. There will be one tank or missile on the battlefield, and possibly a saucer. Enemy tanks and missiles appear on radar, obstacles and saucers do not.
Scoring: 1000 for a "slow tank", 2000 for a missile, 3000 for a "super tank", and 5000 for a saucer.
There will always be one hostile unit on the battlefield, either alive or exploding. When a unit dies, the next one is chosen as follows:
For two seconds after the player or enemy unit spawns, the enemy unit is not allowed to fire. This prevents situations where the player gets killed before having a chance to react. ($6595) If the player dies, the enemy unit will proceed on a random heading for 3 seconds after the player respawns. ($5222)
The enemy has a score value that increases by 1000 points when the player is killed. The enemy's level of aggression is based on the difference between the player's score and the enemy's score. If the enemy is winning, tanks will appear in front of the player, move uncertainly, and take bad shots. This changes gradually until the player is out-scoring the enemy by 7000 points, at which time the enemy spawns in any direction and moves with full aggression. ($69fd) Even when initially mild, the enemy will become more aggressive ~17 seconds after it has spawned.
The enemy's spawn position is determined as an angle and a distance. While the angle can be just about anything after the player has racked up a few kills, the distance is either 3/4 or 3/8 of $7fff ($5fff or $2fff). There is a 50/50 chance of getting a "near" or "far" spawn. Missiles are always spawned at the far point, within a few degrees of the player's facing.
Missiles have a second score threshold equal to the base threshold plus 25K points. As the player's score nears the threshold the missiles will make their final turn later and later, until eventually they're swerving until just before the collision. ($6679) The amount of swerve doesn't change, but the exact movement depends on a game frame counter, so it won't move exactly the same way every time.
Saucers start appearing at 2000 points. The delay between saucer appearances is random (0 - 17 seconds). Their initial position and movement are random, and do not depend on the player's position or facing.
Most of the technologies involved are discussed in detail on classic arcade game sites, and personal pages from engineers like Jed Margolin. This is a high-level overview that will provide some background so that the disassembly will make sense to people without prior experience.
Let's start with a map of main memory. (cf. the Battlezone hardware driver source code in MAME.)
Addr Range | Description |
---|---|
0000-03ff | RAM |
0800 | IN0 (read) |
0a00 | DSW0 (DIP switch config) |
0c00 | DSW1 (DIP switch config) |
1000 | Coin counter (write) |
1200 | Vector generator start (write) |
1400 | Watchdog clear (write) |
1600 | Vector generator reset (write) |
1800 | Mathbox status register (read) |
1810 | Mathbox output, low byte (read) |
1818 | Mathbox output, high byte (read) |
1820-182f | POKEY I/O |
1840 | Sound control (write) |
1860-187f | Mathbox command registers (write) |
2000-2fff | Vector generator RAM |
3000-3fff | Vector generator ROM |
5000-7fff | Program ROM |
There's 1K of RAM that includes zero page and stack, followed by a bunch of I/O locations. The Analog Vector Generator (AVG) has access to memory from $2000-3fff, the first half of which is RAM, the second half ROM. Finally, there's 12KB of ROM with the game executable. The AVG ROM is not limited to vector graphics data, and in fact about half of its ROM space actually holds non-AVG data like shape vertex coordinates and math tables.
The top half of the 16-bit address space is a mirror of the bottom half, e.g. the RAM can also be addressed as $8000-83ff, but Battlezone doesn't use this (except for the 6502 interrupt and reset vectors).
Battlezone has a timer that ticks at 3KHz. Every 12 ticks (250Hz), a non-maskable interrupt (NMI) fires, starting about 5ms after reset (except in factory test mode, when the NMI is disabled). The main code is responsible for computing the next frame and generating drawing commands, while the interrupt handler manages sound effects, coin drops, and command buffer swaps.
The two threads depend on each other: if the main thread locks up, no new frames will be generated; if the NMI handler fails, the effects can be pretty catastrophic. Having a machine lock up in a video arcade is Not Okay, so the designers added a "watchdog" feature.
The watchdog has a timer that is reset by software. If the timer expires, the machine's state is reset. If the NMI handler fails to feed the dog, or chooses not to because the main thread appears to have stalled, the machine is returned to its initial state.
Battlezone uses a vector graphics "X-Y" display. Cathode-ray tubes (CRT) used for television or computer monitors employed a raster scan system: an electron beam scans from left to right, gradually moving down the screen. The beam intensity is manipulated to cause phosophors to glow with varying intensity, creating a pixel image. In a vector display, the software has full control of the electron beam, and can position and move it arbitrarily. This allows smooth (unaliased) lines to be drawn. The beam's speed can also be controlled, which allows lines of varying intensity to be drawn.
Lines need to be re-drawn frequently, because the phosphors only glow for a brief time. This period determines the refresh rate: too slow and solid lines flicker badly, too fast and you'll see a lot of ghosting. Unlike a raster-scan display, there is no VSYNC pulse. The game software is responsible for managing the update rate, which it does by performing a refresh on every 6th interrupt (41.7Hz).
The 6502 CPU isn't fast enough to drive the display, so a dedicated piece of hardware, the Vector State Machine, was developed. This reads the vector data from memory and uses it to direct the display hardware. To reduce the amount of space required to store the command lists, the data takes the form of an executable byte code, which defines a stack to allow subroutine calls. This means that common shapes, like letters of the alphabet, can be defined in ROM and "called" from code generated in RAM.
When refreshing the display, the VSM reads instructions 16 bits at a time, starting from offset 0. In Battlezone, offset 0 is $2000, the start of AVG RAM space. We need to be able to render the display from one set of commands while we're generating commands for the next frame. How do we do that cleanly?
The game splits the RAM into two separate 2KB segments, one at $2000 and one at $2800. The first two bytes of the segment are reserved for a "jump" instruction that branches to either $2002 or $2802. The drawing code writes commands starting at $2x02, and sets a flag when done. The next time an interrupt is received, the jump instruction is updated to point to the other segment, and a flag is set to tell the non-NMI code that it's free to start drawing again.
Addresses for subroutine calls and jumps are 12-bit addresses. Because the VSM treats memory as 16-bit chunks, a 12-bit address can access 8KB of memory.
All movement and drawing commands use relative positioning, except for the "CNTR" command, which moves the beam to the center of the screen. The addressable area of the display is roughly 1024x768 (+/- 512, +/- 384, with (0,0) in the middle), though some of that will be in the overscan area of the CRT. The hardware can handle values out to +/- 1024, and provides a "window" circuit that blanks vectors that stray outside a defined area, so the game doesn't need to clip lines in software.
Battlezone doesn't use color, but it does make some lines thicker and brighter than others (e.g. distant objects are fainter, dots on the radar pulse brightly and fade). Fading distant objects improves the sense of objects being distant, and also reduces the time required to draw the screen.
A simple AVG disassembler was used to generate comments for the source listing.
The "math box" is a separate board that performs various arithmetic operations faster than the 6502 can. It has its own ROM and can perform complex operations, like multiplying two 16-bit values, adding them together, and dividing the result by another 16-bit value. The 6502 feeds it data one byte at a time, then waits for the 16-bit result to appear.
The 6502 can continue to perform calculations while waiting for the math box to finish. This is not used extensively in Battlezone, but you can find an example in the code (see $5b1f).
Full details can be found on the math box page.
Sound is generated by a POKEY chip and by some discrete components. The POKEY provides four voices. Sound effects are tied to a specific channel.
The fanfare played at 100K points and at the end of the game when a high score is achieved (9 notes from the 1812 Overture) uses channels 1 and 2, playing slightly different tones at the same time.
The saucer sounds have a lower priority than other channel 1 effects, and will pause when those play.
The radar "ping" often cuts off the three-boop new enemy alert because they're on the same channel and there's no priority.
The discrete components provide:
It can also mute all of the audio, including the POKEYs, to silence the game when it's not being played. And it controls the LED on the Start button.
Little things you might not have noticed...
Crude battlefield map (click to enlarge):
The moon is north, the volcano is south-west. The white '+' in the middle is (0,0), which is where the player is placed at the beginning of a new game. Visibility spans nearly half the battlefield, so if you spin in a circle you can see almost every obstacle on the map.
Nobody's perfect.
The game has a couple of authenticity checks that watch for someone tampering with the copyright notice. If the tests fail, the game will start to exhibit strange behavior.
A couple of notes on the MAME v0.221 implementation:
Copyright 2020 by Andy McFadden