Episode 4: “A Rocket Ship That You Control”
In the last episode we put a rocketship on the screen, now it's time to add controls to its. Because, of course, this is one of the most compelling promises made by Computer Space. Also, some refinements…
Computer Space attraction panel.
(Image: Computer Space Simulator by Mike "Moose" O'Malley.)
For a beginning, we tweak the outline of the rocket ship, we already have, just a little bit. Essentially, we move up the last three rows of dots up by a grid unit to make it a bit bulkier. Also, we adjust the scaling (sadly, it's not just a fraction of a power of 2 of the unit vector and we've to add some components). Same applies for the offset of the tip, providing the perceptual pivoting point of our ship. Finally, we increase the brightness level of the rocket display, mostly for a larger dot size.
With this in place, we turn to the graphics of the exhaust flames of our litlle spaceship.
Holy Smoke
The thrust animation in Computer Space consists of just two pixels drawn alternately at the stern of the ship. Should be easy to implement, shouldn't it? — Not so, as may be guessed by this introduction: At a closer inspection of footage of the game, we're thrown back into the Heisenberg uncertainty of Computer Space graphics, once again, as shapes vary with rotation. (We may call this the Bushnell-Dabney Uncertainty of electronically generated visuals.)
Computer Space: Visual representation of exhaust flames at two rotational angles.
(Colorization of exhausts for emphasis; N.L.)
As may be observed, the exhausts are much more accentuated at horizontal or diagonal rotational angles than in a close to vertical position of the ship, where they appear somewhat compressed and also closer to the vertical center of the rocket.
We settle for a compromise — and, of course, it's more on the prominent side, for effects. By this we arrive at the final form of our spaceship:
Ironic Computer Space Simulator: Final outline of the player's rocket ship.
Trusting Thrust
With the thrusting animation in place, we're ready to apply any player contolled dynamics to our little ship. As Computer Space has it's spaceship at a constant velocity, this is more or less about updating the values of delta x and delta y to a scaled amount of the unit vector (sine and cosine of the rotational angle) each time the player engages the thrust button. More or less — as there is more to this: Computer Space not only features a computer-like electronic device, but also a viscous space, where any newly applied dynamics just gradually come into effect.
We're facing two problems here: Constraining the velocity to a certain amount and implementing an accumulating acceleration. And, most important, we would like to avoid complex calculations, like square roots (as would be required for scaling a vector), for runtime's sake.
In order to do so, we come up with the following hack [ not by K.T. ;-) ]:
procedures: 1) scale the unit vector (sin for dx, cos for dy) to max velocity, store it. 2) scale this again for acceleration to be applied. 3) add old delta. 4) restrain the resulting components (for dx, dy) to the range of max velocity vector. (use the same method we've already used for the rotational angle.)
By this, we've arrived at a new value for dx (or dy, respectively) that we can trust to not to exceed the maximum velocity. And this by a few shifts and additions/subtractions only! What's still missing, is a proper viscosity:
(cont.) 5) average with old delta (dx, dy) for viscosity. 6) store results (to be added as constant delta to position later).
With this in place, we are ready to actually burn some fuel and maneuver the ship accross the screen. Thanks to the method descibed above we arrive at a quite pleasing experience that isn't far from the original behavior.
As a final improvement, we put the scalings for max velocity and for the rocket's acceleration in a parameters table on top of our code for the ease of any adjustments, following Steve Russell's best practice (who did similar in Spacewar! 3.1).
▶ Experience the code live: www.masswerk.at/icss/.
Thrusting rocket in ICSS v.0.04 (section of screenshot).
(www.masswerk.at/icss/)
The Dry Stuff — Implementation Details
Since the code has by now become a bit more complex, we wont go into verbose, descriptive details anymore, but rather refer any interested audiences to the comments in the code (see below).
However, it may be appropriate to introduce some more PDP-1 instructions for the benefit of the curious reader:
We're using a program flag (flag 5) to store, whether the ship is thrusting or not. (There are 6 program flags, 1–6, implemented in hardware. Flag 4 is also used for the console typewriter and we will avoid this one.) Instructions for setting and clearing flags are part of the operate group of instructions:
stf n ... set flag n (1...6) clf n ... clear flag n (1...6)
The operate group includes instruction to change the internal state of the CPU and it's registers, like:
cla ...... clear AC cma ...... complement AC cli ...... clear IO lat ...... load AC from testword switches hlt ...... halt nop ...... no operation opr ...... basic instruction code of operate group (syn. to nop)
Instructions of a group may be combined by microprogramming and will be executed in a single cycle. For this, we simply add the instruction codes (and substract the vale of the group, opr, since only the 12 lower bits are used for microprogramming. The micro-sequence in which the instructions occur are hardwired into the CPU. Some of these microprogrammed instructions also enjoy a well known mnemonic:
clc = cma+cla-opr / clear AC and complement it (777777) cla stf 5 .... clear AC and set flag 5 at once
Another group of microprogrammable instructions is the skip group, used to branch on conditions:
sma ...... skip on minus AC (sign-bit set)
spa ...... skip on plus AC
sza ...... skip on zero AC
spi ...... skip on plus IO
szf n .... skip on flag n zero
szs n0 .... skip sense switch n zero (n: 1...6)
e.g., "zss 10" to skip on sense switch 1 zero
Setting the i-bit inverts the condition:
sza i .... skip on AC NOT zero szf i n ... skip on flag n set (not zero)
Again, skip conditions may be combined by microprogramming to form a union. Here, instruction "szf" represents the basic instruction code of the group:
szm = sza sma-szf / skip on zero or minus AC
Having thus covered pretty much of the basic PDP-1 instructions, there are, we'll close our little PDP-1 101 for today.
And here's the code, so far:
ironic computer space 0.04 nl 2016-10-17
mul=mus
div=dis
define initialize A,B
law B
dap A
term
define index A,B,C
idx A
sas B
jmp C
term
define swap
rcl 9s
rcl 9s
term
define load A,B
lio (B
dio A
term
define setup A,B
law i B
dac A
term
define count A,B
isp A
jmp B
term
/macros specific to the program
define scale A,B,C
lac A
sar B
dac C
term
define random
lac ran
rar 1s
xor (355671
add (355671
dac ran
term
/ Computer Space
/ Original arcade game by Nolan Bushnell and Ted Dabney,
/ Syzygy Engineering / Nutting Associates, 1971.
/"A simulated space battle that pits
/ computer-guided saucers against
/ a rocket ship that you control."
3/ jmp sbf / ignore seq. break
jmp a0 / start addr, use control boxes
jmp a1 / alt. start addr, use testword controls
/game parameters
raa, 6, 2700 / rocket angular acceleration
rvm, 7, sar 1s / scaling rocket max velocity
rva, 10, sar 4s / scaling rocket acceleration
ran, 11, 0 / random number
/routine to flush sequence breaks, if they occur.
sbf, tyi
lio 2
lac 0
lsm
jmp i 1
/sine-cosine subroutine - Adams associates
/calling sequence= number in AC, jda sin or jdacos.
/argument is between +/- +2 pi, with binary point to right of bit 3.
/answer has binary point to right of bit 0. Time = 2.35-? ms.
/changed for auto-multiply , ddp 1/19/63
cos, 0
dap csx
lac (62210
add cos
dac sin
jmp .+4
sin, 0
dap csx
lac sin
spa
si1, add (311040
sub (62210
sma
jmp si2
add (62210
si3, ral 2s
mul (242763
dac sin
mul sin
dac cos
mul (756103
add (121312
mul cos
add (532511
mul cos
add (144417
mul sin
scl 3s
dac cos
xor sin
sma
jmp csx-1
lac (377777
lio sin
spi
cma
jmp csx
lac cos
csx, jmp .
si2, cma
add (62210
sma
jmp si3
add (62210
spa
jmp .+3
sub (62210
jmp si3
sub (62210
jmp si1
/subroutines for background display
nos=77 /number of stars
/table of stars coors (nos words, 9 bits x and 9 bits y)
bst, . nos/
/setup (nos random coors starting at bst)
bsi, dap bsx / deposit return address
init bsc, bst / deposit first addr of bst in bsc
bsl, random / get a new random number
bsc, dac . / store it in current loc of st
index bsc, (dac bst+nos, bsl / increment bsl, repeat at bgl for nos times
bsx, jmp . / return
/display background stars (displays every 2nd frame only)
bg, dap bgx / deposit return address
isp bgc / increment bgc, skip on positive
bgx, jmp . / return
law i 2 / load -2 into ac
dac bgc / deposit in bgc to reset frame counter
init bgl, bst / init bgl to first addr of stars table
bgl, lac . / get coors of next star (x/y)
cli / clear io
scr 9s / shift low 9 bits in high 9 bits of io (x)
sal 9s / move remaining 9 bits in ac in high part (y)
add bgv / add vertical offset
swap / swap contents of ac and io
add bgh / add horizontal offset
dpy-i / display a dot at coors in ac (x) and io (y)
index bgl, (lac bst+nos, bgl / repeat the loop at bgl nos times
jmp bgx / return
bgc, 0
bgh, 0
bgv, 0
/here from start
a0, law rcb /configure to read control boxes (sa 4)
dap rcw
jmp a2
a1, law rtw /configure to read testword (sa 5)
dap rcw
/start a new run
a2, jsp bsi / initial setup of bg-stars
dzm bgh / reset offsets of stars to zero
dzm bgv
/rocket reset
ar, dzm \rth / rotational angle
lac (-70000
dac \rpx / pos x
cla
dac \rpy / pos y
dzm \rdx
dzm \rdy
/main loop
fr0, load \ict, -4500 / initial instruction budget (delay)
jsp bg / display background
jsp rkt / rocket routine
count \ict, . / use up rest of time of main loop
jmp fr0 / next frame
/player rocket routine
rkt, dap rkx
rcw, jsp . /read control word (ccw, cw, trust, fire)
cla clf 5 -opr
dio \cw /merge (or) spacewar player inputs
rcr 4s
ior \cw
dac \cw
lio \cw /parse and process rotation
lac \rth /load angle
spi /sign cleared in io?
add raa /no, add angular acceleration
ril 1s /next bit
spi
sub raa
sma /normalize 0 >= angle >= 2Pi (311040)
sub (311040
spa
add (311040
dac \rth /update angle
ril 1s /parse thrust input
spi
stf 5 /set flag 5 for thrust
jda sin /get sin, store it in \sn
dac \sn
lac \rth
jda cos /get cos, store it in \cs
dac \cs
szf i 5 /flag 5 set? update dx / dy
jmp rp0
lac \sn /dx
cma
sar 7s
xct rvm /apply scaling for max velocity
dac \t1
xct rva /apply scaling for acceleration
add \rdx /add old dx
spa /is it positive?
jmp . 6 /no, skip next 5 instr.
sub \t1 /constrain positive vel.
sma
cla
add \t1
jmp . 5
sub \t1 /constrain negative vel.
spa
cla
add \t1
add \rdx /average with old dx for viscosity
sar 1s
dac \rdx /store updated dx
lac \cs /same for dy
sar 7s
xct rvm
dac \t1
xct rva
add \rdy
spa
jmp . 6
sub \t1
sma
cla
add \t1
jmp . 5
sub \t1
spa
cla
add \t1
add \rdy
sar 1s
add \rdy
rp0, scale \sn, 4s, \sn1 /get position of rocket tip
sar 4s /offset x = (sin >> 4) - (sin >> 8)
cma
add \sn1
dac \sn1
scale \cs, 4s, \cn1 /offset y = (cos >> 4) - (cos >> 8)
sar 4s
cma
add \cn1
dac \cn1
lac \rpx /update pos x (add dx)
add \rdx
dac \rpx
sub \sn1 /subtract offset for tip, store it in \px
dac \px
lac \rpy /same for y
add \rdy
dac \rpy
add \cn1
dac \py
scale \sn, 7s, \sn4 /scaled sine 4 steps
sar 1s
add \sn4
dac \sn4 /sn4 = (sin >> 7) + (sin >> 8)
dac \sm4 /sm4 for lateral offsets (will be complemented)
sar 1s
dac \sn2 /scaled sine 2 steps
dac \sm2
sar 1s /scaled sine single step
dac \sn1
dac \sm1
scale \cs, 7s, \cn4 /same for cosine
sar 1s
add \cn4
dac \cn4
dac \cm4
sar 1s
dac \cn2
dac \cm2
sar 1s
dac \cn1
dac \cm1
jsp rod /display it
lac \ict /update instruction count
add (1000
dac \ict
rkx, jmp .
/control word get routines
/read control boxes
rcb, dap rcx
cli
iot 11
rcx, jmp .
/read testword
rtw, dap rtx
lat
swap
rtx, jmp .
/rocket display
/ step rearwards - x add \sn1, y sub \cn1
/ step outwards - x add \cm1, y add \sm1
/ step inwards - x sub \cm1, y sub \sm1
define disp
dpy-i 100 /display a dot at brightness level +1
term
rod, dap rox
stf 6 /set flag 6
lac \px
lio \py
disp
rop, swap /y +3, x +3
sub \cn2
sub \cn1
add \sm2
add \sm1
swap
add \sn2
add \sn1
add \cm2
add \cm1
disp
swap /y +4, x +2
sub \cn4
add \sm2
swap
add \sn4
add \cm2
disp
swap /y +4, x +1
sub \cn4
add \sm1
swap
add \sn4
add \cm1
disp
swap /y +4, x -1
sub \cn4
sub \sm1
swap
add \sn4
sub \cm1
disp
swap /y +5, x +4
sub \cn4
sub \cn1
add \sm4
swap
add \sn4
add \sn1
add \cm4
disp
swap /y +1, x -6
sub \cn1
sub \sm4
sub \sm2
swap
add \sn1
sub \cm4
sub \cm2
disp
swap /y +4, x +3
sub \cn4
add \sm2
add \sm1
swap
add \sn4
add \cm2
add \cm1
disp
szf i 6 /flag 6 set? (skip on not zero)
jmp rot
clf 6 /clear flag 6
lac \cm1 /invert unit vector components
cma
dac \cm1
lac \sm1
cma
dac \sm1
lac \cm2
cma
dac \cm2
lac \sm2
cma
dac \sm2
lac \cm4
cma
dac \cm4
lac \sm4
cma
dac \sm4
lac \px /load first pos
lio \py
jmp rop /second pass for other side
rot, szf i 5 /no, flag 5 set?
rox, jmp . /no, return
swap /draw exhaust flame
sub \sm4 /x -6
sub \sm2
swap
sub \cm4
sub \cm2
dac \px /store position
dio \py
idx \rtc /increment a counter and check second least bit
and (2
sza /is it zero? (state switch)
jmp ro2
ro1, lac \py /state 1, display at y-1
add \cn1
swap
lac \px
sub \sn1
disp
jmp rox
ro2, lac \py /state 2, display at y+4
sub \cn4
swap
lac \px
add \sn4
disp
jmp rox /jump to return
constants
variables
start 4
That's all for this post.
▶ Next: Episode 5: Yet Another Progress Update
◀ Previous: Episode 3: Rocketry — Or, Fly Me to the Moon Stars
▲ Back to the index.
2016-10-17, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2016/10. —