Spacewar 3060
This project recreates and embellishes the first truly interactive computer game: Spacewar!
A Little History
The legendary game of
Spacewar was created in 1961, 10 years before Pong, and led to the
first-ever interactive use of the computer CRT display (it was, in fact, first
run on an oscilloscope).
Spacewar was created on a PDP-1 (not 11),
one of the enormous room-sized computers created by DEC and supported by massive
government and university funding, that ran at approximately 200 Hz (effectively
half that fast when memory was accessed). Even the simple gravity calculations
on two ships caused the game to flicker, until math lookup routines were
implemented.
Spacewar involves two dueling ships attempting to
outlast each other as they rotate and apply thrusters to avoid the strong
gravitational grip of a star in the middle of the screen. Players fire
single-pixel "torpedoes" at each other as they fly around, and can fall back on
a one-shot hyperspace "panic" button that warps to a random safety
location.
The original game machine was retired in 1975, and now serves as a
source of spare parts for another PDP-1 on display at the DEC museum in Marboro,
MA.
Source: Creative Computing, August 1981 issue, republished here.
Our Project / Problem
Description
Spacewar 3060 will depart from the
simple two-ships-and-a-star configuration of the original game, and will
implement a number of features to modernize the gameplay:
Team Members
Note:
Side-by-side function assignments listed in 'Procedures' below
| Member | Jobs |
| Eric Tyson | Team leader; game states, MAIN; gameplay, game timer
interrupt; graphics design, graphics drawing of rotated game objects,
animations, and sprites; math routines (trig. lookup) |
| Andrew Scally | Interrupt (un)installation; game input, key interrupt; sound design, asynchronous sound implementation (play sound, timer-based sound updates) |
| Alex Mihov | Data initialization, lookup (bitmap, trig. functions); data organization; menu screen coolness (design, input, and timer interrupt) |
Implementation
Our
implementation of Spacewar 3060 will involve a number of interesting
techniques and algorithm considerations that add to the complexity and overall
technical accomplishment of the project:
conf_keymap, will be set in the menu, and
incoming keystrokes will be scanned in the array to determine function.
Segments / Data / Variables
StkSeg SEGMENT stack ; stack segment
db 64 dup ('STACKSTA')
StkSeg ENDS
BufferSeg SEGMENT 'DATA' ; screen buffer to reduce flicker
db 320*200 dup ('X')
BufferSeg ENDS
ImageSeg SEGMENT 'DATA' ; All animation frames and source images
; Various game and menu element images - organization by Alex
ImageSeg ENDS
DataSeg SEGMENT 'DATA' ; Runtime data vars
; SineTable = array of 256 bytes; SineTable[i] = sin((2*Pi)i/256)*128
; CosTable = 64 byte (Pi/2) offset into SineTable
include 'sine.inc'
; NormSineTable = 256 byte array; NormSineTable[i] = sin((2*Pi)i/256)*(256/sqrt(2))
; NormCosTable = 64 byte (Pi/2) offset into NormSineTable
include 'nsine.inc'
; Various filenames for Initialize procedure to use for loading
include 'titles.inc'
; Sound - set of sound frequency arrays, each terminated with zero
; (design by Andrew)
include 'sound_data.inc'
currentSound dw ? ; offset of current tune's sound array
frame dw ? ; current frame of gameplay - AND'd for use with animation
ship1Move db ? ; ship 1 moving state: b0 = move left, b1=right, b2=thrust
ship1X dw ? ; x position of ship 1 (pixels)
ship1Y dw ? ; y position of ship 1 (pixels)
ship1Angle db ? ; angle of ship 1 in (2*Pi/256) units
ship1VX db ? ; x velocity (signed, pixels per tick)
ship1VY db ? ; y velocity (signed, pixels per tick)
ship1Shield db ? ; shield left on ship 1
ship2Move db ? ; ship 2 moving state: b0 = move left, b1=right, b2=thrust
ship2X dw ? ; x position of ship 2 (pixels)
ship2Y dw ? ; y position of ship 2 (pixels)
ship2Angle db ? ; angle of ship 2 in (2*Pi/256) units
ship2VX db ? ; x velocity (signed, pixels per tick)
ship2VY db ? ; y velocity (signed, pixels per tick)
ship2Shield db ? ; shield left on ship 2
torp1X dw 16 dup (?) ; array of torpedos' x locations for player1
torp1Y dw 16 dup (?) ; y locations (pixel)
torp1VX db 16 dup (?) ; x velocities (signed, pixels per tick)
torp1VY db 16 dup (?) ; y velocities (signed, pixels per tick)
torp1T db 16 dup (?) ; time before torpedo self-destruct in clock ticks
torp2X dw 16 dup (?) ; array of torpedos' x locations for player2
torp2Y dw 16 dup (?) ; y locations (pixel)
torp2VX db 16 dup (?) ; x velocities (signed, pixels per tick)
torp2VY db 16 dup (?) ; y velocities (signed, pixels per tick)
torp2T db 16 dup (?) ; time before torpedo self-destruct in clock ticks
db astAngle 8 dup (?) ; array of asteroid angles (2*Pi/256 units)
dw astX 8 dup (?) ; array of asteroid x locations (pixels)
dw astY 8 dup (?) ; array of asteroid y locations (pixels)
db astVX 8 dup (?) ; array of asteroid x velocities (pix per tick)
db astVY 8 dup (?) ; array of asteroid y velocities (pix per tick)
; Game state: 0 = Game in progress, 1 = Esc pressed,
; 2 = Player 1 died, 3 = Player 2 died, 4 = computer(s) dead,
; 5 = Next level, 6 = Game paused
game_state db ?
dw stars 128 dup (?) ; array of 2*64 words holding 64 star positions in [x,y]
; Rotated images - drawn by DrawRotated in GameKeyInt, drawn to buffer in GameTimeInt
db ship1rot 48*48 dup (?) ; rotated ship prebuffer
db ship1rott 48*48 dup (?) ; rotated ship with thrust prebuffer
db ship2rot 48*48 dup (?) ; rotated ship prebuffer
db ship2rott 48*48 dup (?) ; rotated ship with thrust prebuffer
db roid1rot 48*48 dup (?) ; asteroid prebuffer
conf_mode db 18 ; game mode: PvP (0), PvC (1), CvP (2), P1 only (3),
; set bit 3 (add 4) for asteroids on,
; set bit 4 (add 8) for sun on,
; set bit 5 (add 16) for sound on,
; set bit 6 (add 32) for stars on
; array of 5*2 scancodes for each function:
; {p1left, p1right, p1thrust, p1fire, p1hyper; p2left, ...}
; (initialized to some nice, easy default values)
db conf_keymap 10 75, 77, 72, 29, 82, 30, 32, 17, 31, 16
dd oldKeyInt ; original key interrupt before loading
dd oldTimeInt ; original time interrupt before loading
DataSeg ENDS
Procedures
MAIN - EricInitialize - AlexOpenFile - AlexReadImage - AlexCloseFile - AlexMenuScreen - AlexMAIN when done. Will continually update screen from
BufferSeg.
InstallMenu - AndrewUninstallMenu - AndrewInstallGame - AndrewUninstallGame - AndrewMenuTimeInt - AlexGameTimeInt - EricGameKeyInt - AndrewGameInit - EricGameScreen - EricPlaySound - AndrewUpdateSound - AndrewDrawSprite - EricDrawCentered - EricDrawRotated - EricMath formulas
Sine tables were calculated using simple C++ programs: sine.cpp and normsine.cpp. These calculate SineTable[i] = sin(i*2*Pi/256)*128, which results in a signed byte from -128 to 127. The normalized table is similar, but divided by sqrt(2) for optimal scaling. Cosine tables are simply offsets of 64 bytes into their respective sine arrays, which is equivalent to an extra Pi/2.
Image rotation was done by calculating the rotation of the upper-left corner of the source image, in the destination rotated buffer. This meant using the formula:
Rot(<x,y>) = <cos(theta) * x - sin(theta) * y, sin(theta) * x + cos(theta) * y>. DX and DY are calculated as the step size of each successive point in the horizontal direction. To draw a rotated horizontal line of image, the plotting point <x,y> is incremented by <dx, dy> for each step. At the end of each line, the starting point is restored, and <dy, -dx> is added to get the next point in the orthogonal direction, which is equivalent to the beginning of the next line. This allowed an entire 32x32 square to be drawn on a 48x48 buffer, properly rotated and scaled. Additionally, using normalized cos/sin, the image exhibits no "gap" flaws while maintaining the largest image size.
Some extra details of the image rotation scheme involve decentering and recentering the image (as the rotational center of the source is (0,0), and the real center is (32/2, 32/2)). Also, to avoid using floating points, the output of sine/cosine functions return a number in 1/256ths. This is added to word-storage point variables which are also in 1/256ths scale, so that to get the "real" x and y coordinates, one must simply look at only the high word to ignore the sub-pixel details.
External Procedures
There was absolutely no external code used in this project. Every technology
we used was learned from primary sources and implemented entirely independently. We feel that
this gave us a great learning experience, and gave us confidence in our ability to write projects
entirely from scratch.
Play the Game
This project
is not yet complete. It may be finished up during the summer of 2003.
Until then, you can play the original Spacewar here (on a
PDP-11 emulator, no less!) or get more history here.