So what was White Lightning? A short answer would be that it was a version of the Forth programming language ( https://en.wikipedia.org/wiki/Forth_(programming_language) ) packaged for 8-bit micro game development. The packaging took the form of extensions to the core language and the provision of a simple sprite editor. These extensions provided access to routines in the spectrum ROM and - more importantly - to the additional functionality provided by the IDEAL ( Interrupt Driven Extensible Animation Language ) sub-language.
The box contained two tapes, the first tape held the game development environment and the sprite editor.
The second held a demo and some additional arcade sprites. The demo, while not an actual game, is polished and professional looking and hints at the sorts of things - smooth scrolling, animation and sprites - that could be achieved using this tool.
The credit screen at the end of the demo further sells the tool with its emphasis on compactness and quick development.
As well as to the tapes, there were two manuals. These were A6 and printed in an unfriendly black-on-green format.The justification for this was "piracy prevention" since this colour scheme reproduced poorly on the photocopiers of this time. The larger of the two manuals covered white lightning and the sprite editor. The second - strangely called the "Cheat Sheet" - covered more advanced techniques and presented the listing for a complete lunar lander game. The listing, mind you, so if you wanted to see it you would have to type it in by hand and likewise enter the numerical sprite data into the sprite editor. I never did this and imagine few people did.
The sprite editor was a BASIC program that had to be loaded separately to white lightning itself.
Graphics could then be saved from here and loaded into the main program. A "large" work space is used to assemble sprites from 8x8 tiles. Up to 255 sprites can be constructed this way with the largest sprite being 16x16 tiles / 128x128 pixels in size. A few simple compositing ( bit-wise OR, AND, XOR ) operations are supported.
So having loaded the program, loaded some sprites how did you start writing professional games? Well, you first had to learn Forth....
Forth
To be fair to Oasis, there is quite a lot to recommend Forth- and a hit game has indeed been written in it ( Starflight - https://www.filfre.net/2014/10/starflight/ ). Forth's properties make it appear a good candidate for the middle ground between assembly and BASIC: It is compact enough that you fit the development environment, sprites, source code and compiled code into the spectrum's 41k of usable memory. It is also much faster than BASIC - not as fast as machine code, but impressively quick compared to what a person working in BASIC would be used to. So what did code look like? Something like this was probably the first thing I tried after reading the manual:
1 SPN ! 0 COL ! 0 ROW ! PUTBLS
This has the following output when run with the demo sprites loaded. I.e. a green vintage car in the top left hand corner of the screen. Cool!
This code first sets the sprite number ( SPN ) global variable to 1 and draws it a col 0, row 0. If this code looks peculiar - and it certainly did coming from BASIC - that would not be surprising. Forth is an unapologetically stack-centric language with every token manipulating the stack in some way. The code above is short for:
Put 1 on the computation stack
Put the address of "SPN" on the computation stack
Assign the second thing on the stack to the address on the top of the stack
... and so the same for "COL" and "ROW"
Execute the code for the word "PUTBLS" - PUT BLock Screen
The units of Forth are words. Words are gathered in a list called the dictionary. New words can be defined much as you define functions in more modern programming languages. I.e. to define a word that returns the square of a number, you can write:
: SQR DUP * ;
And call it with
10 SQR
which would leave 100 on the stack. DUP here is a word that duplicates the top of the stack, '*' multiplies the top two elements are leaves the multiplicand on the stack. One of the appealing aspects of Forth is the minimalism of the core language - less that a quarter of the words of the core language are written in native code - that is, in terms of the machine instructions of the host machine. The rest are written in Forth itself, so one would only have to implement a relatively small number of words in order to port a version of Forth - or a game written in Forth - from one system to another. Oasis' advertising materials emphasised this attractive feature, since it could allow game developers to easily port code from one micro to another.
Forth is a good bit faster than BASIC. This code fills the screen memory in 3 seconds compared to roughly 30s for the BASIC equivalent. It also illustrates Forth's equivalent of the common "for()" idiom.
: FILL-SCR 16384 6144 + 16384 DO 123 I C! LOOP ;
( the C equivalent would be
16384 is the start of zx spectrum video memory and is 6144
bytes in length
for(char* I = 16384; I < (16384+6144); I++)
{
*I = 123;
}
)
Much of the speed is due to Forth's use of 16-bit integer arithmetic as opposed to BASIC's floating point. But It is still fairly slow compared to even naive assembly. This is because Forth - at least the classic "indirect threaded Forth" of which this is an instance - is not compiled to assembly. In modern terms, you would say its running a Virtual Machine (VM) , but maybe calling this code a VM is a bit of a stretch:
DoColon:
LD HL,(RP) ; get the return address ptr
DEC HL
LD (HL),B ; save the parameter return address
DEC HL
LD (HL),C
LD (RP),HL
INC DE ; advance to the next word of the params
LD C,E
LD B,D
JP Next ; execute it
Next:
LD A,(BC) ; the code field of the next word
INC BC
LD L,A
LD A,(BC)
INC BC
LD H,A
LD E,(HL) ; the actual native code to run
INC HL
LD D,(HL)
EX DE,HL
JP (HL) ; execute the native code
That's the bulk of the Forth "kernel". So what is going on here? Take the word AT. This is used to position the cursor for printing. E.g.
0 8 AT ." Hello World!"
Would print the the string "Hello World!" 8 character cells from the left hand edge of the screen. In memory, the definition of this world is:
AT db 82h ; flag and length (2)
AT_NFA ds "AT" ; Name Field Address ( NFA )
AT_LFA addr EXX ; Link Field Address ( LFA )
AT_CFA addr DoColon ; Code Field Address ( CFA )
AT_PFA addr LIT_CFA ; Parameter Field Address ( PFA )
dw 22
addr EMITC_CFA
addr SWAP_CFA
addr EMITC_CFA
addr EMITC_CFA
addr SEMI_COLON_CFA
Each word has four fields - the Name Field Address which is simply the name of the word, the link field address that points to the preceding word in the dictionary ( in this case the word EXX ) and the code (CFA) and parameter (PFA) field addresses. The CFA points at the code used to implement the word. AT is a colon definition - that is, a word written in Forth using the Colon word. De-compiled it looks like this:
: AT 22 EMITC SWAP EMITC EMITC ;
The DoColon native code above ( pointed to by the word's CFA ) , saves the current parameter pointer onto a return stack ( RP ) and then proceeds to jump indirecly ( JP ) to the code referenced by the CFA fields of each member of the parameter field. Note the indirection here - the CFA fields do not point to the code itself, but to the address of the code in another word. Each of these words will complete by jumping to Next, which advances to the next word of the parameters. The return stack is distinct from the computation stack and used for holding return addresses and - sometimes - for storing local variables. The last CFA in this definition, the CFA corresponding to the semi-colon ";" pops from the return stack. The other words in this definition are EMITC and LIT. EMITC is a low-level word that emits a character - here used to first emit a control code ( 22 ) followed by the cursor position. The control code is pushed onto the stack using the word LIT, whose CFA pushes the 16-bit literal that follows it onto the computation stack.
Writing a compiler to go from the source above to the definition is straight forward - all you conceptually need to do to compile the source for AT us to allocate some memory for the name and various fields, initialize those fields ( i.e. LFA to point to the last word defined, the CFA to DoColon ) and append the CFAs of following words. You find the CFAs for the words in the source by searching the dictionary of all words that have been defined by following the LFA from the most recently defined word. There are no string comparisons at runtime.
One can see that this is not going to be all that fast. Something as simple as
: INC 1 SWAP +! ;
- which implements a C-style ++ increment operator - would end up first executing DoColon to set up a return address, then calling Next to add a literal 1 to the stack, then Next again to call SWAP, which swaps the top two items on the stack in order to apply +! ( pronounced "plus-store") - which expects the top of the stack to be an address, This updates the contents of the memory address and finally returns to the caller.
To the assembly programmer, this seems like a fairly perverse way of implementing a series of instructions equivalent to
INC (HL)
which is the z80 opcode to increment the value referenced by the 16-bit pointer in the HL register pair.
The Development Environment
From a cold start, the memory map looks something like this. Forth occupies the 16k of memory below BASIC and system variables. Only about half of this is the core language - the rest is dedicated to the custom words that make up the IDEAL vocabulary. This leaves the dictionary pointer DP ( the address where new words will be added ) pointing at 0x9e5e. This leaves 7K for new words. Forth stores its system state ( the text input buffer ( TIB ), various system variables and the return stack ) above 0xbb00. 0xbb00 itself is also the initial address of the computation stack that grows down memory. Bad things happen if the dictionary pointer and the computation stack collide.
0xc000 and above are available for source code. This is managed somewhat wastefully as a ram disk. Forth is designed around a disk environment, so it is expected that source will be loaded into memory before being edited. Some memory is set aside for BASIC - about 1k - as Forth does not support any commands for loading and saving from tape so one is expected to do this from BASIC.
The text input buffer sits between the computation stack at 0xbb00 and the return stack at 0xbba0. The reason for this is probably to provide some degree of protection from computation stack underflows and return stack overflows, as overwriting / reading from the TIB is likely to be relatively benign. There is not any practical way of protecting from return stack underflows.
A 512 byte buffer at the end of the Forth environment is set aside for caching the source "from disk". When a 512 "Screen" ( SCR ) of source is edited, it is copied from "disk" into this buffer. These 512 byte screens are arranged into 8 rows of 32 characters. As this example from the cheat sheet illustrates, clean formatting and lots of white space is not really encouraged - nor practical given the amount of memory available.
Source code must also compete for memory with sprites. These grow down from the top of memory, eating into the space available for source. The manual points out that you don't need all the source to be resident at once. In principle, so long as you keep one or two "screens" free, and your words don't run outside these screens, you can incrementally load ( from tape ) and compile your program in an incremental fashion.
The limit of 7K for game code might seem very restrictive, but bear in mind that you would probably need to compile over 12K of source to hit this limit. This is based on an estimate that 512 bytes of source is likely to generate 300 bytes of code. Oasis probably thought that this was unlikely to be a problem. As a rule of thumb, an early single screen 80s arcade game ( Invaders, Frogger, Pac-Man, Asteroids etc. ) can be implemented in about 4k of assembly code. A more expansive action-adventure such as Ultimate's Sabre-Wulf has about 10k of code, with the remainder being graphics and map data. With that in mind, 7K of Forth is not completely unreasonable.
In total, then, one has about 23K of memory to play with, out of the 48k Spectrum's fairly modest 41K of non-video memory. We are paying quite a high memory price for using White Lightning.
So what is White Lightning bringing to the table? One attractive feature is the environment - for all its limits and primitiveness, it does let us compile, edit and test code without leaving White Lightning. Here one can test the words that make up a program in isolation and check that the values they are returning are correct, coupled with some degree of confidence that a silly error will not cause the program to crash. It's not Unity nor Unreal, but this is as close as you are going to get on a simple microcomputer in 1984.
The other attractive feature - and a major selling point - is the graphics oriented IDEAL sub language, which I will discuss next ...
コメント