Episode 6: Saucers!
It's the 20th — and thus our personal halftime (since we started Oct, 10). To celebrate the event, let's have some saucers!
Some work has been done since our last episode. Actually, by now the code is now growing to fair complexity and we've to constrain ourselves to just illustrating the outlines and approches we chose. Let's start with …
The Rocket Ship, Again
As we're planning to finally put some saucers on the screen, we notice that we ended up scaling our rocket sprite to a grid based on one-and-a-half locations of the PDP-1's Type 30 scope as the unit. Since the signal to the scope is a digital one, our nifty fixed point coordinates will be truncated to integers, resulting in possible distortions of the outline with regard to the fractual part of the coordinates. While this may be OK for the rocket ship and may even mimic the skewy outlines of the original, this won't work well for the saucers and the rather steady appearance of their outlines.
So back to the drawing table and readjust the outline, once more, to match the native resolution of the Type 30 CRT Display. — Maybe we're just finding out why Spacewar! featured configurable outlines early on in the process by the means of experimental software archeology.
And this is, what our final outline looks like:
To Parallax Or Not To Parallax
When we were setting up our starfield routine, we were also thinking of some kind of parallax effect relative to the movements of the player's ship. This may actually be the very time to address this. The implementation is easy: Just update the base position of the background (as in "
bgh" and "
bgv") by a fraction of the rocket ship's delta x and delta y, but in opposite direction. We choose to do this in the background routine, to be exact, in the skip-frames when we're not displaying anything, thus sharing runtime fairly between tasks.
But the result isn't that pleasing, to say the least: Thanks to the afterglow of the Type 30 Display the screen appears like hatched by the strokes drawn by the moving stars. Even some inclinations to dizziness may be involved. So we decide to scale the parallax down somewhat and leave an option for a bit more prominent effect for those who want to try. Still, I'm actually more pleased with the steady display. So, add another option to switch off the effect entirely. In case parallax is off, we'll add a bit of nearly indiscernible movement to the left, just to prevent burn-in.
We control our options by the sense switches on the control panel of the PDP-1 and read the respective state by the istruction "
szf n0" (skip on zero sense switch n, n = 1 .. 6). In our emulation, we add two checkboxes for this at the top right of the virtual scope. And this is how the background behavior is controlled:
- Sense switch 1: Parallax effect on/off (on for parallax, default off).
- Sense switch 2: On for stronger parallax effect.
Having dealt with our Hamletesque ponderings that way, let's leave this Denmark of celestial aesthetics behind and head for outer space, where there are …
Saucers, Flying Saucers!
Let's have, finally, some saucers. Because, strictly speaking, this is why we visted the code at all.
Of course we need a plan for this, and we begin with a sketch of the outline transposed to Type 30 coordinates space:
On most machines, there are peculiar half-sized pixels to be seen at the very left of a moving saucer. Presumably, these are artifacts produced by aging components (capacitors?) of analog timing circuits. Most machines exhibit them, some do not, and there's footage of one-and-the-same machine showing these half-pixels in some stages of a restoration process, while not in others. Also, the FPGA implementation doesn't show them, indicating that they are not to be supposed to be there according to the digital layout. Thus, we're not going to implement this effect.
(The basic idea about these half-pixels is that they are produced by timing issues that occur only when the circuits for "rotating" the center part of the saucer are active, causing the gates controlling the pixels that make the outer "rim" of the saucer to be opened early, and that they are not intended to be there. Had these been meant as some kind of thrust animation, we might expect them to show up on the other side, as well.)
Still, there are other thrills to the saucer display, namely an animated, dashed center line:
The animation consists (in our case) of 6 steps, moves both to the left and right, involves some clipping, and is of either of two speeds. Apparently, direction and speed of the animation is chosen at random for a certain leg, from time to time it stops completely for an idle saucer, and, if a saucers starts to speed in any direction, the animation is — so it seems to me — never changed at all. (Thus, an idle saucer first starts to animate before it zooms off in any direction, a distant pre-echo of the eyes of the "ghosts" in Pac-Man.)
Putting the Saucers on the Screen
The basic outline of a saucer is as simple as 3 dots mirrored allong two axis, drawn in any of the 4 quadrants around its center. Then, there are two dots to the left and right to mark the sides of the saucer, and finally, there's the animated center.
We decide to handle the mirroring by flags to control the sign of the center-offsets for each of the quadrants. But, how are we to decide, which flags are to be set on each of the four passes in total? Also, there are lots of clippings and offsets involved with the center animation, how may we draw these?
Assembler Trick of the Day: Dispatch Tables
A dispatch table is the machine code equivalent to a switch statement. It's as simple as fixing up a jump vector pointing to any of the locations of the dispatch table and executing the jump. The dispatch table either consists of a list of yet another jumps, or evenly spaced blocks of instrcutions. We may chose to exit at the end of a given block by a final jump, or we may chose to fall through to the next one.
1) Basic Dispatch lac \dpc / load a value, here \dpc = (0, 1, 2) add (dpv 1 / add address of label "dpv" + 1 dap dpv / fix up jump target at "dpv" dpv, jmp . jmp dp0 / jump to code for 0 jmp dp1 / jump to code for 1 jmp dp2 / jump to code for 2 2) Dispatching for Offset-Based Execution Here we (partly) execute an unrolled loop, starting at an offset in "
\dpc": lac \dpc / load an offset (again of 0, 1, 2) sal 2s / multiply by 4, offset by 4 instr. add (dpv 1 / add address of label + 1 dap dpv / fix up jump target dpv, jmp . / go there idx \tbp / case 0, some code, 4 instructions long lac i \tbp / (copy some using pointers) dac i \dxp idx \dxp idx \tbp / case 1, next iteration, offset 4 ... ... ... idx \tbp / case 2, next iteration, offset 8 ... ... ...
We make havy use of this trick in order to implement the saucer graphics. For the center animation, we use a state counter for offsets and clippings. With the saucers eventually hovering on our virtual screen, it may be nice to have them move around, just a little bit. There are just 8 directions and idle (i.e., no movement) for the saucers, and we decide to put the values for delta x and delta y into a lookup table, for each of the 8 basic directions respectively. Thus we may load them by a random offset that is conveniently just fitting into a power of 2. For idle state (no movement), we decide to use the sign-bit of the random number used to generate the next direction. (Thus, the saucers are more likely to stop than to go in any other of the 8 directions.)
Since the two saucers are always moving in parallel at a constant vertical offset, we just get along with a single set of variables to control them. Essentially, there's a single saucer displayed twice. — Much too easy! — As a compensation, choosing movement directions is a bit tricky again, especially for the zero states (idle saucers). There are actually 4 possible states involving up to 3 steps, providing us ample opportunity to exercise our nifty dispatch trick. Timing and animation are controlled by yet another frame counter.
(In reallity, it took a fair time to figure this out. As for the pupose of this blog, we'll keep it that simple. Moreover, while inspecting footage of Comuter Space over and over, I started to hate those nasty people who are always shooting at the saucers when the animation starts to reaveal a crucial step. As if this would be the purpose of the game!)
Miraculously, all this still works out without any debugging facilities. — For sure, this is a world full of wonders! :-)
And in the end, this is what we get (TIWWG):
▶ Experience the code live: www.masswerk.at/icss/.
And here's our growing code, so far (we're currently using about 0.9 K of memory,
01605 words including variables):
ironic computer space 0.07 nl 2016-10-20 mul=mus div=dis ioh=iot i 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, 2400 / rocket angular acceleration rvl, 7, sar 7s / scaling rocket velocity ras, 10, law i 4 / rocket acceleration sensitivity (skip) rad, 11, law i 3 / rocket acceleration damping (averaging) ran, 12, 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 lac \frc / check frame conter and (1 sza / skip every second frame jmp bgi / jump to star display szs i 10 / sense switch 1 for parallax effect jmp bgd lac \rdx szs i 20 / sense switch 2 for stronger effect jmp . 3 sar 1s add \rdx sar 3s cma add bgh dac bgh lac \rdy szs i 20 jmp . 3 sar 1s add \rdy sar 3s cma add bgv dac bgv jmp bgx / return bgd, lac \frc / advance x offset slowly and (37 sza jmp bgx law i 400 add bgh dac bgh jmp bgx / return bgi, 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 bgx, jmp . / return 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, lac (240130 dac \rth / rotational angle lac (-200000 dac \rpx / pos x cma dac \rpy / pos y law 600 dac \rdx law i 1200 dac \rdy dzm \rac / acceleration skip counter /saucer reset au, lac (200000 dac \upy / pos x lac (140000 dac \upx / pos y dzm \udc / animation skip counter law 1 dac \udd / animation direction dzm \umo / direction code law 1400 dac \udy / delta y dzm \udx / delta x law i 100 dac \ufc / duration of movement law 1 dac \uft / speed of center animation (1,3) /main loop fr0, load \ict, -4500 / initial instruction budget (delay) idx \frc / increment frame counter jsp bg / display background jsp rkt / rocket routine jsp ufo count \ict, . / use up rest of time of main loop jmp fr0 / next frame /saucers ufo, dap ufx clf 6 lac \upx /update position add \udx dac \upx dac \px lac \upy add \udy dac \upy dac \py jsp sd /display first saucer lac \py sub (400000 /half-screen vertical offset dac \py jsp sd /display second saucer isp \ufc /increment leg counter jmp uf1 /continue lac \umo /new direction spa jmp uz0 /we're in a 3-steps stop (\umo: -3..-1) random sma jmp uf2 /set up new leg dzm \udx /stop dzm \udy lio ran /what kind of stop will it be? ril 3s spi jmp . 4 /three-steps stop stf 6 /single-step stop, keep center animation lac \umo jmp uf2 /set up leg and continue law i 3 /first period of three-steps stop dac \umo /reuse \umo as step counter (negative) law 3 dac \ufc /set animation to slow (3) jsp utd /get duration sar 1s dac \ufc jmp ufi /continue uz0, cma /3-steps stop, dispatch on -\umo (-3..-1) add (. 3 dap . 2 idx \umo jmp . jmp uz1 jmp uz2 uz3, dzm \udd /3 - stop animation jmp . 2 uz2, jsp ucd /2 - still stopped, new animation direction jsp utd sar 1s dac \ufc jmp ufi uz1, stf 6 /1 - start over, but keep animation random jmp . 2 uf2, clf 6 /set up for a new leg (flag 6: keep anim. dir.) and (7 dac \umo /new motion code (0..7) sal 1s /read corresponding dx/dy form table umt add (umt / setup reference location for dy in \t1 dac \t1 lac i \t1 /load dy indirectly from addr. in \t1 dac \udy idx \t1 /increment addr. in \t1 lac i \t1 /load dx indirectly from addr. in \t1 dac \udx szf 6 i /skip next on flag 6 jsp ucd /set new direction for animation jsp utd /get delay dac \ufc uf1, lac \frc /increment center animation and \uft sza jmp ufi lac \udc /udc = 0..5 sub \udd dac \udc sma jmp . 4 law 5 dac \udc jmp ufi sad (6 dzm \udc ufi, lac \ict /update instruction count add (1000 dac \ict ufx, jmp . ucd, dap ucx /subroutine to set center animation lio ran ril 1s law 1 spi cma dac \udd lac ran /animation speed, favor faster rar 9s and (3 sza jmp . 3 law 1 jmp . 2 law 3 dac \uft ucx, jmp . utd, dap utx /subroutine to get random delay (leg length) random sar 9s sar 6s sub (200 utx, jmp . /saucer movement table (dy, dx) umt, 1400 0 -1400 0 0 -1400 0 1400 1000 1000 -1000 -1000 -1000 1000 1000 -1000 /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 \frc /load frame counter isp \rac /sensitivity, frame skip jmp rp1 xct ras /reset counter dac \rac lac \sn /dx cma xct rvl /apply scaling for acceleration swap /swap result into io xct rad /damping, get loop count dac \rdc rx0, swap /get intermediate value from io add \rdx /average with old dx sar 1s swap /swap into io isp \rdc /increment loop jmp rx0 dio \rdx /store updated dx lac \cs /same for dy xct rvl swap xct rad dac \rdc ry0, swap add \rdy sar 1s swap isp \rdc jmp ry0 dio \rdy jmp rp1 rp0, dzm \rac rp1, scale \sn, 4s, \sn1 /get position of rocket tip sar 2s /offset x = (sin >> 4) - (sin >> 8) cma add \sn1 dac \sn1 scale \cs, 4s, \cn1 /offset y = (cos >> 4) - (cos >> 8) sar 2s 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, 6s, \sn8 /scaled sine, 8 steps sar 1s dac \sn4 /4 steps sar 1s dac \sn2 /2 steps sar 1s dac \sn1 /1 step dac \sm1 add \sn2 dac \sn3 /3 steps dac \sm3 add \sn2 dac \sn5 /5 steps add \sn1 dac \sn6 /6 steps dac \sm6 scale \cs, 6s, \cn8 /scaled cosine, 8 steps sar 1s dac \cn4 /4 steps sar 1s dac \cn2 /2 steps sar 1s dac \cn1 /1 step dac \cm1 add \cn2 dac \cn3 /3 steps dac \cm3 add \cn2 dac \cn5 /5 steps add \cn1 dac \cn6 /6 steps dac \cm6 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 +4, x +6 sub \cn4 add \sm6 swap add \sn4 add \cm6 disp swap /y +5, x +3 sub \cn5 add \sm3 swap add \sn5 add \cm3 disp swap /y +6, x +1 sub \cn6 add \sm1 swap add \sn6 add \cm1 disp swap /y +6, x -1 sub \cn6 sub \sm1 swap add \sn6 sub \cm1 disp swap /y +8, x +6 sub \cn8 add \sm6 swap add \sn8 add \cm6 disp swap /y 3, x -10 sub \cn3 sub \sm3 sub \sm6 sub \sm1 swap add \sn3 sub \cm3 sub \cm6 sub \cm1 disp swap /y +7, x +6 sub \cn8 add \cn1 add \sm6 swap add \sn8 sub \sn1 add \cm6 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 \cm3 cma dac \cm3 lac \sm3 cma dac \sm3 lac \cm6 cma dac \cm6 lac \sm6 cma dac \sm6 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 \sm6 /x -11 sub \sm6 add \sm1 swap sub \cm6 sub \cm6 add \cm1 dac \px /store position dio \py lac \frc /load frame counter 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+5 sub \cn5 swap lac \px add \sn5 disp jmp rox /jump to return /saucer display define sdisp B /display a dot (opt. brightness parameter) dpy -4000 B / request completion pulse term sd, dap sdx clf 5 clf 6 law i 4 dac \sdc sdl, law 5000 /y +/- 10, x +/- 4 szf 6 cma add \py swap law 2000 szf 5 cma add \px disp law 3400 /y +/- 7, x +/- 7 szf 6 cma add \py swap law 3400 szf 5 cma add \px disp law 3400 /y +/- 7, x x+/- 15 szf 6 cma add \py swap law 7400 szf 5 cma add \px disp lac \sdc /dispatch on \sdc (-4..-1) for passes (set flags) cma add (sdd dap sdd idx \sdc /increment counter and jump sdd, jmp . jmp sd1 jmp sd2 jmp sd3 stf 6 /2nd pass jmp sdl sd3, stf 5 /3rd pass jmp sdl sd2, clf 6 /4th pass jmp sdl sd1, lio \py /done, display outer dots lac (12400 /y 0, x + 21 (right side) add \px sdisp 100 lac (-12400 /y 0, x - 21 (left side) add \px ioh sdisp 100 add (1000 swap lac \udc /draw first group of dots at the left sal 1s / dispatch on \udc x 3 (udc = 0..5) for clipping add \udc add (sd4 1 dap \sd4 swap lio \py sd4, jmp . add (1000 /0 nop nop add (1000 /1 ioh sdisp add (1000 /2 ioh sdisp add (1000 /3 ioh sdisp add (1000 /4 ioh sdisp add (3000 /5, display 4 dots ioh sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (3000 /display 4 dots sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (3000 /draw group of remaining dots at the right swap lac \udc /dispatch on \udc x 3 (udc = 0..5) for clipping add (sd5 1 dap sd5 swap lio \py sd5, jmp . jmp sd0 jmp sd0 jmp sd9 jmp sd8 jmp sd7 jmp sd6 sd6, ioh /4 dots sdisp add (1000 sd7, ioh /3 dots sdisp add (1000 sd8, ioh /2 dots sdisp add (1000 sd9, ioh /last dot sdisp sd0, ioh /fetch and clear last completion pulse sdx, jmp . /return constants variables start 4
That's all for this post.
— stay tuned! —
▶ Next: Episode 7: Skeet in Space!
◀ Previous: Episode 5: Yet Another Progress Update
▲ Back to the index.
2016-10-20/21, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2016/10. —