Iatheory Scrapbook
Iatheory Scrapbook
2 A Little Archaeology 15
3.0.1 Important Concept Number One: High Bytes and Low Bytes . . . 40
4.1 Sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4
CONTENTS
4.3.1 A Complication . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
5
CONTENTS
7.11 Bytes 30-31: Data to Load After Colliding with the Player . . . . . . . . . 159
6
CONTENTS
7
CONTENTS
I Bumph 406
Index 411
8
About This Book
This book describes the inner workings of a relatively obscure video game created by
an eccentric Englishman by the name of Jeff Minter in 1986 for the Commodore 64.
If you are curious about old computers such as the Commodore 64 or the detailed me-
chanics of making a glori\=ed digital breadboard produce something on a screen that
flashes, bleeps, and fascinates then this book is hopefully for you. Iridis Alpha was
not an enormous success on its release but it is widely regarded as one of the great
achievements on the Commodore 64 hardware. It embodies an 8-bit arcade aesthetic
that was unique to its time, it was slightly mad in its concept and execution, and its
emphasis on speed and unforgiving gameplay influenced generations of game devel-
opers in the years that followed.
Since the source code of Iridis Alpha is no longer available, I have had to unpack and
reinterpret it from the game binary Jeff Minter released into the public domain in 2019.
I describe this reverse-engineering process in the opening chapters because I think
it is an interesting exercise in and of it self to go from a binary blob to a set of fully
commented source code that provides insight to the inner workings of the game. A
Little Archaeology describes how we extract the game binary from the cassette tape
it was originally distributed. Some Disassembly Required shows you how to go from a
very long list of bytes to a full source code listing. Hopefully you are here because you
enjoy this kind of gory detail too.
If you are just interested in learning about the mechanics of the game itself you can
flick straight to The First 16 Milliseconds, which begins by covering the loading the title
screen and all that goes on in there. Making Planets for Nigel, Blasting Fast and Slow,
and Enemies and Their Discontents cover the main gameplay and dive into how the
game levels are created and how the speed of the game is achieved. These chapters go
deep into the game's reassembled code and look closely at how Jeff Minter designed
its core engine.
I dedicate a full chapter A Hundred Thousand Billion Theme Tunes to the ingenious
9
CONTENTS
procedural programming behind Iridis Alpha's theme tunes and trace its inspiration
back to an article 'Musical Fractals' in a 1986 issue of Byte magazine. Another 164
Tunes looks at the early expermentiations with this fractal music in the demo 'Torus'
Minter released while developing the game. An Oscillator in 4 Parts delves into the
animation experiment in 'Torus', something that was used in the bonus screen in Iridis
Alpha itself.
Iridis Alpha is full of little additional extras. Congoatulations Hotshot! unpacks the
vertical scrolling mini-game Minter inserted as a bonus sequence. Even if the game
itself is a little half-baked this bonus sequence is full of intrepid effects and some clever
techniques for generating the game maps. Made in France and A Pause Mode for Your
Pause Mode unpick the pause mode games bundled with the main game.
As a \=nal coda, Iridis Oops! takes a look at some of the bugs that slipped through to
the \=nal release.
If you are reading the PDF version of this book the Appendices contain a data dump of
sprite sheets, character sets, maps and tables that should provide hours of rewarding
bedtime reading, or more likely, deep undisturbed sleep.
The version you are reading is little more than a scrapbook still many revisions away
from a \=nished product. If you \=nd the writing hard going or the attempts to explain
things dif\=cult to follow, by all means leave me a note and I will gratefully accept your
complaint. In the meantime, please skip over any blemishes to the next pretty picture
or promising-looking block of text.
The full source code is available in its own Github repository. You should \=nd that it
matches exactly the snippets of code provided in the book, though in some cases the
extracts in the book have been edited and reformatted for brevity.
10
We Need to Talk About Binary
Here we are, ground zero in every book about assembly programming. Where the
reader \=nds themselves in an awkdward sit-down discussion about the B word. It's
a necessary evil of course. Nothing will ever make sense without a rudimentary but
slowly growing grasp of what bytes are and why they keep being referred to in a con-
fusing mixture of letters and numbers such as $D012 and $F4.
So what am I supposed to do with you here? Succeed where every other author has
usually failed? I assume that's the expectation. OK here goes.
You already know, OK maybe you already know, that computers, even those in the
1980s, understand everything in terms of 1s and 0s. All it knows is a world in which
something is on or off, high or low, present or absent. The reason for this is simply be-
cause yes/no and on/off are so fundamental to the reality of very small things. Once
you break anything down far enough you are always left with a simple question of there
being somethere or nothing. The idea of a 1 or a 0 as the simplest possible building
blocks for every single thing is not so outrageous. You put enough 1s and 0s together
in a row you can build patterns that repeat and create larger patterns and suddenly you
\=nd yourself with a string of 1s and 0s that means something.
The idea of on/off is so simple even a computer can understand it. If you feed it a
signal all it has to do is determine whether it is one or the other and store the result.
Then read the next signal. And before long the computer has a sequence of 1s and 0s
it might be able to do something with.
So when you are programming a computer or in our case trying to understand how
it was programmed to give us Iridis Alpha you have to be able to visualize how the
computer has stored all the ones and zeros it has been given and how it shuffles them
around. An unruly mound of 1s and 0s is not much use when operating on them inten-
sively. There has to be some bene\=t to splitting up the blob in some way that allows
12
CHAPTER 1. WE NEED TO TALK ABOUT BINARY
some order to be imposed. Some way of segmenting a string of 1s and 0s that is both
useful to us as the programmers and an ef\=cient way of directing the computer to
make effective use of the amorphous blob fed into it.
Trial and error has eventually arrived at an optimal arrangement, one which you have
de\=nitely heard of: this is the byte. The idea of the byte is simply to divide and conquer.
We take any string of 1s and 0s and split them up into segments of eight. We now
consider ourselves to have a string of bytes and we call each of the individual 1s and
0s bits.
Any scheme that reduces the number of items we have to deal with is a boon, but is
there any rhyme or reason to choosing 8 as our magic number instead of say 7, or 12?
Believe it or not, the number 8 was chosen almost solely after much experimentation
with others because it proved the easiest and most convenient for people to deal with
when understanding how they would make computers work.
The reason a byte of 8 bits is so convenient to deal with is because of the number
of different values it can represent and the compact notation this particular number
allows. Having a convenient notation system that allows humans to compose larger
values from smaller values is surprisingly important when it comes down to it. It is
humans doing the important part of the computing: making the big decisions, \=guring
out where things go and how they should work. The computer is just a glori\=ed bit
shuffler for which everything is on or off and there is no bigger picture. Humans need
to be able to at least intuit some of this shuffling with a mental model of what happens
when one set of ones and zeros is clashed with another. Using the magic number of 8
as the denominator for batches of bits enables this simple mental model.
The \=rst great thing about 8 is that it can be divided into two. That is, into two segments
of 4 bits. The great thing about a sequence of four 1s and 0s is that it has 16 possible
permutations. That's to say it can be ordered in sixteen unique ways. Yet another way,
is to say that a sequence of 4 bits can store up to sixteen different values.
13
CHAPTER 1. WE NEED TO TALK ABOUT BINARY
You have to agree this is marvellous stuff. But what is so convenient about a number
between 0 and 15? If you're using decimal notation it's not convenient at all, in some
cases you end up having to write two whole characters rather than one. We could avoid
that if we invented a system that allowed a single character for each of the 16 values.
For example 0-9 for 0-9, and A-F for 10-15. Seems a bit clunky. But it works.
The genius of this system is that it allows us to write every possible sequence of 8 bits
with just two characters.
But that is not its only bene\=t. When we look at any speci\=c number using this notation
we can almost immediately tell how the bits in it are set, without needing to memorize
all 256 possible permutations.
This is possible because we can start by looking at either character in the two-
14
CHAPTER 1. WE NEED TO TALK ABOUT BINARY
character notation and quickly divine the 1s and 0s that make it up. We can do this
because each character represents a value between 0 and 15 that made is up of some
combination of 8, 4, 2, and 1 - each representing a bit from left to right.
8 4 2 1 Decimal Hex
0 0 0 0 0 0
0 0 0 1 1 1
0 0 1 0 2 2
. . . .
1 1 0 1 13 E
0 1 1 0 14 D
1 1 1 1 15 F
""All"" we have to do when confronted with a single hexadecimal digit like 9 is \=gure out
that its made up of 8 and 1 in order to know that the bits are set as follows:
8 4 2 1 Decimal Hex
1 0 0 1 9 9
Likewise with the slightly more inscrutable D. Since F is 15, D must be 13. And 13 is
made up of 8, 4, and 1:
8 4 2 1 Decimal Hex
1 1 0 1 13 D
The ability to do this is especially important when doing the low level programming
that is typical of assembly. We'll see plenty of examples in future chapters but here
is a taste of the kind of operation we ask a computer to perform by expressing our
intentions using hexadecimal notation for binary.
Imagine we have a value from some random source stored in a byte. Imagine we hap-
pen to be in need of a random number, for example to select a random planet to use
the next time we run attract mode in Iridis Alpha.
This random number we have stored in a byte sounds like it should be useful to us, but
15
CHAPTER 1. WE NEED TO TALK ABOUT BINARY
since its random it can be any value between 0 and 255 and since we only have four
planets to choose from what we need is a number between 0 and 3.
A simple binary operation provides a way of solving this problem and our hexadecimal
notation gives us a straightforward way of expressing it.
First the binary operation itself. Let's say our random value is a string of bits like
01011010 which is 5A in hexadecimal.
8 4 2 1 8 4 2 1 Decimal Hex
0 1 0 1 1 0 1 0 90 5A
Since we only want a value between 0 and 3 we can extract out just the last two bits
to give us that number. In other words, if we set all the other bits to zero but that last
two we would have a number between 0 and 3:
8 4 2 1 8 4 2 1 Decimal Hex
0 0 0 0 0 0 1 0 2 02
We can ask the computer to do this us by asking it to compare 01011010 with 00000011
and give use as a result all the bits where both are 1.
AND'ing $5A and $03 gives $01 (1). For AND to give a 1 both bits must 1 or both must be 0.
Notice that in this new bit string we've set the bits that we're interested in to 1. This is
called an AND operation. We have used 00000011 to mask out the bits we're not inter-
ested in and hence turn a random number between 0 and 255 into a random number
between 0 and 3.
Our choice of 8 as a magic number becomes slightly less magical when we realize
that we never had all possible numbers to choose from in the \=rst place. Sure we
could have chosen 7 or 9 or 13 but how practical would this really be when the actual
value assigned to any individual bit is always going to be a power of 2. Even if we could
16
CHAPTER 1. WE NEED TO TALK ABOUT BINARY
never precisely intuit the reasons as to why, a magic number that is also the power of
2, e.g. 8,4,16,64, is always going to make things easier over the long haul even if we
can't articulate them.
So now some jargon we might have heard in the past is starting to make sense. The
Commodore 64 was an '8-bit computer'. What this means is that its basic unit of cur-
rency is the 8-bit byte we've invented here. Everytime it performs an operation it does
so using a single byte as its fundamental building block. This is true whether we talk-
ing about the AND operation or storing a value for use later on. When we give the C64 a
value it's always a single byte. The subsequent ear of 16-bit computing (e.g. the Amiga)
took this a step further by introducing a two-byte block as its basic unit of currency.
32-bit and 64-bit computing are the lingua franc of modern processors - no matter how
small the value we're using it will be managed as 4-byte or 8-byte value respectively by
32-bit and 64-bit computer architectures.
But even the C64 had use for values larger than 255. In fact it could handle staggering
numbers as large as 65,535 using a little sleight of hand to put two bytes side by side
and treat them as a single number.
This was as far as its ken could reach however. And since it could look no further than
65,535 that imposed an upper bound on the number of values it could reach for at any
given time. This \=nite horizon is known as its address space. When we want to store
a value in the C64's memory we only have 65,536 slots available to specify or select
from. The address we choose must be between $0000 (0) and $FFFF (65,535).
43 36 34 00 40 .. 41 50 45 DE A2
$0000 $0001 $0002 $0003 $0004 .. $FFFC $FFFC $FFFD $FFFE $FFFF
Figure 1.1: The C64's memory visualized as a long strip of 65,536 slots, each holding a byte represented in
hexadecimal.
This upper limit is what gives the Commodore 64 its name. It has 64 kilobytes of
memory.
17
CHAPTER 1. WE NEED TO TALK ABOUT BINARY
This is all we need to get started. The rest we can pick up along the way. So let's start.
18
A Little Archaeology
Iridis Alpha was distributed on cassette tape by the publisher Hewson Consultants.
Normally used to play audio, cassette tapes were the cheap and ubiquitous medium du
jour of the 1980s and a natural choice for the nascent 8-bit game industry to disribute
its wares.
Playing a cassette tape for a C64 game such as Iridis Alpha on a normal cassette tape
player would be an ear-splitting mistake. Instead of music you would be subjected to
a cacophony of mechanical chirruping. This is the tape attempting to convey to you
its long stream of binary data in the only language available to it: lots of sound waves
of varying length.
19
CHAPTER 2. A LITTLE ARCHAEOLOGY
Without knowing how it's actually done, it's tempting to imagine a variety of possible
schemes that might have been used. For example, one sound wave denoting a '0' and
another one denoting a '1'. The actual method used isn't very far away from such a
thing but there is plenty of intricacy layered on top, particularly in a bid to spend as
little time as possible loading data from the tape.
In order to get something to work with our \=rst step is to somehow convert the contents
of the cassette tape into a binary \=le so that we can emulate the steps the C64's tape
player performed to read the sounds from the tape and load them into memory as
something that could be run as a computer program.
The earliest and most durable attempt at standardizing this was from Per Hakan Sun-
dell in 1997. He invented what is now known as the 'tap' format for representing the
beeps and bloops encoded on the tape as a \=le of bits and bytes. The idea is that each
byte in the \=le represents the length of a pulse. It is the length of these pulses that will
ultimately tell us whether we should interpret a value of 1 or 0. When we get eights 1s
and 0s we have a byte. We get enough bytes, we have a program we can run!
Someone, somewhere has kindly decoded the contents of the Iridis Alpha cassette
tape distribution to a 'tap' \=le for us. So we have something to dig into. This is going
to be a slightly bonkers journey into the bowels of decoding a 54kb game \=le from
over 500kb of raw data. Every time you think you are nearly done there will be yet
another convolution to wrap your head around. But at the end of it we will \=nally have
our binary game \=le and will be ready to \=gure out how to decipher it into something
approximating the original assembly langage.
20
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.2: Simple, right? Here each short-pulse sound is represented by a light grey pixel, each
medium-duration pulse by a blue pixel, and each long pulse by a pink pixel. Roughly speaking, gray is a
shorthand for 0 bits and dark pixels for 1 bits.
This is what the start of the our iridis-alpha.tap \=le looks like:
21
CHAPTER 2. A LITTLE ARCHAEOLOGY
43 36 34 2D 54 41 50 45 2D 52 41 57 00 00 00 00
5A 0A 08 00 00 5D 32 2F 30 2F 2F 30 30 31 30 31
30 31 31 2F 31 31 30 31 30 30 31 30 30 30 31 30
31 31 30 31 31 30 31 30 30 31 30 31 31 30 30 30
31 31 31 30 30 31 30 31 31 31 30 30 30 31 31 30
30 30 31 30 31 30 30 30 31 30 31 31 30 30 30 31
31 31 30 30 31 30 32 31 30 31 30 30 32 31 30 30
Figure 2.3: The leading material read by the Megasave loader in the run up to retrieving game data. The
Pilot Bytes are in red, the 'Sync Train' in blue, and the Data Header \=elds from the grey cell onwards.
Figure 2.4: The meaning of the \=rst batch of data we've read in.
After the header information described above, each byte in the tap \=le represents the
pulse length or duration of a single sound emitted by the tape. A pair of sounds taken
together represent a single bit, i.e. a 0 or a 1. A medium length sound followed by a
short one represents a 1, a short one followed by a medium one represents a 0.
The following table shows us whether we should consider a byte on the tape to repre-
sent short, medium, or long duration sound:
Figure 2.5: Values for short, medium, and long pulses. For example, ny byte value on the tap \=le between
$24 and $36 would be considered a 'Short' pulse.
22
CHAPTER 2. A LITTLE ARCHAEOLOGY
Remarkably the \=rst 27,000 or so pulses on the Iridis Alpha tape are nothing but short
sounds (values between $2F and $31) so cannot be interpreted as anything. It's not
until the 27,157th byte in the tape that we start to encounter real data:
00006a10 : 3031 3031 3156 4144 3130 4231 4243 3130 01011 VAD10B1BC10
00006a20 : 4231 4131 4244 312 f 4057 4032 4231 4231 B1A1BD1 /@W@2B1B1
00006a30 : 4244 312 f 4231 4231 4243 3142 3055 4144 BD1 / B1B1BC1B0UAD
00006a40 : 3142 3143 3130 4231 4231 4232 4244 3142 1 B1C10B1B1B2BD1B
00006a50 : 3055 4131 4243 3142 3130 4231 4231 4231 0UA1BC1B10B1B1B1
00006a60 : 4244 3130 4056 4143 3230 4143 3130 4231 BD10@VAC20AC10B1
00006a70 : 4231 4232 4244 312 f 4056 4232 4231 4243 B1B2BD1 /@VB2B1BC
00006a80 : 3230 4231 4231 4231 4243 3142 2 f54 4245 20B1B1B1BC1B / TBE
00006a90 : 3141 3130 4231 4231 4230 4230 4243 3030 1 A10B1B1B0B0BC00
Listing 2.1: Data \=nally gets started at 56 41 in the \=rst line above.
56 41 44 31 30 42 31 42 43 31 30 42 31 41 31 42 44 31 2 F 40
You get a sense of how wasteful, or ahem redundant, this encoding scheme is when
you learn that these twenty pulses are required to give us a single byte. The table below
shows how we interpret them to construct a series of 1s and 0s.
First Byte Second Byte First Byte Pulse Length Second Byte Pulse Length Meaning
$56 $41 Long Medium Start of Byte Indicator
$44 $31 Medium Short $01
$30 $42 Short Medium $00
$31 $42 Short Medium $00
$43 $31 Medium Short $01
$30 $42 Short Medium $00
$31 $41 Short Medium $00
$31 $42 Short Medium $00
$44 $31 Medium Short $01
$2F $40 Short Medium Parity Bit of $00
Figure 2.6: Interpretation of the \=rst 20 meaningful bytes creating a byte. The parity bit at the end is a $00
if there are an odd number of 1s and $01 if there are an even number of 1s. 10010001 has an odd number of
1s.
We can visualize the twenty bytes as a square sound wave. When reading the tape
the C64 would interpret these sound pulses as long, short, or medium to construct a
meaning for the entire sequence.
23
CHAPTER 2. A LITTLE ARCHAEOLOGY
The First 'Real' Byte of Data on the Tape as a Square Sound Wave.
1
Amplitude
0.5
0
0 5 10 15 20 25 30 35 40 45 50
Time\rightarrow
Figure 2.7: Medium-Short pairs in red represent a '1' bit, Short-Medium pairs in blue represent a '0' bit. So
this gives us '10010001'. The black wave form at the beginning is the 'Start of Byte' indicator and the one at
the end is a parity bit.
Once we've extracted our result of 10010001 from these twenty bytes we now must
reverse it. We must do this because the bits are encoded on the tape with the 'most
signi\=cant bit' \=rst and we are used to reading binary with the 'least signi\=cant bit \=rst'.
In hexadecimal the reversed bit-string of 10001001 is $89.
With this precious commodity in hand we now continue reading off bytes in the same
manner from the tape. Eventually we encounter a signal that tells us we've reached the
end of the data block, a 'Long-Short' sequence. When this happens we \=nd we've read
202 bytes in total:
24
CHAPTER 2. A LITTLE ARCHAEOLOGY
89 88 87 86 85 84 83 82 81 03 A7 02 04 03 49 52
49 44 49 53 00 00 00 00 00 00 00 00 00 00 78 A9
6E 8D 06 DD A2 01 20 D4 02 26 F7 A5 F7 C9 63 D0
F5 A0 64 20 E7 03 C9 63 F0 F9 C4 F7 D0 E8 20 E7
03 C8 D0 F6 C9 00 F0 D6 20 E7 03 99 2B 00 99 F9
00 C8 C0 0A D0 F2 A0 00 84 90 84 02 20 E7 03 91
F9 45 02 85 02 E6 F9 D0 02 E6 FA A5 F9 C5 2D A5
FA E5 2E 90 E7 20 E7 03 C8 84 C0 58 18 A9 00 8D
A0 02 20 93 FC 20 53 E4 A5 F7 45 02 05 90 F0 03
4C E2 FC A5 31 F0 03 4C B9 02 A5 32 F0 03 6C 2F
00 20 33 A5 A2 03 86 C6 BD F3 02 9D 76 02 CA D0
F7 4C E9 02 A9 07 85 F8 20 D4 02 26 F7 EE 20 D0
C6 F8 10 F4 A5 F7 60 00 00 E4
Figure 2.8: The data we've read in so far. The unshaded section is machine code.
Fortunately this data has a meaning. It contains the \=rst part of a machine code pro-
gram the C64 can execute:
This small program, once we have loaded the rest of it, is where the fun starts. But
before we do that we have lots more busywork to do. How about reading in all of the
25
CHAPTER 2. A LITTLE ARCHAEOLOGY
above data again from the tape? Yup, that's correct. As we keep reading the tape we
will \=nd that it contains all of the above data a second time, with the slight difference
that the 'Data Block Header' will be 09 08 07 06 05 04 03 02 01 instead of 89 88
87 86 85 84 83 82 81.
We have to read another 23,000 or so more pulses before we get to something new
that we're interested in, the second and \=nal part of the program that we can execute.
89 88 87 86 85 84 83 82 81 A9 80 05 91 4C EF F6
A9 A7 78 8D 28 03 A9 02 8D 29 03 58 A0 00 84 C6
84 C0 84 02 AD 11 D0 29 EF 8D 11 D0 CA D0 FD 88
D0 FA 78 4C 51 03 AD 0D DC 29 10 F0 F9 AD 0D DD
8E 07 DD 4A 4A A9 19 8D 0F DD 60 20 8E A6 A9 00
A8 91 7A 4C 74 A4 52 D5 0D 00 00 00 00 00 00 00
00 00 8B E3 AE 02 53
Figure 2.10: The data we've read in so far. The unshaded section is machine code.
Figure 2.11: The meaning of the second batch of data we've read in.
26
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.12: The bytes we've read so far from the tape.
Would you be surprised to learn that we have to read in this payload a second time from
the tape before we're done? Let's just assume that you're not and let's move swiftly
on to wondering what we're supposed to do with 100 or so bytes of data we've \=nally
managed to read after listening to some 50,000 chirrups and clicks from cassette tape.
The answer is simple. We load the data we've received into RAM and execute it. We
load the second chunk of data we received at address $02A7 (this was given in the
'Load Address' \=eld) and the \=rst chunk of data we received directly after it.
What is this program? It seems a bit short to be Iridis Alpha right? What it is is a whole
new program for reading the rest of the data from the tape. That's right: we've read
all this data from the tape to get a program for reading data from the tape. This type
of program is called a 'loader', or perhaps in an effort to justify it's existence, a 'turbo
loader'.
27
CHAPTER 2. A LITTLE ARCHAEOLOGY
The idea is that this little program will do a better job of reading data from the tape
and more quickly than the C64 can manage by itself. There is a whole menagerie of
these programs that proliferated in the 1980s with exotic names such as Jetload, Easy-
tape, Audiogenic and so on. Luigi di Fraia maintains a utility called tapclean that does
a great job of identifying and emulating these loaders and thanks to him we have a
disassembled version of the loader we've just found on the Iridis Alpha tape. It has
the quintessentially 1980s moniker 'MegaSave' and as you can see in the listing repro-
duced below is relatively compact.
; **************
; * CBM Header *
; **************
*=$033C
; Cassette I /O B u f f e r − Header
T033C
. b y t e $03 , $A7 , $02 , $04 , $03 , $49 , $52 , $49 , $44 , $49 , $53 , $00 , $00 , $00 , $00 , $00
T034C
. b y t e $00 , $00 , $00 , $00 , $00
AlignAndSynchronizeLoop
SEI
28
CHAPTER 2. A LITTLE ARCHAEOLOGY
LDX \#$01
align
JSR r d b i t ; Read a b i t
pilot
JSR r d b y t e ; Keep r e a d i n g byt es u n t i l the p i l o t t r a i n i s over
CMP \#$63
BEQ p i l o t
sync
CPY $F7 ; I s the c u r r e n t l y expected sync b y t e f o l l o w i n g ?
BNE a l i g n ; S t a r t over i f not
JSR r d b y t e ; Read b y t e
header
JSR r d b y t e ; Read 10 header byt es
STA $002B , Y ; O v e r w r i t e BASIC program p o i n t e r s
STA $00F9 , Y ; Also s t o r e them i n RAM where t h e y w i l l be changed
INY
CPY \#$0A
BNE header
LDY \#$00
STY $90 ; Zero the s t a t u s f l a g s ( not s e t by t h i s code )
STY $02 ; Zero the checkbyte r e g i s t e r
data
JSR r d b y t e ; Read a b y t e
STA ( $F9 ) , Y ; S t o r e i n RAM
check complete
JSR r d b y t e ; Read a b y t e
INY
STY $C0 ; C o n t r o l motor v i a software
CLI
CLC
; Make sure the c a l l t o $FC93 does not r e s t o r e the standard IRQ
LDA \#$00
29
CHAPTER 2. A LITTLE ARCHAEOLOGY
STA $02A0
; D i s a b l e i n t e r r u p t s , un− b la nk the screen , and stop c a s s e t t e motor
JSR $FC93
; Code e x e c u t i o n a f t e r a f i l e i s c o m p l e t e l y loaded i n
JMP J02E9
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; Read b y t e : read 8 b i t s from tape , grouping them MSbF
; Returns : the read b y t e i n A
rd byte
LDA \#$07 ; Prepare f o r 8 b i t s
STA $F8 ; Using $f8 as a c o u n t e r
B03EB
JSR r d b i t ; Read a b i t
; S h i f t each of them i n t o the b y t e r e c e i v e r e g i s t e r , MSbF
ROL $F7
INC $D020
DEC $F8 ; And loop u n t i l 8 b i t s are r e c e i v e d
BPL B03EB
LDA $F7 ; Return read b y t e i n A
RTS
. c e r r o r * > $03FC , "" The CBM Header code i s too long t o f i t i n the tape b u f f e r ! ""
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; ************
; * CBM Data *
; ************
*=$02A7
NMIH
LDA \#$80
ORA $91
JMP $F6EF
J02AE
LDA \#<NMIH
SEI
J02B9
CLI
LDY \#$00
STY $C6 ; No char i n keyboard b u f f e r
30
CHAPTER 2. A LITTLE ARCHAEOLOGY
DEX ; Wait a b i t
BNE * −1
DEY
BNE *−4
SEI
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; Read b i t : loops u n t i l a f a l l i n g edge i s r e c e i v e d on the read l i n e
; and uses the read b i t t i m e r / c o u n t e r t o d i s c r i m i n a t e
; the c u r r e n t b i t v a l u e
;
; Returns : the read b i t i n the C a r r y f l a g
rd bit
LDA $DC0D ; Loop u n t i l a f a l l i n g edge i s d e t e c t e d
AND \#$10 ; on the read l i n e of the tape p o r t
BEQ r d b i t
LDA $DD0D
STX $DD07
LSR
LSR ; Move read b i t i n t o the C a r r y f l a g
RTS
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
J02E9
; Reset p o i n t e r t o c u r r e n t t e x t c h a r a c t e r t o the b e g i n n i n g
; of program t e x t
JSR $A68E
LDA \#$00
TAY
STA ( $7A ) , Y
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; C h a r a c t e r s t o be i n j e c t e d i n the keyboard b u f f e r , i f required
T02F4
. b y t e $52 , $D5 , $0D ; R , <s h i f t> + U , <r e t u r n>
. c e r r o r * > $0300 ,
"" The CBM Data code i s too long t o f i t i n f r o n t of the v e c t o r t a b l e ! ""
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; O v e r w r i t e BASIC v e c t o r s i n RAM
T0300
.word $E38B ; Leave IERROR unchanged
; A u t o s t a r t the t u r b o l o a d e r , once loaded , by o v e r w r i t i n g IMAIN
.word J02AE
Listing 2.2: The data we have just loaded translated back into assembly language. This is the MegaSave
loader.
What does MegaSave do that makes it so much faster than the default C64 tape
loader? The simple answer is that it cuts corners and strips away all of the cautious
redundancy we observed when loading the loader itself. Instead of reading 20 bytes
(or pulses) from the tape in order to construct a single byte it just needs 8. It does
what we naively thought at the beginning might be the way to read data from the tape:
31
CHAPTER 2. A LITTLE ARCHAEOLOGY
a long pulse is a 0, a short pulse is a 1, you read 8 of them you have the 8 bits for your
byte.
This simplicity is risky. With no repetition of data blocks, no parity check on each indi-
vidual byte, and no delimiters between bytes, the loader is vulnerable to corruption of
the tape itself or any hardware flakiness in the cassette reader. The fact that it gener-
ally works is simply due to a lack of conservatism paying off in practice. In addition,
the MegaSave loader isn't totally bereft of precautions. There is a slightly elaborate
dance it goes through to gain some assurance that the tape medium is going to yield
a reliable string of bytes.
The First Pilot Byte of '63' as read by the loader from the tape.
1
Amplitude
0.5
0
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
Time\rightarrow
Figure 2.14: Long square waves in red represent pulses giving a '1' bit, Short square waves in blue represent
a '0' bit. So this gives us '01100011' i.e. $63. Unlike the default tape loader, MegaSave expects the 'most
signi\=cant bit \=rst' - which is the natural way of representing bits on paper so we don't need to reverse the
bits to 'make sense' of them.
.
32
CHAPTER 2. A LITTLE ARCHAEOLOGY
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63
63 63 63 63 63 64 64 65 66 67 68 69 6A 6B 6C 6D 6E
6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F
80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90
91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1
A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2
B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3
C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4
D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5
E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6
F7 F8 F9 FA FB FC FD FE FF 01 00 08 FF BF 00 00 01
02 CA 00
Figure 2.15: The leading material read by the Megasave loader in the run up to retrieving game data. The
Pilot Bytes are in red, the 'Sync Train' in blue, and the Data Header \=elds from the grey cell onwards.
The bytes after the blue cells above are our \=rst bit of raw meat in a while. Here's what
they mean:
33
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.16: The meaning of the Data Header values read in by MegaSave.
What the loader can garner from this is that the data that follows should be read in
and stored at $0800 and that rather than execute it straight away it should then resume
loading more data from the tape.
The entire game is stored across four separate chunks on the tape. Once it has loaded
this \=rst one, the loader reads in the next three chunks.
34
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.17: All the data that has been read from the tape. The four chunks of game data are in green the
third is only a sliver. The relative sizes of the red data (the MegaSave loader which is only actually 200 or
so bytes long) and the green data (representing over 50,000 bytes of game data) illustrates how ef\=cient
the MegaSave loader's storage is by comparison with the default.
35
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.18: Our image of the spool from the start of this chapter but this time with each section colored in
as described in the previous image.
36
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.20: Where the different parts of the game end up in memory.
With all the data read in you might wonder how the loader knows what to do next (i.e.
to run the game) and how it will know where to start running it from. The answer is
given in the Header Data for the \=nal chunk of data:
37
CHAPTER 2. A LITTLE ARCHAEOLOGY
Figure 2.21: The meaning of the Data Header values in the \=nal chunk of data read in by MegaSave.
So according to the header data, the loader should stop reading data now and start ex-
ecuting what has already been loaded, and it should start doing this at address $0810.
*= $0810
Start Executi on
SEI
; Tell the C64 to execute the code at M a in Co nt r ol Lo op
; the next time an interrupt happens.
LDA \# ?` Ma in Co n tr ol Lo o p
STA $0319 ; NMI
LDA \# !` Ma in Co n tr ol Lo o p
STA $0318 ; NMI
Listing 2.3: The \=rst piece of code that is executed in Iridis Alpha.
This routine does two things: it turns off the tape deck and tells the C64 to execute
the code at a different location (MainControlLoop) the next time it wakes up and won-
ders what to do. This will be in a few microseconds time. When it starts executing
MainControlLoop, Iridis Alpha will get underway.
38
CHAPTER 2. A LITTLE ARCHAEOLOGY
39
Some Disassembly Required
We've reached the point where the game has started to execute. We just saw a snippet
of code that turned off the tape recorder and prompted the C64 to run a routine a
called MainControlLoop. Though perhaps a little cryptic, and you shouldn't expect to
understand it yet, this code is not exactly what the machine 'saw'. Instead it read and
executed something far more puzzling looking:
Listing 3.1: The \=rst piece of machine code that is executed in Iridis Alpha.
Before we can dig into the internals of how Iridis Alpha works we have to convert all of
the machine code we've loaded into memory in the previous chapter into something
we have a chance of reading and understanding. This process is called disassembly
and here we're going to explain how it is done and along the way gain a little basic
understanding of the human-readable language, called 6502 Assembly Language, that
we convert the machine code back into.
The process is called disassembly simply because it is the exact reverse of the pro-
cess that was originally followed to generate the data on the tape from the assembly
language written by Jeff Minter in the \=rst place. Programs that do this are referred to
as 'assemblers'. They assemble the instructions written by the programmer into ma-
40
CHAPTER 3. SOME DISASSEMBLY REQUIRED
chine code that the C64 can execute. As self-appointed disassemblers we are going
to turn it back into assembly language.
Along the way we will have to invent names for things that are meaningful to us: the
names Jeff Minter gave to his functions, routines, and variables are long since lost
to us. They were thrown away by the assembler as unnecessary for execution of the
machine code. As we proceed, we will see why but it hopefully makes sense to say
that the 6502 CPU in the C64 doesn't care what things are called it just cares where in
the 65,632 bytes of its RAM things are located. That is to say it only cares about their
'address'.
So let's start by stepping back for a second. Let's put the machine code and the snippet
of code we disassembled it back into and see what we can learn:
78 SEI
A9 40 LDA \# $40
8D 19 03 STA $0319
A9 00 LDA \# $00
8D 18 03 STA $0318
A9 10 LDA \# $10
8D 04 DD STA $DD04
A9 00 LDA \# $00
8D 05 DD STA $DD05
A9 7F LDA \# $7F
8D 0D DD STA $DD0D
A9 81 LDA \# $81
8D 0D DD STA $DD0D
A9 19 LDA \# $19
8D 0E DD STA $DD0E
58 CLI
4C 35 08 JMP $0835
Byte Instruction
78 SEI
A9 LDA
8D STA
58 CLI
4C JMP
Figure 3.1: Machine code bytes and their assembly language counterparts.
41
CHAPTER 3. SOME DISASSEMBLY REQUIRED
Let's quickly explain what two of these mean as they're extremely common and also
fundamental to machine code programming in general.
LDA loads the byte you give it into a small one-byte slot called the 'Accumulator' ('A'
for short so LDA is an abbreviation for 'Load to Accumulator'). Think of this slot as a
pocket - somewhere for the CPU to store a value for use later on. The technical name
for this kind of slot/pocket is a 'register'. The 'Accumulator' is so-called because a lot
of the operations performed on bytes stored in this particular pocket have very precise
behaviour when addition operations are executed on it. But we never really have to
worry about this too often and in the general case it is simply used, as here, as a place
to put a value so we can do something else with it.
LDA \# $40
What we do with it for the most part in the code above is go on to store it somewhere
else. This is where the STA instruction comes in. This stores the value in the 'Accumula-
tor' pocket at the address in memory that you give to it. As we can see such addresses
are not one byte long, but two bytes. Are they ever more than two bytes long? No, and
for a very simple reason. The C64 can only understand addresses that are at most
two bytes long and this is what ultimately limits it to 64KB of memory. The largest
address it can understand is therefore $FFFF - the largest value that can be expressed
by 2 bytes. Which translates to 65,532. 65,532 bytes is 64KB of RAM.
When we look at the disassembly of the STA instructions we see something quite puz-
zling:
8 D 19 03 STA $0319
42
CHAPTER 3. SOME DISASSEMBLY REQUIRED
of the two numbers. This approach is known as 'little-endian' byte ordering. The op-
posite, which is closer to what we are familiar with when reading numbers ourselves,
is called 'big-endian'. If you had never heard of either of those terms before, then you
have now.
The next incremental step in understanding our disassembly is to apply some mean-
ing to these two-byte values that we've decoded. As it happens, all the ones given in
this piece of code represent addresses in the C64's memory of $FFFF bytes, or in the
preferred parlance of we humans, 65,532 bytes. Each address here has a particular
function so storing a value in it does something. Let's add the meanings, which are
still a little cryptic, to our listing:
SEI
LDA \# $40 SEI
STA $0319 LDA \# $40
LDA \# $00 STA $0319 ; Non - Maskable Interrupt
STA $0318 LDA \# $00
LDA \# $10 STA $0318 ; Non - Maskable Interrupt
STA $DD04 LDA \# $10
LDA \# $00 STA $DD04 ; CIA2 : Timer A : Low - Byte
STA $DD05 LDA \# $00
LDA \# $7F STA $DD05 ; CIA2 : Timer A : High - Byte
STA $DD0D LDA \# $7F
LDA \# $81 STA $DD0D ; CIA2 : CIA Interrupt Register
STA $DD0D LDA \# $81
LDA \# $19 STA $DD0D ; CIA2 : CIA Interrupt Register
STA $DD0E LDA \# $19
CLI STA $DD0E ; CIA2 : CIA Control Register A
JMP $0835 CLI
JMP $0835 ; Jump to address $0835
Listing 3.5:
Assembly Listing 3.6: Assembly Language with some comments
As you may remember we said in the previous chapter that this little routine is doing
two things: the \=rst is telling the C64 to jump to and execute the routine that starts
the actual game the next time it 'wakes up'; the other thing it's doing is turning off the
cassette tape reader so that no more data is read from the tape.
LDA \# $10
STA $DD04 ; CIA2 : Timer A : Low - Byte
LDA \# $00
STA $DD05 ; CIA2 : Timer A : High - Byte
LDA \# $7F
STA $DD0D ; CIA2 : CIA Interrupt Control Register
LDA \# $81
STA $DD0D ; CIA2 : CIA Interrupt Control Register
43
CHAPTER 3. SOME DISASSEMBLY REQUIRED
LDA \# $19
STA $DD0E ; CIA2 : CIA Control Register A
We are better off treating this series of instructions as a magic formula. The operation
of the tape reader is managed by the values stored in a series of bytes between $DD04
and $DD0F. The fact that we have to write a variety of values to 5 of them to just stop
reading from the tape is just a testament to the power of boring overhead - we'll never
interact with the tape reader again so studying the entrails here is not going to bene\=t
us in any way.
The other thing this routine is doing - preparing the C64 to execute the game proper - is
more compact and introduces two concepts that are going to be important throughout
our tour of the Iridis Alpha code so it is worth spending some time on them here to get
them clear.
LDA \# $40
STA $0319 ; Non - Maskable Interrupt
LDA \# $00
STA $0318 ; Non - Maskable Interrupt
3.0.1 Important Concept Number One: High Bytes and Low Bytes
On the face of it these four instructions are doing something very simple. They are
storing the value $40 at address $0319 and the value $00 at $0318. What they are
actually doing is storing the address $4000 in a place where the C64's 6502 CPU will
be expected to look in a moment's time and take that as a command to start executing
whatever is at address $4000.
43 36 34 00 40 41 50 45
Figure 3.2: The slice of C64 memory at addresses $0315 and $031C and the bytes that live there after
we've written $40 to $0319 and $00 to $0318.
The reversed order we spoke about earlier, the 'little-endian' order, in which the 6502
CPU stores and reads pairs of bytes is observed again here. When the CPU reads the
contents of $0318 and $0319 it interprets them not as $0040, which is the order in which
they appear to us, but as $4000.
44
CHAPTER 3. SOME DISASSEMBLY REQUIRED
When discussing this storage arrangement we refer to the contents of $0318 as the
'Low Byte' and the contents of $0319 as the 'High Byte'. 'High' is just another way of
saying '\=rst', and 'Low' another way of saying 'second. When taking about a values like
$4000 stored in $0318-$0319 such as in this case $40 is the 'High Byte' and $00 is the
'Low Byte'.
Writing values to a pair of adjacent addresses in memory like this so that they can be
subsequently interpreted as yet another address to get something from or do some-
thing with is a very common pattern in programming 6502 CPUs such as the C64's and
we will encounter it a *lot* in this book.
It's a strange sort of indirection when you \=rst attempt to understand it. Instead of
storing actual values at an address we're storing an address in the address. If you are
familiar with other programming languages you may already recognize this concept
and understand how powerful it can be. If you are not it will probably seem strange
and maybe even wasteful. Seeing the many uses it is put to in the Iridis Alpha code
may persuade you otherwise, but hopefully when we look at the use it is put to here you
may begin to get a flavor of its utility. Let's do that by looking at our second important
concept.
LDA \# $40
STA $0319 ; Non - Maskable Interrupt
LDA \# $00
STA $0318 ; Non - Maskable Interrupt
We previously waved away what's happening in this code by saying that the address
$4000 will be interpreted as an address to jump to and start executing the next time
the 6502 CPU 'wakes up'. That's a lot of hand-waving.
The technical term for this 'waking up' is an 'interrupt'. This waking up happens incred-
ibly frequently, 60 times every second. 60 times a second the C64 will stop what it's
doing and execute whatever is given as an address by the bytes at $0318-$0319.
The number 60 may ring a bell for you in this context. A common aspiration for graph-
ics-based games, and minimum table stakes today, is that a game runs at 60 'frames
per second'. In other words, that at least 60 times a second the display is updated and
whatever is on the screen moves a little bit.
While a C64 programmer would never achieve 60 frames per second in practice, 'inter-
rupts' are the C64's mechanism for at least getting some of the way there. They allow
45
CHAPTER 3. SOME DISASSEMBLY REQUIRED
the game developer to at least do something to the display many times per second.
Whatever it is, it has to be something short and sweet and at the same time effective
enough to actually progress the gameplay. This is why this concept is important to
us: most of the important things that happen in Iridis Alpha will be effected during an
interrupt. When we look at moving, shooting, and blowing things up, all of them are
going to happen in routines that are called multiple times per second by the 6502 CPU
executing the code that has been stored at the address $0318-$0319 at that point in
time.
And this is our \=rst taste of the power of storing an address in an address. Depending
on where we are in the game - be it the title screen, the main game, the bonus phase,
or in a pause mode sub-game, the code that we want to run during these interrupts will
be different.
We can see this in action if we look at what happens when the code at $4000 is exe-
cuted. We've given this address $4000 a 'label' name of MainControlLoop in our dis-
assembled code so MainControlLoop always refers to whatever lives at $4000.
M ai nC on t ro lL oo p
LDA \# $00
SEI
p4003 LDA \# !` M a i n C o n t r o l L o o p I n t e r r u p t H a n d l e r
STA $318 ; NMI
LDA \# ?` M a i n C o n t r o l L o o p I n t e r r u p t H a n d l e r
STA $319 ; NMI
As you can see the \=rst thing it does is change the address of the code that should be
executed at every 'interrupt'. It's now an address referred to by the label MainControlLoop-
InterruptHandler. This happens to be address $6B3E but the beauty of using labels
in our code is that we no longer need to worry about the number of the addresses
anymore. The label will do the job for us. It does mean we have to know what the
syntax \#!`MainControlLoopInterruptHandler means though. What it means is: if
MainControlLoopInterruptHandler lives at $6B3E the \#!` decorators refer to the $3E
part of the address, so we're actually saying LDA \#$3E, i.e. load the value $3E into the
'Accumulator'. Similarly the syntax \#?`MainControlLoopInterruptHandler refers to
the $6B part of the address.
43 36 34 3E 6B 41 50 45
Figure 3.3: The values in $0318 and $0319 after we've updated them in MainControlLoop.
46
CHAPTER 3. SOME DISASSEMBLY REQUIRED
We've almost squeezed as much as we can out of this short and boring snippet of
code. There's just one last thing to point out that will be useful to us later. The last
instruction in the routine:
JMP $0835
This tells the CPU to jump to the address $0835 and execute whatever is there. What
is at $0835? This:
JMP $0835
That's right - it's jumping back to itself and will execute in an in\=nite loop repeatedly
executing the instruction over and over again. The reason it does this is because it
won't be doing it very long. The interrupt will take over and steal execution away so
that the C64 can \=nd better things to do that run in circles.
Before we move on let's take one last look at our disassembly accomplishment.
Start Executi on
SEI
; Tell the C64 to execute the code at M a in Co nt r ol Lo op
; the next time an interrupt happens.
LDA \# ?` Ma in Co n tr ol Lo o p
STA $0319 ; NMI
LDA \# !` Ma in Co n tr ol Lo o p
STA $0318 ; NMI
Hopefully with these basics under our belt we can begin to understand how the inter-
esting parts of Iridis Alpha work.
47
The First 16 Milliseconds
The race is now on to get the title sequence up on the screen. After a little more setup
in MainControlLoop we call a routine to set up the main title screen:
48
CHAPTER 4. THE FIRST 16 MILLISECONDS
You'll notice we've changed our interrupt handler again, this time to a routine called
TitleScreenInterruptHandler. What we also do is make this interrupt something
called a 'Raster Interrupt'. A 'raster' can be thought of as a beam of light that scans
across the screen from top to bottom and left to right painting each pixel on the screen
one at a time. It travels so quickly down and across the screen painting pixels that it
can do so up to 60 times a second. As it makes this journey our 'Raster Interrupt' gives
us the opportunity to tell it to stop once it reaches a certain position on the screen and
allow us to run some code before it resumes again. We can do this as many times
as we want along the journey, but each time we interrupt it we have to be quick. If
our code takes too long the display will flicker and the content of the screen become
inconsistent.
In this routine we set our \=rst interrupt to line 16 ($10 on the screen:
This facility is the key that will allow us to do all sorts of magic in the 16 milliseconds
it takes to traverse the screen. Every time we get the opportunity to run some code
thanks to this interrupt we'll change the location of the screen it should stop at the next
time so that we get to stop dozens of times in each single 16 millisecond traversal.
Before we look at how we \=t it all in, let's \=rst appreciate just how much we plan to do
each time the screen is painted.
4.1 Sprites
The C64 makes 8 sprites available to us. A sprite is a special purpose graphical object
that can be up to 24 pixels wide by 20 pixels high. We can place them wherever we
want on the screen. They are the core of graphics programming and Iridis Alpha has
dozens of them. But if the C64 only has 8 sprites, are we limited to displaying just 8
sprites at once on the screen? The simple answer is that thanks to Raster Interrupts we
are not: when we run some code after receiving an interrupt we can place new sprites
wherever we like in any position that the raster hasn't reached yet. This means our
49
CHAPTER 4. THE FIRST 16 MILLISECONDS
only effective limitation is the number of sprites we can place on a single line, which
is eight.
If you look carefully at the title screen of Iridis Alpha you'll notice that it is actually split
in two. The top half has the title in large letters and the bottom half has a rainbow of
jumping gilbies. Each half uses seven sprites to display these assets.
LAND GILBY1 LAND GILBY2 LAND GILBY3 LAND GILBY4 LAND GILBY5 LAND GILBY6 LAND GILBY7
Figure 4.1: The sprites used by the top half of the screen and the bottom half of the screen.
The eighth sprite (Sprite 7) is used on both halves of the screen to display the star\=eld.
This sprite pushes right up against the line limitation. It's painted at intervals through-
out the screen but we're careful to avoid it ever being painted twice on the same line.
We'll see how this is achieved very soon.
STARFIELD SPRITE
Figure 4.2: The sprite used for painting the star\=eld. Only a part of the sprite is ever painted!
50
CHAPTER 4. THE FIRST 16 MILLISECONDS
Before it comes in we just have time to prepare the relatively light amount of text we
want displayed on the screen in memory. We only need to do this once. Throughout
the code we refer to this area we write to as SCREEN RAM. It's an address range between
$0400 and $07E8 This is a very simple bitmap representation of the entire screen that
is 40 characters wide and 25 characters wide, giving a total of 1000 bytes ($3E8 bytes
in hex). If we wanted to think of it as pixels it is 320 pixels wide (40 * 8) and 200 pixels
high (25 * 8). The important thing to remember about this SCREEN RAM is that it is solely
for storing what we call character data. You can think of character data as 'text'. This
text gets painted \=rst and then sprites get painted on top of it.
In EnterTitleScreenLoop we call two routines that will prepare the character data for
the raster to paint.
The \=rst, DrawStripesBehindTitle writes the rainbow stripes to \=ve lines in the top
half of the screen. The second, DrawTitleScreenText writes some text to the bottom
half of the screen. Before we look at these in detail we need to understand how this
thing SCREEN RAM works and how we store characters for display in it.
Our starting point for displaying text on screen is to de\=ne what our characters look
like. We de\=ne the appearance of a character using 8 bytes. This is what the de\=nition
of the stripe character looks like:
characterSetData
.BYTE $FF , $00 , $FF , $00 , $00 , $FF , $00 , $FF ; .BYTE $FF , $00 , $FF , $00 , $00 , $FF , $00 , $FF
; CHARACTER $00
; 11111111 ********
; 00000000
; 11111111 ********
; 00000000
; 00000000
; 11111111 ********
; 00000000
; 11111111 ********
As you can see each byte translates to a row of 0s and 1s. Each 1 de\=nes a dot and
each 0 a blank space. We end up with a character that is 8 pixels wide and 8 pixels
high:
51
CHAPTER 4. THE FIRST 16 MILLISECONDS
1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1
We create this de\=nition for every character we want to display and store it at the ad-
dress starting at $2000 in RAM. The order in which we store them determines the ref-
erence we use for them later. So for example the stripe character is referred to as $00,
the 'A' character we've de\=ned as $01 and so on:
52
CHAPTER 4. THE FIRST 16 MILLISECONDS
With our character set de\=ned we can now write some text to the screen ram. Note that
when we write it SCREEN RAM we're not yet writing it to the actual screen. This is just a
place in memory that the raster (our beam of light) will refer to later when it is actually
writing dots to the screen. If we write a stripe character to a particulas position in this
SCREEN RAM memory it will know to write it the corresponding position on the screen.
53
CHAPTER 4. THE FIRST 16 MILLISECONDS
DrawStripesBehindTitle
LDX \# $28
LDA \# $00
STA s h o u l d U p d a t e T i t l e S c r e e n C o l o r s
D ra wS tr i pe sL oo p
LDA \# RED
STA COLOR\.RAM + LINE2\.COL39 ,X
LDA \# ORANGE
STA COLOR\.RAM + LINE3\.COL39 ,X
LDA \# YELLOW
STA COLOR\.RAM + LINE4\.COL39 ,X
LDA \# GREEN
STA COLOR\.RAM + LINE5\.COL39 ,X
LDA \# LTBLUE
STA COLOR\.RAM + LINE6\.COL39 ,X
LDA \# PURPLE
STA COLOR\.RAM + LINE7\.COL39 ,X
LDA \# BLUE
STA COLOR\.RAM + LINE8\.COL39 ,X
LDA \# $00 ; Stripe character
STA SCREEN\.RAM + LINE2\.COL39 ,X
STA SCREEN\.RAM + LINE3\.COL39 ,X
STA SCREEN\.RAM + LINE4\.COL39 ,X
STA SCREEN\.RAM + LINE5\.COL39 ,X
STA SCREEN\.RAM + LINE6\.COL39 ,X
STA SCREEN\.RAM + LINE7\.COL39 ,X
STA SCREEN\.RAM + LINE8\.COL39 ,X
DEX
BNE D ra wS t ri pe sL o op
As you can hopefully see, what we're dealing with here is a loop. We load X with the
value $28 (40 in decimal) and perform everything inside DrawStripesLoop until DEX
has reduced the value of X to zero.
The magic number 40 gives us a clue that what we are doing in each loop is drawing a
character in each column of the screen: remember that our screen is 40 columns wide
and 25 rows high. The bit actually writing the stripe character to RAM is:
54
CHAPTER 4. THE FIRST 16 MILLISECONDS
For the current column, this writes the stripe character (reference by $00 as we men-
tioned above) to each of lines 2 to 8. The use of the X in the STA statement is an offset.
So where X is 14, for example, it will write to the position referred to by SCREEN RAM +
LINE2 COL39 plus 14.
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
Figure 4.4: The shaded areas of SCREEN RAM after they have been written to by DrawStripesBehindTitle.
The other thing we do in DrawStripesLoop is set the colors of the stripes. This is
achieved using a region of memory similar in concept to SCREEN RAM, that we call
COLOR RAM. This lives at $D800 - $DBFE. Another region of 1000 bytes, each one con-
trolling the color of the character placed at a position in the 40 * 25 character rectangle
of our screen.
LDA \# RED
STA COLOR\.RAM + LINE2\.COL39 ,X
LDA \# ORANGE
STA COLOR\.RAM + LINE3\.COL39 ,X
LDA \# YELLOW
STA COLOR\.RAM + LINE4\.COL39 ,X
LDA \# GREEN
55
CHAPTER 4. THE FIRST 16 MILLISECONDS
We've used a meaningful alias for each of the color values that we write, these are
de\=ned as:
RED = $02
PURPLE = $04
GREEN = $05
BLUE = $06
YELLOW = $07
ORANGE = $08
BROWN = $09
LTBLUE = $0E
So by writing a value to the corresponding place in COLOR RAM, we're de\=ning the color
of the character in that position.
56
CHAPTER 4. THE FIRST 16 MILLISECONDS
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02
08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08
07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07
05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05
0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E
04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04
06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
Figure 4.5: The shaded areas of COLOR RAM after they have been written to by DrawStripesBehindTitle.
Next up is to write out the title screen's text to SCREEN RAM. This we do in DrawTitleScreenText
using a similar loop to DrawStripesBehindTitle.
DrawTitleTextLoop
LDA t i t l e S c r e e n T e x t L i n e 1 - $01 , X
AND \# ASCII\.BITMASK
STA SCREEN\.RAM + LINE11\.COL39 ,X
LDA t i t l e S c r e e n T e x t L i n e 2 - $01 , X
AND \# ASCII\.BITMASK
STA SCREEN\.RAM + LINE13\.COL39 ,X
LDA t i t l e S c r e e n T e x t L i n e 3 - $01 , X
AND \# ASCII\.BITMASK
STA SCREEN\.RAM + LINE15\.COL39 ,X
LDA t i t l e S c r e e n T e x t L i n e 4 - $01 , X
AND \# ASCII\.BITMASK
STA SCREEN\.RAM + LINE17\.COL39 ,X
LDA t i t l e S c r e e n T e x t L i n e 5 - $01 , X
AND \# ASCII\.BITMASK
STA SCREEN\.RAM + LINE19\.COL39 ,X
LDA \# GRAY2
57
CHAPTER 4. THE FIRST 16 MILLISECONDS
In this case we're not writing a single character over and over, rather we're writing text
we've de\=ned elsewhere in variables titleScreenTextLine[1-5]:
In each iteration of the loop we write a character to all \=ve columns, plucking it from
the position in titleScreenTextLine[1-5] given by X.
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
09 12 09 04 09 13 20 01 0C 10 08 01 2E 2E 2E 2E 2E 20 20 08 01 12 04 20 01 0E 04 20 06 01 13 14 20 1A 01 10 10 09 0E 07
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
10 12 05 13 13 20 06 09 12 05 20 14 0F 20 02 05 07 09 0E 20 10 0C 01 19 2E 2E 20 0F 0E 03 05 20 13 14 01 12 14 05 04 2C
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
06 31 20 06 0F 12 20 10 01 15 13 05 20 0D 0F 04 05 20 20 20 20 20 11 20 14 0F 20 11 15 09 14 20 14 08 05 20 07 01 0D 05
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
03 12 05 01 14 05 04 20 02 19 20 0A 05 06 06 20 0D 09 0E 14 05 12 2E 2E 2E 13 10 01 03 05 20 05 01 13 19 2F 08 01 12 04
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
0C 01 13 14 20 07 09 0C 02 19 20 08 09 14 20 30 30 30 30 30 30 30 3B 20 0D 0F 04 05 20 09 13 20 0E 0F 17 20 05 01 13 19
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
Figure 4.6: The shaded areas of SCREEN RAM after they have been written to by DrawStripesBehindTitle
and DrawTitleScreenText.
58
CHAPTER 4. THE FIRST 16 MILLISECONDS
While writing text for the column we also set the color for each of the text lines to grey:
LDA \# GRAY2
STA COLOR\.RAM + LINE11\.COL39 ,X
STA COLOR\.RAM + LINE13\.COL39 ,X
STA COLOR\.RAM + LINE15\.COL39 ,X
STA COLOR\.RAM + LINE17\.COL39 ,X
STA COLOR\.RAM + LINE19\.COL39 ,X
Once it is done the COLOR RAM has the appropriate lines set to grey:
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02
08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08
07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07
05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05 05
0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E
04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04 04
06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06
01 02 02 08 08 08 07 07 07 05 05 05 0E 0E 0E 07 07 09 01 0B 07 03 00 07 02 0B 0B 0B 0B 0C 0C 0C 0C 0F 0F 0F 0F 01 01 01
01 04 00 07 09 0C 02 09 05 03 00 0F 06 00 0F 02 02 08 08 08 07 07 07 05 05 05 0E 0E 0E 07 07 09 01 0B 07 03 00 07 02 05
0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C
01 04 00 07 09 0C 02 09 05 03 00 0F 06 00 0F 02 02 08 08 08 07 07 07 05 05 05 0E 0E 0E 07 07 09 01 0B 07 03 00 07 02 05
0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C
01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C
01 01 00 01 01 01 01 04 04 01 01 01 01 01 01 01 01 01 01 01 00 00 01 01 01 01 00 07 07 00 01 01 01 01 01 01 01 01 01 01
01 01 00 02 07 07 05 05 07 07 02 00 00 00 00 00 00 00 01 01 01 01 00 01 05 00 00 07 07 00 07 07 04 04 0E 0E 08 08 0A 0A
01 01 00 02 07 07 05 05 07 07 02 00 00 02 02 08 08 07 07 05 05 0E 0E 04 04 06 06 07 07 00 07 07 04 04 0E 0E 08 08 0A 0A
01 01 00 01 01 01 01 04 04 01 01 01 01 01 01 01 01 01 01 01 00 00 01 01 01 01 00 07 07 00 01 01 01 01 01 01 01 01 01 01
Figure 4.7: The shaded areas of SCREEN RAM after they have been written to by DrawStripesBehindTitle
and DrawTitleScreenText.
Now that we've looped through all 40 columns we have both SCREEN RAM and COLOR RAM
fully prepared for painting by the raster. As we watch the screen getting painted in
the next section we'll see the following picture we've prepared gradually appear - with
the sprites painted on top of course. The magic of adding the sprites to this picture,
and animating them while we're at it, is what we will unpick as follow the raster on its
journey to the bottom of the screen in the next few milliseconds.
59
CHAPTER 4. THE FIRST 16 MILLISECONDS
Figure 4.8: The screen as it would appear after DrawStripesBehindTitle and DrawTitleScreenText have
run. The added grid helps compare with our previous \=gures for SCREEN RAM and COLOR RAM.
Now we're ready to receive our i\=rst beam. You may remember we set this to happen
when the raster reached line 16:
And that the routine we'll run when that happens is TitleScreenInterruptHandler
(which itself will pass the work onto TitleScreenAnimation:
60
CHAPTER 4. THE FIRST 16 MILLISECONDS
The painting of sprites and playing of music as the screen gets painted is all handled
by TitleScreenAnimation. This routine works by calling one of three different sub-
routines each time its called. It picks the one to run depending on some internal state
it maintains, all with a view to ensuring that the sprites spelling out the game's title and
the sprites depicting the animated gilbies are updated and in place before the raster
reaches them.
To ensure it gets called by the interrupt when its needed it will repeatedly update the line
that the next interrupt should happen. We'll trace this as it actually happens, interrupt
by interrupt, and sift through what the routine does at each step during the raster's \=rst
pass at painting the entire screen.
The \=rst time the raster is called, this is what the screen looks like:
Figure 4.9: The state of the screen the \=rst time the raster interrupt is received at line 16.
Of course we never actually see the screen in this state because it only appears for a
microsecond or two, much too fast for us to observe. But as you can see the painting
has aleady started. Everything above line 16 indicated in the \=gure has been painted
black, as per our preparation of SCREEN RAM a little earlier.
What our diagram above also tells us is that in this visit from the beam TitleScreenAnimation
chose to execute the sub-routine DoStarfieldAnimation and that it took 127 CPU cy-
61
CHAPTER 4. THE FIRST 16 MILLISECONDS
cles to complete it. Since it takes the raster 63 cycles to do an entire line this means
that by the time we've \=nished this piece of work, the raster will have moved on to the
next line - which is why the diagram shows 17 rather than 16. Every time we get an
interrupt, the raster doesn't wait for us. We have to work quickly, especially if we're
preparing graphics on lines that its likely to reach soon. This is why each of these
subroutines does as little as it can get away with to get the job done.
The three sub-routines the work at each raster interrupt can get divvied out to are
UpdateJumpingGilbyPositionsAndColors, DoStarfieldAnimation, and a cluster of
routines starting with UpdateTitleScreenSpriteColors. The one that's called the
most often is DoStarfieldAnimation as it is responsible for sprinkling the screen with
animated stars traversing it left to right.
; -------------------------------------------------------
; TitleScreenAnimation
; This handles all the activity in the title screen and is called
; roughly 60 times a second by the Raster Interrupt .
; -------------------------------------------------------
TitleScreenAnimation
LDY t i t l e S c r e e n S t a r F i e l d A n i m a t i o n C o u n t e r
CPY \# $0C
BNE M a y b e D o S t a r F i e l d O r T i t l e T e x t
JSR U p d a t e J u m p i n g G i l b y P o s i t i o n s A n d C o l o r s
LDY \# $10
STY t i t l e S c r e e n S t a r F i e l d A n i m a t i o n C o u n t e r
MaybeDoStarFieldOrTitleText
LDA t i t l e S c r e e n S t a r F i e l d Y P o s A r r a y ,Y
BNE D o S t a r f i e l d A n i m a t i o n
PaintTitleTextSprites
JSR T i t l e S c r e e n M u t a t e S t a r f i e l d A n i m a t i o n D a t a
LDA \# $00
STA t i t l e S c r e e n S t a r F i e l d A n i m a t i o n C o u n t e r
LDA \# $10
STA $D012 ; Raster Position
JSR UpdateTitleTextSprites
JSR MaybeUpdateSpriteColors
JSR RecalculateJumpingGilbyPositions
JSR PlayTitleScreenMusic
JMP ReEnterInterrupt
62
CHAPTER 4. THE FIRST 16 MILLISECONDS
The internal accounting responsible for choosing the routine to run is tricky to decipher
by just looking at the code. So instead let's follow what actually happens in practice.
If we roll ahead to the next interrupt we can already see something happening:
If you look closely, you can see a yellow star painted over the \=rst band of red stripes.
This is our \=rst sprite. You may be wondering: what about the title sprites? Shouldn't
they be there by now? The answer is no: we will paint them when the raster reaches
the end of the screen. When it goes to paint the screen a second time (the second 16
milliseconds) they will be ready for painting. We'll see this in action a little later.
So let's see what DoStarfieldAnimation did to get this sprite ready for painting.
MaybeDoStarFieldOrTitleText
LDA t i t l e S c r e e n S t a r F i e l d Y P o s A r r a y ,Y
BNE D o S t a r f i e l d A n i m a t i o n
Since Y is zero at this point this means it referenced the \=rst value in the array, which
is $48:
63
CHAPTER 4. THE FIRST 16 MILLISECONDS
So when DoStarfieldAnimation is called the \=rst thing it does is set the y position of
the star to paint to $48 (72 in decimal):
DoStarfieldAnimation
; A was loaded from t i t l e S c r e e n S t a r F i e l d Y P o s A r r a y
; by the caller.
STA $D00F ; Sprite 7 Y Pos
You can also see it then sets the X position of the star using values plucked from
titleScreenStarFieldXPosArray. So we're leaning heavily on these two arrays to
decide where to place stars. But so far, so simple. We've placed the star on the screen
more or less and when the raster reaches line 72 it will paint it. There's an additional
complication to specifying the X coordinate of the star though and we can't really gloss
over it here. We'll also encounter this wrinkle elsewhere too so it's worth pausing on
for a moment.
The next few lines of the routine do quite a bit of convoluted work to handle something
called the spriteMSBXPosOffset of the star. This is our complication.
4.3.1 A Complication
BEQ S t a r F i e l d S k i p M S B
LDA \# $80
STA s p r i t e M S B X P o s O f f s e t
StarFieldSkipMSB
LDA $D010 ; Sprites 0 -7 MSB of X coordinate
AND \# $7F
ORA s p r i t e M S B X P o s O f f s e t
64
CHAPTER 4. THE FIRST 16 MILLISECONDS
If you look at the diagram again you may recall we said the screen we're painting is 504
pixels wide. Fortunately the only part we can paint is the section in the center that is
320 pixels wide and 200 pixels high.
Figure 4.11: The different parts of the screen, we can only paint the bit in the middle. (Source:
dustlayer.com)
200 is a value that can be expressed with a single byte. However, 320 is not. A byte
can only store a number up to 255 so if we want to specify an X co-ordinate greater
than 255 a single byte will not do. The way the C64 works around this is by making a
single extra bit for our sprite's X co-ordinate available that brings the available values
up from 256 (0 - 255) to 512. Since there are 8 sprites in total we need 8 extra bits to
cover this requirement for all of them. For this purpose we use a single byte at address
$D010 that contains the extra bit for all 8 sprites. We refer to this bit as the MSB for the
X co-ordinate because it is the 'Most Signi\=cant Bit', i.e. the left most bit, in the 9-bit
number that we store the X co-ordinate in. That's to say our x co-ordinate is given by
combining the value between 0 and 255 we store in our 8-bit byte for 'Sprite 7' in $D00E
and the extra bit we store in $D010.
65
CHAPTER 4. THE FIRST 16 MILLISECONDS
Since we are using 'Sprite 7' for painting the star\=eld the bit we are interested in is
bit 7. The way we're going to manage this value for the star\=eld is by keeping array
titleScreenStarfieldMSBXPosArray that indicates whether the x co-ordinate for the
current index is greater than 255. If the value in there indicates that it is we'll set our
bit in $D010 to 1.
LDA t i t l e S c r e e n S t a r f i e l d M S B X P o s A r r a y + $01 , Y
AND \#$01
STA spriteMSBXPosOffset
.. we set spriteMSBXPosOffset to indicate that that's the case. That's all this step,
with the help of the AND \#$01 statement is doing. If 'Bit 1' is set in the value we pluck
from titleScreenStarfieldMSBXPosArray it is just an indicator that the x co-ordinate
for this star is greater than 256. So if we see a value of $02 in there our operation AND
\#$01 will give us a zero result, meaning the intended value of the x co-ordinate is not
greater than 256, othwerise it will give us a non-zero result indicating that it is:
AND'ing $02 and $01 gives $00 (0). For AND to give a 1 both bits must 1 or both must be 0.
This zero result allows BEQ StarFieldSkipMSB to evaluate as True so we skip ahead
to StarFieldSkipMSB to set $D010. It means for us that the value of the x co-ordinate
is not going to be greater than 255. If this is not the case, we instead load a value of
$80 to spriteMSBXPosOffset to overwrite the 00 there. This will indicate that the value
of the x co-ordinate is greater than 255.
BEQ StarFieldSkipMSB
66
CHAPTER 4. THE FIRST 16 MILLISECONDS
LDA \#$80
STA spriteMSBXPosOffset
StarFieldSkipMSB
LDA $D010 ; S p r i t e s 0−7 MSB of X c o o r d i n a t e
AND \#$7F
ORA spriteMSBXPosOffset
STA $D010 ; S p r i t e s 0−7 MSB of X c o o r d i n a t e
The remaining step above is to load the value we've arrived at in spriteMSBXPosOffset
to $D010. Since we want to do this without affecting any of the other bits in there that
have been set for the other sprites we can't just do a LDA/STA as that will overwrite
what's already there. The combination of the AND/OR operations here accomplishes
something quite nifty - it allows us to update just the bit (Bit 7) that interests us in
$D010.
If we suppose the current value in $D010 is $F3, our AND \#$7F operation clears 'Bit 7'
so that it is always set to zero:
AND'ing $F3 and $00 gives $73. It clears 'Bit 7' for us of whatever value was there originally.
Now when we perform an 'or' operation on the result with ORA spriteMSBXPosOffset it
will have the effect of just setting 'Bit 7' with the value we've stored in spriteMSBXPosOffset.
In this case it remains at zero because that's what we have in spriteMSBXPosOffset
but if we have $80 in there it would set it to 1:
Now that we've \=xed the star's co-ordinates there's just two main things left to do be-
fore we're done with handling this raster interrupt. One is to set the color of the star.
67
CHAPTER 4. THE FIRST 16 MILLISECONDS
We do this using a look-up array where we get the color for the star per our current
index and set it:
LDA t i t l e S c r e e n S t a r F i e l d C o l o r s A r r a y L o o k U p ,Y
TAX
LDA t i t l e S c r e e n C o l o r s A r r a y - $01 , X
STA $D02E ; Sprite 7 Color
The second, and most important, is that we update the position on the screen that
we want the next interrupt to happen. Remember for this visit we were interrupted at
line 17. We want to place stars on other lines so we keep a list of the lines we want
to write stars on in titleScreenStarFieldYPosArray, i.e. an array that stores the y
co-ordinates of our stars. What we do is simply update the Raster Interrupt with the
next position from this array so that we get called when the raster reaches it:
In this instance we're setting the value to $48 (72). So when the next interrupt happens
it will reveal the star we just prepared. THis is the one we took a peek at in Figure 1.10.
The next ten interrupts will continue to revisit DoStarfieldAnimation. Let's look at the
screen as it unfolds through each of these interrupts:
68
CHAPTER 4. THE FIRST 16 MILLISECONDS
Figure 4.12: The next ten interrupts paint the star\=eld on the screen until we reach the point at which we
want to prepare the gilby sprites.
69
CHAPTER 4. THE FIRST 16 MILLISECONDS
Figure 4.13: The point we reach in the screen paint when we decide to prepare the gilby sprites.
Finally we've reached a point in the screen where we're not just going to add another
star to the background. We've been keeping a count of the number of interrupts we've
handled in titleScreenStarFieldAnimationCounter. When it reaches $0C (12) we've
handled the raster interrupt twelve times and painted nothing but the text background
we prepared earlier and the stars we've added along the way. Now's the time to do
something else:
TitleScreenAnimation
LDY t i t l e S c r e e n S t a r F i e l d A n i m a t i o n C o u n t e r
CPY \# $0C
BNE M a y b e D o S t a r F i e l d O r T i t l e T e x t
JSR U p d a t e J u m p i n g G i l b y P o s i t i o n s A n d C o l o r s
LDY \# $10
STY t i t l e S c r e e n S t a r F i e l d A n i m a t i o n C o u n t e r
70
CHAPTER 4. THE FIRST 16 MILLISECONDS
raster's journey - because the raster hasn't reached that position in the screen yet (but
will reach it shortly) so now is our opportunity to position the gilbies where we want
them.
The next time around we will pick up the positions as re-calculated by Recalculate-
JumpingGilbyPositions. So the positioning of the gilbies and the calculation of the
updated positions happen separately. The reason for that approach is simple: there
isn't enough time right now to do anything but simply update the positions the gilbies
are displayed at. Later on, when the raster has passed line 320 we will have a lot more
time available to perform complex calculations because we don't need to worry about
the raster painting anything on the screen for a while.
Since there are 7 of them, setting the x and y co-ordinates of the seven gilby sprites is
handled by a loop:
SkipGilbyMSBXPos
LDA $D010 ; Sprites 0 -7 MSB of X coordinate
AND t i t l e S c r e e n G i l b i e s M S B X P o s O f f s e t ,X
STA $D010 ; Sprites 0 -7 MSB of X coordinate
UpdateYPosJumpingGilbies
LDA t i t l e S c r e e n G i l b i e s Y P o s A R r a y ,X
71
CHAPTER 4. THE FIRST 16 MILLISECONDS
LDA c u r r e n t T i t l e S c r e e n G i l b y S p r i t e V a l u e
STA Sprite0Ptr ,X
INX
CPX \# $07
BNE U p d a t e J u m p i n g G i l b i e s L o o p
RTS
We can see in here the verbosity required to handle the most signi\=cant bit of the
sprite's x co-ordinate. Just as with the star\=eld we need a separate array (titleScreen-
GilbiesMSBXPosArray to handle this in addition to arrays to manage the basic x/y
positions themselves.
With the gilbies prepared we fall through and update the star\=eld again. Once that's
done we're \=nished handling the current raster interrupt. This is followed by another
dozen or so interrupts where we again just prepare stars for display and as the raster
progress our gilbies are revealed.
72
CHAPTER 4. THE FIRST 16 MILLISECONDS
73
CHAPTER 4. THE FIRST 16 MILLISECONDS
Figure 4.15: We've \=nally reached the bottom of the screen, with gilbies and stars painted, but still no title.
We've \=nally reached the bottom of the screen in our \=rst raster paint, at least the
bottom of the portion of the screen that we can paint. When the raster hits line 270
we're beyond the point that we can place anything on the screen and into the border
area. This gives us time to do some more complicated and time consuming stuff.
LDA \# $10
STA $D012 ; Raster Position
74
CHAPTER 4. THE FIRST 16 MILLISECONDS
JSR R e c a l c u l a t e J u m p i n g G i l b y P o s i t i o n s
JSR P l a y T i t l e S c r e e n M u s i c
JMP R e E n t e r I n t e r r u p t
First of all we set the raster interrupt to line 16 at the top of the screen again, then we
acknowledge the interrupt. The raster will continue its journey but because we're going
to do this while it works its way the next 42 lines at the bottom of the screen we have
more time than at any point previously to get things done.
Adding the title sprites is relatively light work. Just as with the gilbies we use a tight
loop to paint each of them on the screen. THere's no animation to handle here.
PaintSpriteLettersLoop
; Assign the sprite.
LDA t i t l e T e x t S p r i t e A r r a y ,X
STA Sprite0Ptr - $01 , X
The other complex thing we do is calculate the next step in the jumping gilby anima-
tions. RecalculateJumpingGilbyPositions updates the x and y positions of the gilby
sprites in titleScreenGilbiesYPosArray and titleScreenGilbiesXPosArray.
Finally we play a single note from the title music, we cover this in detail in a later chap-
ter.
75
CHAPTER 4. THE FIRST 16 MILLISECONDS
The raster continues on its journey and progresses through its second paint journey of
the screen. The title sprites are \=nally revealed.
And with that the title sequence is \=nally up and running after 20 milliseconds or so.
76
CHAPTER 4. THE FIRST 16 MILLISECONDS
77
Making Planets for Nigel
Jeffrey Says
Redid the graphics completely, came up with some really nice looking metal-
lic planet structures that I'll probably stick with. Started to write the GenPlan
routine that'll generate random planets at will. Good to have a C64 that can
generate planets in its spare time. Wrote pulsation routines for the colours;
looks well good with some of the planet structures. The metallic look seems to be 'in' at
the moment so this \=rst planet will go down well. There will be \=ve planet surface types
in all, I reckon, probably do one with grass and sea a bit like 'Sheep in Space', cos I did like
that one. It'll be nice to have completely different planet surfaces in top and bottom of the
screen. The neat thing is that all the surfaces have the same basic structures, all I do is \=t
different graphics around each one.
When making a planet, ensure you perform each of the following simple steps in the
order given below.
78
CHAPTER 5. MAKING PLANETS FOR NIGEL
Figure 5.1: Step One: Add the sea across the entire surface of the planet.
Figure 5.2: Step Two: Insert a land mass at least 32 bytes and at most 128 bytes long.
79
CHAPTER 5. MAKING PLANETS FOR NIGEL
Figure 5.4: Step Four: Add warp gates at the beginning and end of the planet surface.
Now you have not just a layout for one planet, but a layout for all \=ve.
80
CHAPTER 5. MAKING PLANETS FOR NIGEL
Figure 5.5: A layout that will suit all the planets in your life.
But making planets isn't all simple steps and big picture decisions. There are also
trifling details for the little people to wrestle with.
Making a sea is very easy. You come up with a character than can be repeated 1024
times to \=ll the surface of the planet.
Figure 5.6: There are two characters used for creating the sea and they're both the same! This will make
more sense when we look at the land, where they are different.
81
CHAPTER 5. MAKING PLANETS FOR NIGEL
The bit that needs explaining is how you de\=ne the character. If it was a simple bitmap
then we could imagine the character as 8 rows of 8 bits and where a bit is set to 1 you
color that pixel in. That is not the case. You can see how the bits are actually set below:
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0
0 0 0 0 0 0 1 0
1 0 0 0 1 0 1 0
1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0
Look closely at the picture above and you should see how it works. What is happening
is that we \=ll two adjacent cells with blue when together they form the value 10. So we
create graphic characters not with a simple bit-map but with a map of bit pairs. Each
pair of bits is treated as a unit giving us four units on each row. Maybe it's intuitively
obvious that 00 means 'blank' or 'background' but I've pointed that out to you now just
in case.
planet1Charset
.BYTE $00 , $00 , $20 , $02 , $8A , $AA , $AA , $AA ; .BYTE $00 , $00 , $20 , $02 , $8A , $AA , $AA , $AA
; CHARACTER $40
; 00000000
; 00000000
; 00100000 *
; 00000010 *
; 10001010 * * *
; 10101010 * * * *
; 10101010 * * * *
; 10101010 * * * *
Listing 5.1: Character $40 representing the sea as it is de\=ned in the source code. A full eight bytes are
required to de\=ne each character so not cheap.
Is that all there is to it? No. Before we look at how me might color things other than
blue, let's look at how we color them with the big blue brush we have so far. The \=rst
thing we do is clear down the entire surface of the planet:
82
CHAPTER 5. MAKING PLANETS FOR NIGEL
Listing 5.2: The surface data is stored from $8000 to $8FFF. This code overwrites it all with the value $60
which is an empty bitmap.
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 ; .BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
; CHARACTER $60
; 00000000
; 00000000
; 00000000
; 00000000
; 00000000
; 00000000
; 00000000
; 00000000
Listing 5.3: The empty character bit map (all zeroes) used to overwrite the surface before populating it.
With the planet surface cleared out (overwritten with all $60s) we can now.. overwrite
it all again with sequences of $40,$42. No, that's not right. We're only overwriting the
bottom layer - the surface layer - this time. This is the layer that contains the land
and/or sea and it lives between $8C00 and $8FFF which if your hexadecimal arithmetic
is better than mine you will realize is 1024 bytes ($400 in hex).
; Fill $8C00 to $8FFF with a $40 , $42 pattern . These are the
; character values that represent ' sea ' on the planet .
LDA \# $8C
STA p l a n e t S u r f a c e D a t a P t r H i
WriteSeaLoop
LDA \# $40
STA ( p l a n e t S u r f a c e D a t a P t r L o ) ,Y
LDA \# $42
INY
STA ( p l a n e t S u r f a c e D a t a P t r L o ) ,Y
DEY
; Move the pointers forward by 2 bytes
LDA p l a n e t S u r f a c e D a t a P t r L o
CLC
ADC \# $02
STA p l a n e t S u r f a c e D a t a P t r L o
83
CHAPTER 5. MAKING PLANETS FOR NIGEL
LDA p l a n e t S u r f a c e D a t a P t r H i
ADC \# $00
STA p l a n e t S u r f a c e D a t a P t r H i
; Loop until $8FFF
CMP \# $90
BNE WriteSeaLoop
Listing 5.4: Filling the entire bottom surface of the planet with $40,$42 which gives us the sea. Our next
step is to overwrite some of this with land.
There are other possible values aside from 10 and 00 that we could use to paint colors.
We could also have 11 and 01. This is useful since we want to color things in with more
than one color. We have blue assigned to 10 on Planet 1, while for the land we can use
two other colors: 11 which we will assign 'green' and 01 which we will assign 'brown'.
We can assign whatever colors we like but we can only choose three, not counting the
background. This is the kind of limitation you run into when you only allow two bits for
assigning possible colors.
1 1 0 0 1 1 0 0 1 1 0 0 0 0 1 1
1 1 0 0 1 1 1 1 1 1 1 1 0 0 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 0 1 1 1 0 1 0 1 1 1 1 1 0 1
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
Figure 5.10: Planet 1 Land uses two different characters that alternate to generate the land surface.
84
CHAPTER 5. MAKING PLANETS FOR NIGEL
The location and length of the landmass is randomly generated with a couple of con-
straints: it must be at least 128 bytes and not more than 256 bytes from the start of
surface and it must be at least 32 bytes and not more than 150 bytes long. The result
is that the planet surface will be mostly sea since the entire surface is 1024 bytes long.
Picking a random number between 128 and 256 is slightly convoluted in assembly:
This little snippet's job is to return a quasi-random byte for use in the planet
generation routines. To achieve this, it does something quite \=endish that is
more or less unhead of in modern programming: it mutates itself.
PutProceduralByteInAccumulatorRegister
r a n d o m I n t T o I n c r e m e n t =*+ $01
LDA ra nd om Pl an et Da t a
INC r a n d o m I n t T o I n c r e m e n t
RTS
When called for the \=rst time it loads a value from the address at randomPlanetData to the
accumulator. On \=rst run randomPlanetData points to the address $9ABB which contains
the value $42:
r a n d o m P la ne tD ata
.BYTE $42 , $E4 , $3F , $94 , $4E , $29 , $B0 , $59
.BYTE $2C , $FE , $7F , $B2 , $40 , $9B , $63 , $2B
Before returning this value as its result it alters itself by changing randomPlanetData to
reference $9ABC (INC randomIntToIncrement). In other words, it increments the pointer.
In the assembly listing we make randomIntToIncrement reference the position that holds
randomPlanetData by positioning it one byte before and adding a 1 to shift is reference
beyond the byte holding LDA to randomPlanetData.
85
CHAPTER 5. MAKING PLANETS FOR NIGEL
Every time the routine is called it increments the reference again so that the next time it
will pick up whatever lies in the bytes beyond 9ABB. The results it returns are never truly
random, but random enough to permit the procedural generation of planets that they're
used for.
Since the random number we get can be anything between $00 - $FF (i.e. 0 and 255)
and we want a number that's between 0 and 128 we need to do a bitwise AND to mask
out Bit 7 which by itself is 128.
With the position and length selected we can start laying turf. We don't just plop down
our basic land tiles. Posh and proper means giving the shore of the land its own look
and feel. This we have in the characters $5C and $5E in our character set:
Figure 5.12: Character tiles for the left shore ($5C,$5E) and the right shore ($5D,$5F).
86
CHAPTER 5. MAKING PLANETS FOR NIGEL
INC charSetDataPtrLo
b7420 JSR StoreRandomPositionInPlanetInPlanetPtr
LDY \# $00
LDA \# $41
STA ( planetPtrLo ) ,Y
LDA \# $43
INY
STA ( planetPtrLo ) ,Y
DEC planetSurfaceDataPtrLo
BNE DrawLandMassLoop
Listing 5.9: Write pairs of $41,$43 for the main land mass.
The routines for adding structures to the planet are the opportunity to observe some
assembly language cleverness. For each structure we draw we have to decide two
things: where to drop it on the surface and what type of structure to draw. Apart from
the Warp Gates, there are four structure types available.
87
CHAPTER 5. MAKING PLANETS FOR NIGEL
planet1Charset planet1Charset
littleStructureData mediumStructureData
planet1Charset planet1Charset
nextLargestStructure largestStructureData
planet2Charset planet2Charset
littleStructureData mediumStructureData planet2Charset planet2Charset
nextLargestStructure largestStructureData
You may be getting the sense that there is a sort of economy at work here. The struc-
tures are effectively the same for each planet, but with the textures swapped out. Your
intuition is correct, the structures are only de\=ned once and the same de\=nition does
regardless of which planet we're painting:
Listing 5.11: The de\=nitions of three of the structures above each of which serves all \=ve planets.
The $FF at the end of each line serves as a sentinel for the drawing routine to know
that the subsequent bytes are for the next layer 'up'. The $FE is a terminator, indicating
there is no more data for the structure.
Drawing a structure is relatively straightforward so we'll cover that briefly \=rst. Drawing
88
CHAPTER 5. MAKING PLANETS FOR NIGEL
the littlest structure provides the most compact example of the technique:
DrawLittleStructure
; Start iterating at 0 .
LDX \# $00
DrawLSLoop
; Get the byte in l i t t l e S t r u c t u r e D a t a pointed to
; by X.
LDA l i t t l e S t r u c t u r e D a t a ,X
; If we reached the ' end of layer ' sentinel , move
; our pointer planetPtrHi to the next layer. The
; BNE ' stays on the same layer ' by jumping to
; L S \. S t a y o n S a m e L a y e r if the current byte
; is not $FF.
CMP \# $FF
BNE L S \. S t a y o n S a m e L a y e r
; Switch to the next layer.
JSR S w i t c h T o N e x t L a y e r I n P l a n e t
; S w i t c h T o N e x t L a y e r I n P l a n e t incremented X for us
; so continue looping .
JMP DrawLSLoop
L S \. S t a y o n S a m e L a y e r
CMP \# $FE
; If we read in an $FE , we ' re done drawing .
BEQ R e t u r n F r o m D r a w i n g S t r u c t u r e
STA ( planetPtrLo ) ,Y
; Increment Y to the next position to write to.
INY
; Increment X to get the next byte to read in.
INX
; Continue looping .
JMP DrawLSLoop
Given that we're only writing 4 bytes this is a lot of code. As we will see there are sep-
arate routines for each of the structures and unfortunately for our search for evidence
of coding genius they're all identical. So this is a pretty open-and-shut case of code
duplication. It would have been more compact to rationalize them down to a single
function and use a pointer to the structure data instead of repeating almost verbatim
the same assembly code for each structure.
; -------------------------------------------------------
; D r a w M e d i u m S t r u c t u r e ( $74B1 )
; -------------------------------------------------------
DrawMediumStructure
LDX \# $00
DrawMSLoop
LDA m e d i u m S t r u c t u r e D a t a ,X
CMP \# $FF
89
CHAPTER 5. MAKING PLANETS FOR NIGEL
BNE b74C0
JSR S w i t c h T o N e x t L a y e r I n P l a n e t
JMP DrawMSLoop
; -------------------------------------------------------
; D r a w L a r g e s t S t r u c t u r e ( $74CB )
; -------------------------------------------------------
DrawLargestStructure
LDX \# $00
DrawLargeStructureLoop
LDA l a r g e s t S t r u c t u r e D a t a ,X
CMP \# $FF
BNE b74DA
JSR S w i t c h T o N e x t L a y e r I n P l a n e t
JMP D r a w L a r g e S t r u c t u r e L o o p
Listing 5.13: DrawMediumStructure and DrawLargestStructure are identical to each other and to
DrawLittleStructure and DrawNextLargestStructure.
The cleverness comes a little earlier so let's console ourselves with that. When we've
chosen a position to draw our structure we need to pick a type of structure at random.
The secret to this is to store the addresses to our regrettably repetitive draw routines
in a pair of arrays.
90
CHAPTER 5. MAKING PLANETS FOR NIGEL
Listing 5.14: A 'jump table' containing the addresses to our draw routines. The address for
DrawLittleStructure[escapechar=\%][escapechar
With this in place our routine consists of getting a random number between 0 and 3,
then using that as in index to pick out a value at the same position from structureSub-
RoutineArrayLoPtr and structureSubRoutineArrayHiPtr. We then store those val-
ues in structureRoutineLoPtr and structureRoutineHiPtr respectively. We now
have a pointer to one of our draw routines at structureRoutineLoPtr which we can
jump to with the simple command: JMP (structureRoutineLoPtr).
; ---------------------------------------------------------------
; DrawRandomlyChosenStructure
; ---------------------------------------------------------------
DrawRandomlyChosenStructure
; Pick a random positio to draw the structure
JSR S t o r e R a n d o m P o s i t i o n I n P l a n e t I n P l a n e t P t r
Listing 5.15: DrawRandomlyChosenStructure picks a random position and a random draw routine to use at
that position.
Rinse and repeat this for the length of the map and we get a surface with sea and land
that is dotted with structures of different types.
91
CHAPTER 5. MAKING PLANETS FOR NIGEL
planet1Charset warpGateData planet2Charset warpGateData planet3Charset warpGateData planet4Charset warpGateData planet5Charset warpGateData
There's something funny here I haven't \=gured out yet. The routine for drawing the warp
gate draws it twice. Yet each level has only one warp gate. Each one gets an initial posi-
tion of $F1 and $05 respectively. This is used by StoreRandomPositionInPlanetInPlanetPtr
to point to a position on the surface where the warp gate is drawn.
DrawWarpGates
LDA c h a r S e t D a t a P t r L o
BEQ G e n e r a t e S t r u c t u r e s L o o p
92
CHAPTER 5. MAKING PLANETS FOR NIGEL
STA c h a r S e t D a t a P t r H i
JSR S t o r e R a n d o m P o s i t i o n I n P l a n e t I n P l a n e t P t r
JSR DrawWarpGate
DEC c h a r S e t D a t a P t r L o
JSR S t o r e R a n d o m P o s i t i o n I n P l a n e t I n P l a n e t P t r
JSR DrawWarpGate
Listing 5.16: Why does it draw 2 warp gates when there's only 1? Haven't \=gured this out yet..
When the lower planet is inactive a surface with land, sea, and a warp gate is displayed.
This doesn't reuse any of the logic described above. Instead it is generated from some
customized data in the routine DrawLowerPlanetWhileInactive.
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; DrawLowerPlanetWhileInactive
; Draws the lower p l a n e t f o r the e a r l y l e v e l s when i t i s n ' t
; active yet.
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
DrawLowerPlanetWhileInactive
LDA l o w e r P l a n e t A c t i v a t e d
BEQ b6047
LDX \#$28
DrawLowerTextLoop
LDA t e x t F o r I n a c t i v e L o w e r P l a n e t − $01 , X
AND \#$3F
STA SCREEN RAM + LINE18 COL39 , X
LDA \#WHITE
STA COLOR RAM + LINE18 COL39 , X
DEX
BNE DrawLowerTextLoop
LDX \#$28
Dra wInac tive Surfa ceLoo p
LDA s u r f a c e D a t a I n a c t i v e L o w e r P l a n e t , X
CLC
93
CHAPTER 5. MAKING PLANETS FOR NIGEL
ADC \#$40
STA SCREEN RAM + LINE14 COL39 , X
DEX
BNE Draw Inact iveS urfac eLoop
LDX \#$10
DrawWarpGateInactive
LDY x P o s S e c o n d L e v e l S u r f a c e I n a c t i v e P l a n e t , X
LDA s e c o n d L e v e l S u r f a c e D a t a I n a c t i v e P l a n e t , X
CLC
ADC \#$40
STA SCREEN RAM + LINE12 COL4 , Y
DEX
BNE DrawWarpGateInactive
RTS
textForInactiveLowerPlanet
.TEXT "" WARP GATE GILBY CORE NOT−CORE""
Drawing the upper planet is all very well. The data is just there and as we have seen it's
just a question of 'writing it to the screen'. For the lower planet, which is just an upside
down version of the upper one, we could store the data all over again, but inverted. Or
we could try something a little more clever, so let's be clever.
Our clever trick will be to take each character de\=nition for the upper planet and shift
its bits around so that we can turn it into an upside down version of the original. This
avoids the need to store everything twice. Instead we store it once for the upper planet
and just mutate it for the lower planet.
94
CHAPTER 5. MAKING PLANETS FOR NIGEL
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 1 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 0
1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0
Figure 5.18: The character set de\=nition for a piece of land and it's topsy-turvy counterpart.
Doing this all at once for the full planet would be expensive and there will never be a
good time to do it. So what we'll do instead is make a chutney from this pickle and
do the heavy lifting while distracting the player's attention with an impressive-looking
level-entry sequence.
When the player enters a pair of planets for the \=rst time we'll slowly materialize the
planet surfaces while quietly loading the upper planet data and converting it for the
lower planet too. If we make this entry sequence long enough we'll have time to pop-
ulate all 256 bytes of each planet's surface.
95
CHAPTER 5. MAKING PLANETS FOR NIGEL
; -------------------------------------------------------
; PerformMainGameUpdate
; -------------------------------------------------------
PerformMainGameUpdate
LDX c u r r e n t P l a n e t B a c k g r o u n d C l r 1
LDA b a c k g r o u n d C o l o r s F o r P l a n e t s ,X
STA $D022 ; Background Color 1 , Multi - Color Register 0
LDX c u r r e n t P l a n e t B a c k g r o u n d C l r 2
96
CHAPTER 5. MAKING PLANETS FOR NIGEL
LDA b a c k g r o u n d C o l o r s F o r P l a n e t s ,X
STA $D023 ; Background Color 2 , Multi - Color Register 1
JSR C h e c k K e y b o a r d I n G a m e
JSR S c r o l l S t a r f i e l d A n d T h e n P l a n e t s
JSR A n i m a t e G i l b y S p r i t e M o v e m e n t
JSR P e r f o r m M a i n G a m e P r o c e s s i n g
JSR C h e c k F o r L a n d s c a p e C o l l i s i o n A n d W a r p T h e n P r o c e s s J o y s t i c k I n p u t
JSR P e r f o r m G i l b y L a n d i n g O r J u m p i n g A n i m a t i o n
JSR A l s o P e r f o r m G i l b y L a n d i n g O r J u m p i n g A n i m a t i o n
JSR M a y b e D r a w L e v e l E n t r y S e q u e n c e
JSR P l a y S o u n d E f f e c t s
JSR F l a s h B o r d e r A n d B a c k g r o u n d
JSR U p d a t e G i l b y P o s i t i o n A n d C o l o r
JSR U p d a t e A n d A n i m a t e A t t a c k S h i p s
JSR U p d a t e B u l l e t P o s i t i o n s
JSR D r a w U p p e r P l a n e t A t t a c k S h i p s
JSR U p d a t e C o n t r o l P a n e l C o l o r s
; Jump into KERNAL 's standard interrupt service routine to
; handle keyboard scan , cursor display etc.
JMP R e E n t e r I n t e r r u p t
; Returns From Interrupt
Listing 5.18: PerformMainGameUpdate the spaghetti junction handling nearly everything during main
\.
gameplay. We'll see more of this code section later in the bookDuring the entry level sequence it is
MaybeDrawLevelEntrySequence and PlaySoundEffects that do most of the work.
The routine maintains a counter from 0 to 255 and every time it is visited it increments
this counter and uses it to pick part of a tile in the charset to populate. The thing to
remember here is that it is not actually painting the planet itself - rather it is slowly
\=lling out the character set containing the tiles that de\=ne the textures on the planet.
The game already knows which tiles it wants to put where - it just doesn't yet know
what they look like. If we maintained separate copies of the upper planet and lower
planet tiles we could just give them to the routine and painting would be easy. But
because we don't have a copy of the lower planet tiles we have to generate them on
the fly and resort to this bit of entry level sequence trickery.
Since there are 8 bytes in each character set de\=nition, each represeting a line in the
tile, each visit to this routine updates just one line in one tile. There are 32 tiles in
total so 256 visits to the routine during the entry sequence will be enough to \=ll out the
97
CHAPTER 5. MAKING PLANETS FOR NIGEL
tileset completely. This allows us to \=ll out the tile set in a random-looking way and is
responsible for the materialization effect.
The \=rst part of the MaybeDrawLevelEntrySequence is the easy part. We just pick the
byte we're going to \=ll at this visit from a jumbled array of every value between 0 and
255 stored in sourceOfSeedBytes. This jumbled sequence is the source of the random-
looking nature of the materialization.
MaybeDrawLevelEntrySequence
LDA l e v e l E n t r y S e q u e n c e A c t i v e
BNE D r a w L e v e l E n t r y S e q u e n c e
ReturnFromEntrySequence
RTS
DrawLevelEntrySequence
LDX e n t r y L e v e l S e q u e n c e C o u n t e r
; 'Y ' becomes our ' random ' index into the
; character set definition picking one of its
; 256 bytes to populate a line in one of the tiles.
LDY s o u r c e O f S e e d B y t e s ,X
INC p l a n e t S u r f a c e D a t a P t r H i
; Likewise for the character set data used to
; define the heads up display.
LDA ( p l a n e t S u r f a c e D a t a P t r L o ) ,Y
STA u p p e r P l a n e t H U D C h a r s e t ,Y
JSR I n v e r t S u r f a c e D a t a F o r L o w e r P l a n e t
This all just copying and pasting so far. We're slowly populating the tiles for the current
upper planet without doing anything computationally intensive. The very last state-
ment is where start work on the lower planet, and that's where the sorcery starts. In
InvertSurfaceDataForLowerPlanet we repoint ourselves to the data for the lower
planet and use a routine called InvertCharacter to the do the actual turning of the
byte upside-down:
98
CHAPTER 5. MAKING PLANETS FOR NIGEL
InvertSurfaceDataForLowerPlanet
LDA c u r r e n t B o t t o m P l a n e t D a t a L o P t r
STA p l a n e t S u r f a c e D a t a P t r L o
LDA c u r r e n t B o t t o m P l a n e t D a t a H i P t r
STA p l a n e t S u r f a c e D a t a P t r H i
LDA i n v e r t e d C h a r T o D r a w
; Note that 'X ' was updated by I nv e rt Ch ar a ct er
STA l o w e r P l a n e t S u r f a c e C h a r s e t ,X
INC p l a n e t S u r f a c e D a t a P t r H i
RTS
The approach taken by InvertCharacter is two-fold: it has to reverse the chosen byte
left to right and it also it has to move the byte to its appropriate position in the 8 byte
sequence.
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 1 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 0
1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0
Figure 5.20: Inverting 00100000 means transforming it to 00001000 and moving it from position 3 in the 8
byte character set de\=nition to position 5.
99
CHAPTER 5. MAKING PLANETS FOR NIGEL
The \=rst of these requirements is the more elaborate of the two. We need to look at
each of the four bit-pairs in the byte and move it to its corresponding 'inverted' position
in the byte. So in a way we treat one half of they byte as a mirror of the other and move
the bit pair there as in the following examples.
1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0
Figure 5.21: Examples of the inversion operation.
The way to do through a combination of bit-shifting and masking. This is what the
code responsible looks like. We'll break it down.
I nv er tC h ar ac t er
LDA ( p l a n e t S u r f a c e D a t a P t r L o ) ,Y
PHA
AND \# $03
TAX
LDA b i t f i e l d 2 F o r I n v e r t i n g B y t e ,X
ORA invertedCharToDraw
STA invertedCharToDraw
PLA
ROR
ROR
AND \# $03
TAX
LDA b i t f i e l d 3 F o r I n v e r t i n g B y t e ,X
ORA invertedCharToDraw
STA invertedCharToDraw
LDA ( p l a n e t S u r f a c e D a t a P t r L o ) ,Y
100
CHAPTER 5. MAKING PLANETS FOR NIGEL
ROL
ROL
ROL
AND \# $03
ORA i n v e r t e d C h a r T o D r a w
STA i n v e r t e d C h a r T o D r a w
Our \=rst step is to take the byte we're interested in from the upper planet character set
de\=nition. Let's assume it's the one below.
0 0 0 0 1 0 0 0
We load this into the accumulator (A) so that we can get work on it. What we're go-
ing to do is shift each of the four bit pairs in turn into the rightmost position in the
byte and use that value (between 0 and 3) as in index into a trio of helper arrays
that give the 'mirrored' bit-pair for that particular position. These helper arrays are
bitfield[1-3]ForInvertingByte.
We don't have to do any shifting to compare the rightmost bit-pair. We can just AND it
with \#$03 and store the result in X:
Since the result is zero, we will then store the value at index zero in bitfield1ForInvertingByte
as our result in invertedCharToDraw:
101
CHAPTER 5. MAKING PLANETS FOR NIGEL
LDA b i t f i e l d 1 F o r I n v e r t i n g B y t e ,X
STA i n v e r t e d C h a r T o D r a w
Now we can move on to the second rightmost bitpair. The ones highlighted in blue
below:
0 0 0 0 1 0 0 0
We pushed our original value onto the stack using PHA. First we retrieve it using PLA
and then shift it two bits to the right:
PLA
ROR
ROR
When we've done this our byte looks like this, the bitpair we're interested has moved
to the rightmost position:
0 0 0 0 0 0 1 0
Now we can AND this byte with $03 again to ensure we only deal with the last two bits
(remember that although all the other bits are zero in the example, they may not be in
other cases).
AND \# $03
TAX
This result of $02 gives us an index into bitfield2ForInvertingByte which will give
102
CHAPTER 5. MAKING PLANETS FOR NIGEL
LDA b i t f i e l d 2 F o r I n v e r t i n g B y t e ,X
ORA i n v e r t e d C h a r T o D r a w
STA i n v e r t e d C h a r T o D r a w
As we can see, this has achieved an effective mirroring of our original value!
0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0
Figure 5.22: The result of our bit-shifting, AND'ing and ORing.
The remaining steps for the other two bit-pairs in the byte are similar. In the case of our
example they will have no effect as the result will be always be zero for them. We've
effectively mirrored our bitpair already.
The next and last thing we have to do with our mirrored byte is adjust it's position in the
8 byte sequence in the character de\=nition. In the example below we are dealing with
a byte on the left which occurs as the 5th position in the byte sequence and in order to
be inverted needs to be moved to the 4th position.
So whereas we were dealing with a left-right mirroring in the byte itself we are now
dealing with an up-down mirroring in the position of the byte in its sequence. The total
effect of our inversion operation is to flip the character set from left to righ and top to
bottom:
103
CHAPTER 5. MAKING PLANETS FOR NIGEL
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 1 0 0 0 0 0 1 0 1 0 1 0 1 0
0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 0
1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0
Figure 5.23
; Now that the byte has been inverted , invert the position in
; the 8 byte charset definition. For example , if the position is
; 0 then the inverted position is 7 .
; Mask out everything but the last 3 bits in the current upper
; planet position.
TYA
PHA
AND \# $07
TAY
PLA
PHA
AND \# $F8
STA c h a r S e t D a t a P t r L o
; Now add the inverted position to get the correct lower planet
; position in the 8 - byte charset definition. c h a r Se t D a t a P t r L o is
; just temporary storage here. We store the final value for use
; by the calling routine in 'X ' below.
LDA p o s i t i o n I n I n v e r t e d C h a r S e t ,Y
CLC
ADC c h a r S e t D a t a P t r L o
PLA
TAY
The key to this operation lies in the use of the array positionInInvertedCharSet. If
we know that the current byte is number 5 in the 8 byte sequence (i.e. the red row
104
CHAPTER 5. MAKING PLANETS FOR NIGEL
below):
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0
0 0 0 0 0 0 1 0
1 0 0 0 1 0 1 0
1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0
planet1Charset $40
Figure 5.24
then we can use that as an index into positionInInvertedCharSet to retreve the cor-
responding position in the inverted character set de\=nition. As we can see value of the
the 5th byte in positionInInvertedCharSet is $03:
positionInInvertedCharSet .BYTE $07 , $06 , $05 , $04 , $03 , $02 , $01 , $00
Which as we can see is the appropriate position for the byte in the inverted character
set (the 4th byte or red row starting from the top). (Remember that our index into arrays
starts at zero, so an index value of zero will pick the \=rst byte and of three, as in this
case, will pick the fourth.)
1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0
1 0 1 0 0 0 1 0
1 0 0 0 0 0 0 0
0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
Inverted $40
Figure 5.25
So where do we get the position of the current byte in the uninverted character set? This
was passed into InvertSurfaceDataForLowerPlanet and InvertCharacter in the Y
105
CHAPTER 5. MAKING PLANETS FOR NIGEL
register. It's a value between 0 and 256 which references the offset of the current byte
in the character set data as a whole so all we have to do is clamp it to a value between
0 and 7 to get the value we need within the 8-byte de\=nition for this speci\=c character.
; Mask out everything but the last 3 bits in the current upper
; planet position.
TYA
PHA
AND \# $07
The clamping is achieved by the AND statement. If we imagine that Y has a value of
$D4, we transfer it to the A register (TYA and then do an AND operation with $07:
This gives us $04 as a result, which as we saw returns $03 when used as an index into
positionInInvertedCharSet.
With our new position for the byte in the character set de\=nition established we can
store it to the X register and use that as the position in lowerPlanetSurfaceCharset
where we will store the invertedCharToDraw we calculated earlier. We do this when we
return from the InvertCharacter routine and are back in InvertSurfaceDataForLowerPlanet:
JSR I nv er t Ch ar ac t er
LDA i n v e r t e d C h a r T o D r a w
; Note that 'X ' was updated by I nv e rt Ch ar a ct er
STA l o w e r P l a n e t S u r f a c e C h a r s e t ,X
With that done, we've added another brick in the wall. Or more precisely, another byte in
the 256 that in total make up the tileset for the upper and lower planets respectively. As
the materialization sequence proceeds we slowly \=ll out the tileset byte-by-byte. After
a few seconds we've successfully cheated our way into generating the lower planet's
tileset on the fly into lowerPlanetSurfaceCharset and the game is ready to play.
106
CHAPTER 5. MAKING PLANETS FOR NIGEL
Figure 5.26: The planets when we start the materialization sequence, and when we end.
107
Blasting, Fast and Slow
Every game has a 'main loop'. A tight section of code which is executed multiple times
per second and which controls nearly all aspect of the gameplay. In Iridis Alpha this
boiler-room is PerformMainGameUpdate:
; -------------------------------------------------------
; PerformMainGameUpdate
; -------------------------------------------------------
PerformMainGameUpdate
LDX c u r r e n t P l a n e t B a c k g r o u n d C l r 1
LDA b a c k g r o u n d C o l o r s F o r P l a n e t s ,X
STA $D022 ; Background Color 1 , Multi - Color Register 0
LDX c u r r e n t P l a n e t B a c k g r o u n d C l r 2
LDA b a c k g r o u n d C o l o r s F o r P l a n e t s ,X
STA $D023 ; Background Color 2 , Multi - Color Register 1
JSR C h e c k K e y b o a r d I n G a m e
JSR S c r o l l S t a r f i e l d A n d T h e n P l a n e t s
JSR A n i m a t e G i l b y S p r i t e M o v e m e n t
JSR P e r f o r m M a i n G a m e P r o c e s s i n g
JSR C h e c k F o r L a n d s c a p e C o l l i s i o n A n d W a r p T h e n P r o c e s s J o y s t i c k I n p u t
JSR C a l c u l a t e G i l b y V e r t i c a l P o s i t i o n E a r t h B o u n d
JSR C a l c u l a t e G i l b y V e r t i c a l P o s i t i o n A i r b o r n e
JSR M a y b e D r a w L e v e l E n t r y S e q u e n c e
JSR P l a y S o u n d E f f e c t s
JSR F l a s h B o r d e r A n d B a c k g r o u n d
JSR U p d a t e G i l b y P o s i t i o n A n d C o l o r
JSR U p d a t e A n d A n i m a t e A t t a c k S h i p s
JSR U p d a t e B u l l e t P o s i t i o n s
JSR D r a w U p p e r P l a n e t A t t a c k S h i p s
JSR U p d a t e C o n t r o l P a n e l C o l o r s
; Jump into KERNAL 's standard interrupt service routine to
108
CHAPTER 6. BLASTING, FAST AND SLOW
Listing 6.1: PerformMainGameUpdate the spaghetti junction handling nearly everything during main
gameplay.
Just like in the title sequence this routine is called dozens of times as the beam
of light painting the screen travels from top to bottom up to 25 times per second.
PerformMainGameUpdate, along with a routine called AnimateStarFieldAndScrollPlanets
whose purpose you can hopefully guess from its name, are the two gears that grind
out the gameplay as long as the player is alive and blasting.
If you look closely you'll notice that our so-called boiler-room routine is called last,
when the raster is nearing the end of the screen. This makes sense as it has the
most to do and therefore we need to execute it at a point when most of the screen
has been painted and what to paint on the rest of it has already been prepared.
PerformMainGameUpdate primarily concerns itself with preparing the screen for the
next time it will be painted: updating the position of the enemies on the upper planet,
the position of the player's ship, playing sound effects, moving the bullets and so on.
109
CHAPTER 6. BLASTING, FAST AND SLOW
110
CHAPTER 6. BLASTING, FAST AND SLOW
cialized hardware, and preparing the position of the enemy sprites on the lower planet.
The only reason it is called more than once is because, like in the title screen routine,
the star\=eld is being painted using a single sprite (Sprite 7). Each time it runs it can
change the position of this sprite so that it appears at a new position as well as the
old one on which the raster has already painted it. We covered the mechanics of this
in detail when we dissected the title screen in 'The First 16 Milliseconds'.
We can get a better sense of how the labour is divided between the two routines if we
isolate the position of the raster when the position of the enemy sprites on each planet
is updated.
Figure 6.3: The rasterline position when the enemies on the upper and lower planets are updated.
The routines for updating the sprites on the upper and lower planets are identical. If
we were writing this game in any other language than assembly we would just have
one function and pass the different arrays for each planet in as parameters.
111
CHAPTER 6. BLASTING, FAST AND SLOW
DrawUpperPlanetAttackShips DrawLowerPlanetAttackShips
LDX \#$0C LDX \#$0C
LDY \#$06 LDY \#$06
UpperPlanetShipsLoop LowerPlanetShipsLoop
LDA u p p e r P l a n e t A t t a c k S h i p s X P o s A r r a y , Y LDA l o w e r P l a n e t A t t a c k S h i p s X P o s A r r a y + $01 , Y
STA $D000 , X ; S p r i t e 0 X Pos STA $D000 , X ; S p r i t e 0 X Pos
LDA att ac kSh ip sXP osA rr ay − $01 , Y LDA att ac kSh ip sXP osA rr ay − $01 , Y
AND $D010 ; S p r i t e s 0−7 MSB of X c o o r d i n a t e AND $D010 ; S p r i t e s 0−7 MSB of X c o o r d i n a t e
STA currentMSBXPosOffset STA currentMSBXPosOffset
LDX u p p e r P l a n e t A t t a c k S h i p s C o l o r A r r a y , Y LDX l o w e r P l a n e t A t t a c k S h i p s C o l o r A r r a y , Y
LDA c o l o r s F o r A t t a c k S h i p s , X LDA c o l o r s F o r A t t a c k S h i p s , X
STA $D027 , Y ; S p r i t e 0 C o l o r STA $D027 , Y ; S p r i t e 0 C o l o r
LDA u p p e r P l a n e t A t t a c k S h i p s S p r i t e V a l u e A r r a y , Y LDA l o w e r P l a n e t A t t a c k S h i p s S p r i t e V a l u e A r r a y , Y
STA S p r i t e 0 P t r , Y STA S p r i t e 0 P t r , Y
LDX tempVarStorage LDX tempVarStorage
DEX DEX
DEX DEX
DEY DEY
BNE UpperPlanetShipsLoop BNE LowerPlanetShipsLoop
RTS RTS
We want scrolling to be smooth and fast. Moving swiftly or slowly across the planet
surface depending on how much acceleration we apply is the fundamental dynamic of
the game so will be important to get right.
Exerting some \=ne-grained control on the scrolling requires us to balance two funda-
mental operations: a pixel-by-pixel scrolling mechanism that preserves the smooth-
ness of movement we want at slower speeds, and a bigger, blunter implement that
will accelerate us across larger sections of the planet while preserving an illusion of
relative fluidity.
The \=rst is available as a hardware implementation on the C64. The value stored in
the last three bits of $D016 allows us to specify a pixel offset for the planet graphics
between 0 and 7, effectively shifting the planet left or right by one or more pixels.
The second is up to us. We need to keep an eye on the speed of our gilby and decide
if we should shift the landscape by one or more full characters.
Here are both of these tactics in operation during two paints of the screen while we're
warping into the Sheep planet at the start of a new game. Each row represents a single
112
CHAPTER 6. BLASTING, FAST AND SLOW
pass of the raster. As you can see we adjust the pixel position using $D016 twice on
each pass and adjust the character position once.
Since we're moving fairly fast, we're updating the character position by 3 characters on
each occasion (notice how much the bush moves). At the same time we're applying a
pixel movement to preserve the impression of smoothness.
If we look at the code that looks after the pixel-grained movement in AnimateStar-
FieldAndScrollPlanets we can see it is using a variable called planetScrollSpeed
to control the amount of offset to apply:
This parameter is always kept to a value between 0 and 7, for example here in
DrawPlanetScroll where it gets clamped to the last 3 bits (Bits 0 to 2) by an AND
operation:
LDA p l a n e t S c r o l l S p e e d
AND \# $07
STA p l a n e t S c r o l l S p e e d
113
CHAPTER 6. BLASTING, FAST AND SLOW
As you might guess, planetScrollSpeed is controlled by the speed of the gilby itself:
LDA p l a n e t S c r o l l S p e e d
CLC
ADC c u r r e n t G i l b y S p e e d
STA p l a n e t S c r o l l S p e e d
The more we push on the joystick left or right the greater currentGilbySpeed be-
comes. The greater currentGilbySpeed becomes, the more we add to planetScrollSpeed.
The only thing we have to be careful about when using this simple mechanic is to en-
sure that when we update $D016 with planetScrollSpeed we are only updating the
lower 3 bits - writing to the rest of them will break things as they are not concerned
with scrolling at all. This is why, when we \=rst retrieve $D016 to the A register we mask
out the \=rst four bits:
AND'ing a notional value of $DE with $F0 gives $D0 preserving the \=rst 4 bits (D).
ORA'ing a notional value for planetScrollSpeed of $06 with $D0 gives $D6, adding our
planetScrollSpeed into $D0 without disturbing what was aleady there:
ORA p l a n e t S c r o l l S p e e d
114
CHAPTER 6. BLASTING, FAST AND SLOW
Finally, ORA \#$10 ensures that the multi-color mode bit in $D016 is set:
ORA \# $10
STA $D016 ; VIC Control Register 2
So in a nutshell we're not being too fussy about what value we select for the pixel-
perfect offset. We just take the overall scroll speed and clamp it to a value between
0 and 7. This is 'good enough' in practice - it ensures we're avoiding the appearance
of scrolling purely character-wise by guaranteeing we're nearly always displaying the
surface offset by some small number of pixels.
As a quick reminder from our chapter on generating the surfaces of the planets ('Mak-
ing Planets for Nigel'), the data we came up with for the ever-so-randomly-generated
planet was stored between $8000 and $8FFF.
Listing 6.2: The surface data is stored from $8000 to $8FFF. This code overwrites it all with the value $60
which is an empty bitmap.
As you can see in the code comment above the planet surface itself is 256 bytes long
and we store 4 layers of 256 bytes each. The bottom layer is the surface, the other
three are used for \=lling in the structures that dot the planet's surface.
When we initialize the game we use a bunch of pointers to store the position of each
of these layers:
115
CHAPTER 6. BLASTING, FAST AND SLOW
Every time we scroll by one ore more characters along the planet surface updating
what we see on the screen will just be a simple question of calling a routine called
DrawPlanetSurfaces to write wherever in each layer the current pointer is pointing to:
DrawPlanetSurfaces
....
So all we have to do as we scroll along the planet is adjust the position in RAM between
$8000-$83FF that planetTextureTopLayerPtr is pointing to (and the same for the
other layers) and we will effect the illusion of movement across the surface of the
planet.
This means that our job is a simple one: how many characters should we move along
the planet?
116
CHAPTER 6. BLASTING, FAST AND SLOW
Just like with calculating the more \=ne-grained pixel offset we used planetScrollSpeed
to determine how much to move by. But whereas the pixel movement used the lower
3 bits of planetScrollSpeed to come up with a value between 0 and 7 to adjust the
pixel scroll by, we will here instead use the 3 bits before that.
The reason for doing that is straightforward: if those upper 3 bits are set the number
in planetScrollSpeed must be fairly large and therefore enough to warrant scrolling
an entire character or even more.
In a situation where we're moving to the right, this is the logic that \=gures out how
many characters to move and updates planetTextureTopLayerPtr with the updated
position:
ScrollPlanetRight
LDA p l a n e t S c r o l l S p e e d
EOR \# $FF
CLC
AND \# $F8
ROR
ROR
ROR
STA tempHiPtr1
INC tempHiPtr1
LDA p l a n e t T e x t u r e T o p L a y e r P t r
CLC
ADC tempHiPtr1
STA p l a n e t T e x t u r e T o p L a y e r P t r
117
CHAPTER 6. BLASTING, FAST AND SLOW
This is more complicated than we actually had reason to expect. Surely if the value
in planetScrollSpeed is greater than 7 we could just shift the bits over there and use
that instead? For example with a notional value of $1F in planetScrollSpeed if we just
did the following:
ScrollPlanetRight
LDA p l a n e t S c r o l l S p e e d
AND \# $F8
ROR
ROR
ROR
We would get a value of $03 for our number of characters to move by. THis is because
AND'ing $1F and $F8 gives us $18:
And then using ROR to shift the bits to the right three times results in $03:
Figure 6.5: ROR performed three times shifts everything to the right by three bits.
But instead of this we're doing an exclusive-or with the value in planetScrollSpeed
\=rst:
ScrollPlanetRight
LDA p l a n e t S c r o l l S p e e d
EOR \# $FF
The reason we have to do this is because the value in planetScrollSpeed isn't what
we might have assumed: a linear value between 0 and 40 for example that goes up
and down a sliding scale depending on how fast we're going. Instead, it's something
slightly different. It's fed by the value in currentGilbySpeed that reflects the current
velocity of the gilby and this always starts out at a value of $EA:
SetUpGilbySprite
LDA \# G I L B Y \. A I R B O R N E \. R I G H T
STA c u r r e n t G i l b y S p r i t e
118
CHAPTER 6. BLASTING, FAST AND SLOW
..
LDA \# $EA
STA c u r r e n t G i l b y S p e e d
..
RTS
There's a trick at work here. The gilby can move left or right and currentGilbySpeed
is being used to store both the speed *and* direction of the gilby. When the value is
between 00 and 80 the gilby is moving to the left and the value reflects its relative
velociy. When the value is between 80 and FF the gilby is moving to the right and the
value reflects its relative velocity.
Sure enough, when we look at the calculation using for the character scroll offset when
moving left, it's what we originally envisaged:
ScrollPlanetLeft
LDA p l a n e t S c r o l l S p e e d
CLC
ADC c u r r e n t G i l b y S p e e d
STA p l a n e t S c r o l l S p e e d
..
b72CF CLC
ROR
ROR
ROR
STA tempHiPtr1
LDA p l a n e t T e x t u r e T o p L a y e r P t r
SEC
SBC tempHiPtr1
So the extra steps we see in ScrollPlanetRight are to handle the fact that the speed
is going to be some value between $80 and $FF. The exclusive-or (EOR) has the effect of
reversing the value in planetScrollSpeed and transforming it into a number between
0 and 16 that we can then clamp to a number between 0 and 7.
ScrollPlanetRight
LDA p l a n e t S c r o l l S p e e d
EOR \# $FF
CLC
AND \# $F8
ROR
ROR
119
CHAPTER 6. BLASTING, FAST AND SLOW
ROR
STA tempHiPtr1
INC tempHiPtr1
LDA planetTextureTopLayerPtr
CLC
ADC tempHiPtr1
STA planetTextureTopLayerPtr
; -------------------------------------------------------
; PerformMainGameUpdate
; -------------------------------------------------------
PerformMainGameUpdate
...
JSR C a l c u l a t e G i l b y V e r t i c a l P o s i t i o n E a r t h B o u n d
JSR C a l c u l a t e G i l b y V e r t i c a l P o s i t i o n A i r b o r n e
...
Listing 6.3: The routines responsible for updating the Gilby's vertical position.
When the gilby jumps on the surface of the planet it exhibits a smooth and pleasing
acceleration in ascent and descent that eloquently suggests the gravity of the planet.
This is achieved with a remarkably simple mechanism. Rather than de\=ne distinct be-
haviours for the journey upwards and the journey back, we use a single continuous
movement based on incrementing the gilby's vertical position with an offset that 'cy-
cles around' and transitions naturally from ascent to descent.
Achieving the complementary movements of ascent and descent with the same op-
eration depends on a simple property of byte values when we increment them. If we
keep adding to a value and it eventually reaches the maximum value of 255 (i.e. $FF)
that can be stored in a single byte, the next time we add 1 to it it will cycle around to
$00. So if we add $FB, for example, to $09 we get $05.
So let's take our starting vertical position (our Y co-ordinate) on the planet to be $6D.
This is the Y-coordinate on the screen that coincides with the surface of the planet.
If we're planning to move upwards we might think that the natural number to subtract
from this position to move us up the screen is something like 3 or 4. And then when
we're moving down the screen later on we would increment this position value by 3 or
4, or some smaller number depending on how quickly we want to move. This intuition
is correct, up to a point, but it would mean keeping track of which direction we're going
and complicate our code.
Given the circular property of byte arithmetic we described above we can increment or
120
CHAPTER 6. BLASTING, FAST AND SLOW
decrement our position by simply adding a carefully chosen value. If we add a number
that will force the result to cycle around and come out less than the current Y value
then we've succesfully subtracted from Y while adding to it! This the essence of our
trick.
UpdateGilbyPosition
CLC
ADC g i l b y L a n d i n g J u m p i n g A n i m a t i o n Y P o s O f f s e t
STA g i l b y V e r t i c a l P o s i t i o n U p p e r P l a n e t
LDA \# Y P O S \. P L A N E T \. S U R F A C E
STA g i l b y V e r t i c a l P o s i t i o n U p p e r P l a n e t
StorePositionAndReturn
; Update the position on screen .
LDA g i l b y V e r t i c a l P o s i t i o n U p p e r P l a n e t
STA $D001 ; Sprite 0 Y Pos
RTS
Our choice of $FB as an initial value for the offset is very deliberate. As we increment
this offset it will eventually reach $FF. Each time we do so the resulting degree in move-
ment it creates gets smaller. So the gilby will appear to move quickly at \=rst but slow
down the further it gets from the planet:
121
CHAPTER 6. BLASTING, FAST AND SLOW
Figure 6.7: Each increment in the offset, performed every three movements, results in a deceleration effect.
122
CHAPTER 6. BLASTING, FAST AND SLOW
123
CHAPTER 6. BLASTING, FAST AND SLOW
Figure 6.8: Each increment in the offset, performed every three movements, results in an acceleration
effect.
With a simple, consistent addition operation each time it comes to position the gilby
we've managed to achieve a jumping and landing effect with a neat gravity effect built
in!
124
CHAPTER 6. BLASTING, FAST AND SLOW
; -------------------------------------------------------
; PerformMainGameUpdate
; -------------------------------------------------------
PerformMainGameUpdate
...
JSR P l a y S o u n d E f f e c t s
...
Iridis Alpha has a rich weave of sound effects during play. The game is as much an
assault on the ears as it is on the eyes. In order to create the impression that there are
multiple sounds going on at once so it is necessary to... have multiple sounds going
at once. The fancy word for this is 'multiplexing'. If we want the player to experience
the world of Iridis Alpha as one in which things are not happening sequentially but
simultaneously we will need the sounds they experience to interleave with each other,
rather than just playing whatever the 'current' sound is then waiting for it to \=nish before
we play the next. Playing two sounds at once is more than enough multiplexing to be
going on with, so that is what we do.
The way to achieve this is to come up with a data structure for sound effects that
segments the full sound effect into multiple frames and to process a few frames of
each data structure every time PlaySoundEffects is called by the raster interrupt. We
also need PlaySoundEffects to behave like a 'state machine'. This means that when
it is called a little later it can pick up from where it left off on each data structure,
recognize when it has \=nished playing the frames for that interrupt and also when it
has \=nished playing the full effect. This will allow us to manage two different sound
effects concurrently. Before we look at how this multiplexing is achieved lets look at
how a single sound effect is managed in general.
As ever, the key to managing complexity isn't the cleverness of our code but the simplic-
ity of the data structure we choose. Iridis Alpha solves this with a remarkably compact
solution where each frame in the sound effect is represented by a meagre 5 bytes and
the whole sound effect is encoded by a sequence of these 5-byte records. In order to
unpack what this looks like in practice, let's look at the iconic effect played when we
enter a new planet. The data structure here is just one part of the level entry sound
effect, the duller part. It plays a dull bass note that fades away. (We'll examine the
more interesting leg of the overall sound effect, the one that it's multiplexed with, right
after this. But this is a relatively nice and simple one for us to get an idea of how the
data structure works.)
125
CHAPTER 6. BLASTING, FAST AND SLOW
PLAY\.SOUND = $00
PLAY\.LOOP = $05
LINK = $80
VOICE2\.HI = $08
VOICE2\.CTRL = $0B
VOICE3\.HI = $0F
VOICE 2\.ATK\.D EC = $0C
VOICE 2\.SUS\.R EL = $0D
VOICE3\.CTRL = $12
VOICE 3\.ATK\.D EC = $13
VOICE 3\.SUS\.R EL = $14
planetWarpSoundEffect .BYTE $00 , PLAY\.SOUND ,$0F , VOICE2\. ATK\.DEC , $00
.BYTE $00 , PLAY\.SOUND ,$0F , VOICE3\. ATK\.DEC , $00
.BYTE $00 , PLAY\.SOUND ,$0F , VOLUME , $00
.BYTE $00 , PLAY\.SOUND ,$00 , VOICE2\. SUS\.REL , $00
.BYTE $00 , PLAY\.SOUND ,$00 , VOICE3\. SUS\.REL , $00
.BYTE $00 , PLAY\.SOUND ,$03 , VOICE2\.HI , $00
.BYTE $00 , PLAY\.SOUND ,$03 , VOICE3\.HI , $00
.BYTE $00 , PLAY\.SOUND ,$21 , VOICE2\.CTRL , $00
.BYTE $00 , PLAY\.SOUND ,$08 , VOICE3\.LO , $00
.BYTE $00 , PLAY\.SOUND ,$00 , VOICE2\.LO , $00
pwLoop .BYTE $00 , PLAY\.SOUND ,$21 , VOICE3\.CTRL , $01
.BYTE $18 , PLAY\.LOOP ,$00 , !` pwLoop , ?` pwLoop
.BYTE $00 , PLAY\.SOUND ,$20 , VOICE2\.CTRL , $00
.BYTE $00 , PLAY\.SOUND ,$20 , VOICE3\.CTRL , $00
.BYTE $00 , LINK , !` setVolToMax , ?` setVolToMax , $00
This data is like a piano roll fed into PlaySoundEffects. In simpli\=ed terms each line
is a 'frame' containing a single note for it to play. For the lines with PLAY SOUND it re-
ally is that simple. Byte 3 in each of those lines is the 'note' to play, and Byte 4 (e.g.
VOICE2 ATK DEC is the 'key on the piano' to play it on. PlaySoundEffects will keep play-
ing each line in the roll until it hits one with a value in Byte 5 that is not $00. In this case
that happens when it hits the line that starts with pwLoop, notice the $01 at the very
end.
Let's be more concrete about what's happening when each PLAY SOUND record is pro-
cessed as it's quite simple. The value in Byte 3 is written to the position in the SID
register given by Byte 4. The SID register is an array of bytes in the C64's ROM that
controls the production of sound and they live between addresses $D400 and $D418.
So in the \=rst record in planetWarpSoundEffect we are going to write $0F to the ad-
dress $D40C in the SID register.
What does that do you might ask? Does it play a note? Well, no, as it happens this
address in the SID register is responsible for controlling how much a sound rises or
falls. As you can see in the visualization of the effect below it decays away. This byte
we're setting is reponsible for setting that on 'Voice 2' in the sound chip.
126
CHAPTER 6. BLASTING, FAST AND SLOW
Figure 6.9
The next record does the same for 'Voice 3'. The one after that sets the volume ($D418)
to its maximum value of 15 ($0F.
The next two records ensure Voices 2 and 3 do not sustain their sound by writing $00
to both. We just want them to die away quickly after all.
What we are doing here is writing a frequency value to SID registers for Voice 2 and
Voice 3 that will actually make some noise. We then tell the sound device to start
playing the note by setting its 'gate' to 1. The 'gate' is the 'least signi\=cant bit', i.e. the
1 in the $21 given in Byte 3:
127
CHAPTER 6. BLASTING, FAST AND SLOW
We now encounter the use of a new record type called PLAY LOOP.
VOLUME = $18
pwLoop .BYTE $00 , PLAY\.SOUND ,$21 , VOICE3\.CTRL , $01
.BYTE VOLUME , PLAY\.LOOP ,$01 , !` pwLoop , ?` pwLoop
What this this particular instance of a PLAY LOOP record does is gradually lower the
volume until it reaches zero.
More generally the way PLAY LOOP records are processed is as follows. First of all we
write to the offset in the SID register given by Byte 1 (in this case the register responsible
for volume). The value we write is also derived from Byte 1, but by using it as in index
into an array called soundEffectBuffer:
soundEffectBuffer .BYTE $00 , $94 , $00 , $00 , $11 , $0F , $00 , $00
.BYTE $03 , $00 , $00 , $21 , $0F , $00 , $08 , $03
.BYTE $00 , $00 , $21 , $0F , $00 , $00 , $00 , $00
.BYTE $02 , $00 , $00 , $00 , $00 , $00 , $00 , $00
we can see Byte 1 is VOLUME, which is an alias for the value $18. Using $18 as an index
into soundEffectBuffer retrieves the fourth value on the third line above, $0F. We then
subtract the value in Byte 3 ($01) and store the result back in the soundEffectBuffer.
If it's not zero yet, we then treat the record pointed to by Bytes 4 and 5 as the next one
to play. In this case thats given as pwLoop. So in fact we go round in a loop between
the two records until $0F eventually becomes zero. Here's the code that handles this:
LDX s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 1
LDA s o u n d E f f e c t B u f f e r ,X
SEC
SBC s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 3
StorePointersAndReturnIfZero
STA s o u n d E f f e c t B u f f e r ,X
STA $D400 , X ; Voice 1: Frequency Control - Low - Byte
BEQ J u m p T o G e t N e x t R e c o r d I n S o u n d E f f e c t
LDA s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 4
LDX i n d e x T o P r i m a r y O r S e c o n d a r y S o u n d E f f e c t P t r
STA p r i m a r y S o u n d E f f e c t L o P t r ,X
LDA s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 5
STA p r i m a r y S o u n d E f f e c t H i P t r ,X
RTS
128
CHAPTER 6. BLASTING, FAST AND SLOW
JumpToGetNextRecordInSoundEffect
JMP G e t N e x t R e c o r d I n S o u n d E f f e c t
You can see that once we reach zero BEQ JumpToGetNextRecordInSoundEffect be-
comes true and we end up calling GetNextRecordInSoundEffect which actually lets
us move on to the next record in our data structure (the two we've just looped through
are given at the start to make it easier to see where picked up from):
You can see we write $20 to VOICE2 CTRL and VOICE3 CTRL. This stops the noise we
were making until now. It switches off the sound by setting the 'gate' we set to 1 earlier
on back to 0.
The \=nal record we play is a simple 'jump' record. Its processed by simply jumping to
the address given in Bytes 4 and 5 and playing whatever is there:
In this case it is pointing us to the data structure at setVolToMax. This simply runs in
a loop setting the volume back to $0F, i.e. it's maximum value of 15. Notice the way
the \=rs record below writes $0F (Byte 3) to the VOLUME register. The second one, just
tells it loop back and do the same thing again. This will keep running until a new sound
effect is selected.
We can get a picture of how each record affects the relevant registers in the SID inter-
face by looking at the table below. This is full history of each record that's processed in
the planetWarpSoundEffect data structure by PlaySoundEffects in temporal order.
Notice the way the colume steadily decreases as we go through the PLAY LOOP loop
towards the end.
129
CHAPTER 6. BLASTING, FAST AND SLOW
6.4.2 Multiplexing
While playing the fairly dull component we cover above, Iridis Alpha plays a second
sequence simultaneously that constitutes the one a player will know and recognize.
130
CHAPTER 6. BLASTING, FAST AND SLOW
Figure 6.10
The way this concurrency is managed is by storing the addresses for each effect sep-
arately. In the case of the sequence where the player is entering a new planet this is
done in PerformPlanetWarp, the routine responsible for rendering the warp sequence
as a whole:
PerformPlanetWarp
...
LDA \# !` p l a n e t W a r p S o u n d E f f e c t
STA s e c o n d a r y S o u n d E f f e c t L o P t r
LDA \# ?` p l a n e t W a r p S o u n d E f f e c t
STA s e c o n d a r y S o u n d E f f e c t H i P t r
LDA \# !` p l a n e t W a r p S o u n d E f f e c t 2
STA p r i m a r y S o u n d E f f e c t L o P t r
LDA \# ?` p l a n e t W a r p S o u n d E f f e c t 2
STA p r i m a r y S o u n d E f f e c t H i P t r
PlaySoundEffects
LDA \# $00
STA i n d e x T o P r i m a r y O r S e c o n d a r y S o u n d E f f e c t P t r
LDA s o u n d E f f e c t I n P r o g r e s s
BEQ D o n t D e c r e m e n t S o u n d E f f e c t P r o g r e s s C o u n t e r
DEC s o u n d E f f e c t I n P r o g r e s s
DontDecrementSoundEffectProgressCounter
LDA p r i m a r y S o u n d E f f e c t L o P t r
131
CHAPTER 6. BLASTING, FAST AND SLOW
STA currentSoundEffectLoPtr
LDA primarySoundEffectHiPtr
STA currentSoundEffectHiPtr
JSR PlayCurrentSoundEffect
LDA \# $02
STA i n d e x T o P r i m a r y O r S e c o n d a r y S o u n d E f f e c t P t r
LDA s e c o n d a r y S o u n d E f f e c t L o P t r
STA c u r r e n t S o u n d E f f e c t L o P t r
LDA s e c o n d a r y S o u n d E f f e c t H i P t r
STA c u r r e n t S o u n d E f f e c t H i P t r
; Falls through and plays secondary sound effect.
; -------------------------------------------------------
; PlayCurrentSoundEffect
; -------------------------------------------------------
PlayCurrentSoundEffect
LDY \# $00
; Read the 5 - byte record into working storage.
FillSoundEffectDataStructureLoop
LDA ( c u r r e n t S o u n d E f f e c t L o P t r ) ,Y
STA s o u n d E f f e c t D a t a S t r u c t u r e ,Y
INY
CPY \# $05
BNE F i l l S o u n d E f f e c t D a t a S t r u c t u r e L o o p
The secondary sound effect for the planet warp sequence, which is the one with all the
recognizable noises, looks like this:
Just like the primary sound effect we discussed this one contains a loop, achieving
the same effect as before. There are a couple of new record types in there though,
namely INC AND PLAY FROM BUFFER and REPEAT PREVIOUS. The latter is probably self-
explanatory, it repeats processing of the previous record for as many times as given
by the value in Byte 3 which in this is case is $08 times.
132
CHAPTER 6. BLASTING, FAST AND SLOW
INC AND PLAY FROM BUFFER does something more interesting, in the \=rst case above
it's being used to update the sound we play (i.e. write to register VOICE1 HI) by a reg-
ularly incrementing amounts of $64 given in Byte 3. This value is picked from and
updated in position 1 of the soundEffectBuffer, this position being given by Byte 1.
The result is the 'iconic' bleeping sound that increases in frequency as we warp into
the planet. Here we see the section in PlaySoundEffects that processes records of
type INC AND PLAY FROM BUFFER, and the way it uses each byte in the record to effect
the update and the write to the sound register:
The amount of looping the data structure demands is much greater than the effect we
reviewed previously. We can get a sense of this from the truncated trace which shows
a total of over 250 writes to the sound register and of which the table below is just a
snapshot:
133
Record Type Byte 1 Byte 2 Byte 3 Byte 4 Byte 5
'00' indicates the next record should be
played immediately.
Value to write to
PLAY SOUND Offset to $D400 '01' indicates should play no more records.
Play Sound Unused offset to $D400
($00) to write to. Anything else indicates the next record
given by Byte 4.
should be stored and
no more should be played for now.
'00' indicates the next record should be
played immediately.
Address of byte INC AND PLAY Amount to
Increment and Offset to $D400 '01' indicates should play no more records.
to pick from FROM BUFFER increment
Play from Buffer to write to. Anything else indicates the next record
soundEffectBuffer ($01) picked byte by.
should be stored and
no more should be played for now.
'00' indicates the next record should be
played immediately.
Address of byte DEC AND PLAY Amount to
Decrement and Offset to $D400 '01' indicates should play no more records.
to pick from FROM BUFFER decrement
Play from Buffer to write to. Anything else indicates the next record
soundEffectBuffer ($02) picked byte by.
should be stored and
no more should be played for now.
Address of byte
Amount to
to pick from PLAY LOOP Lo Ptr of Hi Ptr of
Play Loop decrement
soundEffectBuffer ($05) next record. next record.
picked byte by.
and offset to $D400
'00' indicates the next record should be
played immediately.
Link to LINK Lo Ptr of Hi Ptr of '01' indicates should play no more records.
Unused.
Record ($80) next record. next record. Anything else indicates the next record
should be stored and
no more should be played for now.
'00' indicates the next record should be
played immediately.
Repeat REPEAT
Number of '01' indicates should play no more records.
Previous Unused. PREVIOUS Unused.
times to play previous record\. Anything else indicates the next record
Record ($81)
should be stored and
no more should be played for now.
134
CHAPTER 6. BLASTING, FAST AND SLOW
Figure 6.11: The data structures for sound effects used by Iridis Alpha.
CHAPTER 6. BLASTING, FAST AND SLOW
The PlaySoundEffects code has some leftover routines for record types that appear
to have gone unused in the \=nal game. These make slightly more complicated use of
the soundEffectBuffer. All they do is mutate the data in the buffer using the 5-byte
record. They don't actually play any sounds or make any writes to the SID register.
For example here is the logic applied to records with a 'type byte' in Byte 2 of ($03):
TrySequenceByteValueOf3
CMP \# $03
BNE T r y S e q u e n c e B y t e V a l u e O f 4
LDX s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 1
LDY s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 3
135
CHAPTER 6. BLASTING, FAST AND SLOW
LDA s o u n d E f f e c t B u f f e r ,X
CLC
ADC s o u n d E f f e c t B u f f e r ,Y
JMP G e t N e x t R e c o r d I n S o u n d E f f e c t L o o p
This takes the value in Byte 1 as an offset into soundEffectBuffer, adds the value
found at the offset in soundEffectBuffer given by Byte 3 and stores the result in the A
accumulator. It then proceeds directly to reading the next record in the sound effect's
data structure.
Logic for records with a type of $04 does the same thing but subtracts rather than adds:
TrySequenceByteValueOf4
CMP \# $04
BNE M a y b e I s F a d e O u t L o o p
LDX s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 1
LDY s o u n d E f f e c t D a t a S t r u c t u r e \. B y t e 3
LDA s o u n d E f f e c t B u f f e r ,X
SEC
SBC s o u n d E f f e c t B u f f e r ,Y
JMP G e t N e x t R e c o r d I n S o u n d E f f e c t L o o p
So the general idea for these two unused record types seems to have been that the
soundEffectBuffer could be used to generate a sequence of sounds in some sort
of procedural or even fractal manner, similar to the way in which the title music was
generated. The experiment obviously didn't work as they ended up on the cutting room
floor, with just this leftover code to indicate the attempt.
136
CHAPTER 6. BLASTING, FAST AND SLOW
137
Figure 6.12: Some of the sound effects played during the game. On PDF viewers that support it, you can
click the play icon to hear the effect.
Enemies and their Discontents
Jeffrey Says
This is the bit that I knew would take me ages to write and get glitch free, and
the bit that is absolutely necessary to the functioning of the game. The module
ACONT is essentially an interpreter for my own 'wave language', allowing me
to describe, exactly, an attack wave in about 50 bytes of data. The waves for
the \=rst part of IRIDIS are in good rollicking shoot-'em-up style, and there have to be plenty
of them. There are \=ve planets and each planet is to have twenty levels associated with
it. It's impractical to write separate bits of code for each wave; even with 64K you can run
outta memory pretty fast that way, and it's not really necessary coz a lot of stuff would be
duplicated. Hence ACONT.
The bits and bytes that de\=ne the behaviour and appearance of wave after wave of
Iridis Alpha's enemy formations - twenty across each of the \=ve planets giving one
hundred in all - takes up relatively little space given the sheer variety of adversaries the
player faces.
138
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Figure 7.1: Sheep Planet - Level 1 - Enemy Data for the First Wave.
139
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
To get an understanding of how the level data is used we can start with the very \=rst
byte of the data used for the \=rst wave in the very \=rst level. This is the wave of flying
saucers you will already be familiar with if you have played the game (:)):
Figure 7.2: The sprites used to animate the 'UFO' in the \=rst level.
The main colour for this sprite is blue. This may not be obvious from looking at the
sprites themselves, but the way the sprite color schemes work is that you can select
two colors that are available for all sprites and one color that is unique to the sprite
itself. In this case, the unique color selected for the flying saucers is determined by
Byte 1 in the Level Data. You can see in the table in the page opposite that this is
de\=ned with a value of $06. This is how it looks in the code itself:
planet1Level1Data
; Byte 0 ( Index $00 ) : An index into c o l o r s F o r A t t a c k S h i p s that
applies a
; color value for the ship sprite.
.BYTE $06
This value is an index into the array colorsForAttackShips. Starting at zero we count
up to 6 and arrive at the 7th item in the list below, giving us the result BLUE:
colorsForAttackShips
.BYTE BLACK , WHITE , RED , CYAN , PURPLE , GREEN , BLUE , YELLOW
.BYTE ORANGE , BROWN , LTRED , GRAY1 , GRAY2 , LTGREEN , LTBLUE , GRAY3
In the routine that draws the attack wave we \=nd the following code segment writing
this value to the register $D027 that determines the color of the sprite:
; -------------------------------------------------------
; D r a w U p p e r P l a n e t A t t a c k S h i p s Routine
; -------------------------------------------------------
140
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
DrawUpperPlanetAttackShips
LDX \# $0C
LDY \# $06
UpperPlanetShipsLoop
...
LDX u p p e r P l a n e t A t t a c k S h i p s C o l o r A r r a y ,Y
LDA c o l o r s F o r A t t a c k S h i p s ,X
STA $D027 , Y ; Sprite Y Color
...
DEX
DEX
DEY
BNE U p p e r P l a n e t S h i p s L o o p
RTS
Notice that the $06 was originally stored in a position in the array upperPlanet-
AttackShipsColorArray. This happened in an earlier routine that loads the majority
of the data for a level:
GetWaveDataForNewShip
; X is the index of the ship in a c t i v e S h i p s W a v e D a t a L o P t r A r r a y
LDY \# $00
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
STA u p p e r P l a n e t A t t a c k S h i p s C o l o r A r r a y + $01 , X
You pass the interpreter data, that describes exactly stuff like: what each alien
looks like, how many frames of animation it uses, speed of that animation,
colour, velocities in X--- and Y--- directions, accelerations in X and Y, whether
the alien should 'home in' on a target, and if so, what to home in on; whether
an alien is subject to gravity, and if so, how strong is the gravity; what the alien should do
if it hits top of screen, the ground, one of your bullets, or you; whether the alien can \=re
bullets, and if so, how frequently, and what types; how many points you get if you shoot it,
and how much damage it does if it hits you; and a whole bunch more stuff like that. As you
can imagine it was a fairly heavy routine to write and get debugged, but that's done now;
took me about three weeks in all I'd say.
141
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Figure 7.3: Con\=rmation that the game developer was a young male. The sprites used to animate attack
wave 17 in the Mushroom Planet.
Looking again at the table in the previous page we can see the \=rst 7 bytes are con-
cerned with the appearance and basic behaviour of the enemy. Bytes 1 and 2 de\=ne
the sprite used for display on the upper planet, Bytes 4 and 5 for the lower planet. The
reason there's two in each case is because they are describing the start and end point
of the sprite's animation.
We can see this in action in AnimateAttackShipSprites. When this routine runs Byte
3 has been loaded to upperPlanetAttackShipInitialFrameRate for the upper planet
and lowerPlanetAttackShipInitialFrameRate for the lower planet. This routine is
cycling through the sprites given by Byte 1 as the lower limit and Byte 2 as the upper
limit. This is what the animation consists of: an animation effect achieved by changing
the sprite from one to another to create a classic animation effect.
AnimateAttackShipSprites
LDA p a u s e M o d e S e l e c t e d
BEQ A n i m a t e U p p e r P l a n e t A t t a c k S h i p s
RTS
AnimateUpperPlanetAttackShips
DEC u p p e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
BNE A n i m a t e L o w e r P l a n e t A t t a c k S h i p s
LDA u p p e r P l a n e t A t t a c k S h i p I n i t i a l F r a m e R a t e - $01 , X
STA u p p e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
INC u p p e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
LDA u p p e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
142
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
AnimateLowerPlanetAttackShips
DEC l o w e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
BNE D o n t A n i m a t e L o w e r P l a n e t A t t a c k S h i p
LDA l o w e r P l a n e t A t t a c k S h i p I n i t i a l F r a m e R a t e - $01 , X
STA l o w e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
INC l o w e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
LDA l o w e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
AnimateLowerPlanetAttackShips
DEC l o w e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
BNE D o n t A n i m a t e L o w e r P l a n e t A t t a c k S h i p
LDA l o w e r P l a n e t A t t a c k S h i p I n i t i a l F r a m e R a t e - $01 , X
STA l o w e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
INC l o w e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
LDA l o w e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
If it is zero, it instead gets reset to the initial value from Byte 1 (stored in upperPlanet-
AttackShipInitialFrameRate) and the current sprite for the enemy ship is incre-
mented to point to the next 'frame' of the ship's animation:
AnimateUpperPlanetAttackShips
DEC u p p e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
BNE A n i m a t e L o w e r P l a n e t A t t a c k S h i p s
LDA u p p e r P l a n e t A t t a c k S h i p I n i t i a l F r a m e R a t e - $01 , X
143
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
STA u p p e r P l a n e t A t t a c k S h i p A n i m a t i o n F r a m e R a t e - $01 , X
INC u p p e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
LDA u p p e r P l a n e t A t t a c k S h i p 2 S p r i t e V a l u e ,X
Next we check if we've reached the end of the animation by checking the value of
Byte 2 (loaded to upperPlanetAttackShipSpriteAnimationEnd). If so, we reset
upperPlanetAttackShip2SpriteValue to the value initially loaded from Byte 1 - and
that is what will be used to display the ship the next time we pass through to animate
the ship:
DrawUpperPlanetAttackShips
LDX \# $0C
LDY \# $06
UpperPlanetShipsLoop
LDA u p p e r P l a n e t A t t a c k S h i p s X P o s A r r a y ,Y
STA $D000 , X ; Sprite 0 X Pos
LDA a t t a c k S h i p s X P o s A r r a y - $01 , Y
AND $D010 ; Sprites 0 -7 MSB of X coordinate
STA c u r r e n t M S B X P o s O f f s e t
LDA u p p e r P l a n e t A t t a c k S h i p s M S B X P o s A r r a y ,Y
AND a t t a c k S h i p s M S B X P o s O f f s e t A r r a y ,Y
ORA currentMSBXPosOffset
STA $D010 ; Sprites 0 -7 MSB of X coordinate
LDA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y ,Y
STA $D001 , X ; Sprite 0 Y Pos
STX tempVar Storage
LDX u p p e r P l a n e t A t t a c k S h i p s C o l o r A r r a y ,Y
LDA c o l o r s F o r A t t a c k S h i p s ,X
STA $D027 , Y ; Sprite 0 Color
LDA u p p e r P l a n e t A t t a c k S h i p s S p r i t e V a l u e A r r a y ,Y
STA Sprite0Ptr ,Y
LDX tempVar Storage
DEX
DEX
DEY
BNE U p p e r P l a n e t S h i p s L o o p
RTS
144
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
If we look at Byte 18 and Byte 20 for Level 1 we can see that the the fast lateral move-
ment of the 'UFO's is implemented by a relatively high value of $06 for the number of
pixels it moves at each step while the interval between steps is relatively low ($01).
Meanwhile the more gradual up and down movement is implemented by a value of
$01 in Byte 19 and Byte 21.
For the second level ('bouncing rings') the horizontal movement is more constrained
(Byte 18 is $00) while the vertical movement is more extreme (Byte 19 is $24) - achiev-
ing the bouncing effect.
The horizontal movement for Level Three, home to the infamous 'Licker Ships' is
$FA, which would make you think the enemies must be moving horizontally extremely
quickly. In fact, when the high bit is set a special behaviour is invoked:
LDA x P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p - $01 , X
BMI U p p e r B i t S e t O n X P o s M o v e m e n t
This is the special behaviour that makes the Licker Ships on this level such an enor-
mous pain in the arse to play against.
145
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Figure 7.5: Enemy movement for Sheep planet, level 3 - the infamous licker ships.
When the upper bit is set (e.g. $FC,$80) on the value loaded to the accumulator by LDA
then BMI will return true and jump to UpperBitSetOnXPosMovement.
UpperBitSetOnXPosMovement
; This creates a decelerating effect on the attack ship 's
movement .
; Used by the licker ship wave in planet 1 for example.
EOR \# $FF
STA a t t a c k S h i p O f f s e t R a t e
INC a t t a c k S h i p O f f s e t R a t e
LDA u p p e r P l a n e t A t t a c k S h i p 2 X P o s ,X
SEC
SBC a t t a c k S h i p O f f s e t R a t e
STA u p p e r P l a n e t A t t a c k S h i p 2 X P o s ,X
BCS D e c r e m e n t X P o s F r a m e R a t e L o w e r P l a n e t
LDA u p p e r P l a n e t A t t a c k S h i p 2 M S B X P o s V a l u e ,X
EOR a t t a c k S h i p 2 M S B X P o s O f f s e t A r r a y ,X
STA u p p e r P l a n e t A t t a c k S h i p 2 M S B X P o s V a l u e ,X
This \=rst line EOR \#$FF performs an exclusive-or between Byte 19 in the Accumulator
($FA) and the value $FF. An exclusive-or, remember, is a bit by bit comparison of two
bytes which will set a bit in the result if an only if the bit in one of the values is set but
the other is not:
146
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
UpperBitSetOnXPosMovement
; This creates a decelerating effect on the attack ship 's movement .
; Used by the licker ship wave in planet 1 for example.
EOR \# $FF
STA a t t a c k S h i p O f f s e t R a t e
Incremented:
INC a t t a c k S h i p O f f s e t R a t e
SEC
SBC a t t a c k S h i p O f f s e t R a t e
STA u p p e r P l a n e t A t t a c k S h i p 2 X P o s ,X
The net result is a deceleration effect. This is observed in the way the licker ship wave
will accelerate out to the center before dialling back again.
Byte 6 comes into play when setting the initial Y position of a new enemy. This initial
vertical position is random, but subject to some adjustment:
SetInitialRandomPositionUpperPlanet
JSR P u t P r o c e d u r a l B y t e I n A c c u m u l a t o r R e g i s t e r
AND \# $3F
CLC
ADC \# $40
STA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y + $01 , Y
STY p r e v i o u s A t t a c k S h i p I n d e x T m p
; Byte 6 ( $06 ) : Usually an update rate for the attack ships.
LDY \# $06
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BNE R e t u r n F r o m L o a d i n g W a v e D a t a E a r l y
LDA \# $6C
LDY p r e v i o u s A t t a c k S h i p I n d e x T m p
STA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y + $01 , Y
ReturnFromLoadingWaveDataEarly
147
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
RTS
PutProceduralByteInAccumulatorRegister
randomIntToIncrement =*+ $01
LDA r a n d o m P l a n e t D a t a
INC r a n d o m I n t T o I n c r e m e n t
RTS
Since A can now contain anything from 0 to 255 ($00 to $FF) this needs to be
adjusted to a meaningful Y-position value for the upper planet. So if we imagine
PutProceduralByteInAccumulatorRegister returned $85, we now do the following
operations on it:
AND \# $3F
CLC
ADC \# $40
STA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y + $01 , Y
Our result is $05. The effect of the AND'ing here is to ensure that the random number
we get back is between 0 and 63 rather than 0 and 255. Next we add $40 (decimal 64)
to this result:
CLC
ADC \# $40
This gives $45 and this is what we store as the initial Y position for the enemy.
148
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
ditional offset to ensure that the Y position is lower on the screen for the initial position
of the enemy on the lower planet.
We still haven't got into what Byte 7 is doing though. With an initial Y position deter-
mined, it looks like the intention was for Byte 6 to specify some adjustment to this
value. But this looks like another bit of non-functioning game logic. If Byte 6 contains
a value, the function will return early without any further adjustments. If it's zero it will
then try Byte 8. If that's zero, it will return early. So the logic needs Byte 6 to be zero
and Byte 8 to contain something for anything to happen. That's never the case, so the
the adjustment never happens:
STY p r e v i o u s A t t a c k S h i p I n d e x T m p
; Byte 6 ( $06 ) : Usually an update rate for the attack ships.
LDY \# $06
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BNE R e t u r n F r o m L o a d i n g W a v e D a t a E a r l y
Listing 7.4: An adjustment that never happens. Byte 6 and Byte 8 are never set in this way.
This is de\=nitely some forgotten code. Byte 6 is elsewhere used in combination with
Byte 7 and Byte 8 to de\=ne an alternate enemy mode for some levels where the ship
will supplement any dead ships with alternate enemy types and attack patterns peri-
odically.
MaybeSwitchToAlternateEnemyPattern
; Byte 6 ( $06 ) : Usually an update rate for the attack ships.
LDY \# $06
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ E a r l y R e t u r n F r o m A t t a c k S h i p B e h a v i o u r
DEC r a t e F o r S w i t c h i n g T o A l t e r n a t e E n e m y ,X
BNE E a r l y R e t u r n F r o m A t t a c k S h i p B e h a v i o u r
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
STA r a t e F o r S w i t c h i n g T o A l t e r n a t e E n e m y ,X
; Push the current ship 's position data onto the stack.
149
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
TXA
PHA
LDY indexIntoUpperPlanetAttackShipXPosAndYPosArray ,X
LDA u p p e r P l a n e t A t t a c k S h i p s X P o s A r r a y + $01 , Y
PHA
LDA u p p e r P l a n e t A t t a c k S h i p s M S B X P o s A r r a y + $01 , Y
PHA
LDA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y + $01 , Y
PHA
Listing 7.5: Byte 6 is used to periodically switch to an enemy mode de\=ned by Bytes 7-8
Byte 6 is used to drive the rate at which this routine switches over to the enemy data/-
mode de\=ned by Byte 7 and Byte 8.
DEC r a t e F o r S w i t c h i n g T o A l t e r n a t e E n e m y ,X
BNE E a r l y R e t u r n F r o m A t t a c k S h i p B e h a v i o u r
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
STA r a t e F o r S w i t c h i n g T o A l t e r n a t e E n e m y ,X
What this routine is going to do is replace the \=rst dead ship it \=nds in the current wave
with the wave data pointed to by Byte 7-8 and create a new enemy with the current
ship's position with it.
First, we store the current ship's position. The way to do this is get the index (Y) for the
current ship X and store each of the X and Y Position information into the accumulator
\=rst A and then push it onto the 'stack' (PHA which means 'push A onto the stack').
; Push the current ship 's position data onto the stack.
TXA
PHA
LDY i n d e x I n t o U p p e r P l a n e t A t t a c k S h i p X P o s A n d Y P o s A r r a y , X
LDA u p p e r P l a n e t A t t a c k S h i p s X P o s A r r a y + $01 , Y
PHA
LDA u p p e r P l a n e t A t t a c k S h i p s M S B X P o s A r r a y + $01 , Y
PHA
LDA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y + $01 , Y
PHA
When this has run the stack of accumulator values now looks like this:
150
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
upperPlanetAttackShipsYPosArray
upperPlanetAttackShipsMSBXPosArray
upperPlanetAttackShipsXPosArray
Figure 7.6: The stack after the code above has run with upperPlanetAttackShipsXPosArray at the top.
With our position data safely stashed away on the stack we now decide which planet
we're on:
If we do \=nd one we can now pull (or 'pop') the positional data we stored away in the
stack and assign that to the once-dead ship. First we use the index we retrieved to X
to get the ship's index (Y) into the positional arrays:
LDY i n d e x I n t o U p p e r P l a n e t A t t a c k S h i p X P o s A n d Y P o s A r r a y , X
PLA
STA u p p e r P l a n e t A t t a c k S h i p s Y P o s A r r a y + $01 , Y
Listing 7.7: ""PLA remove the top item from the stack and stores it in A
The stack now looks like this, popping from the stack has the effect of removing the
\=rst item:
151
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
upperPlanetAttackShipsMSBXPosArray
upperPlanetAttackShipsXPosArray
Then we pop the rest of the items one by one and assign them to the new ship. We
ignore the sprite's MXB offset if it is zero:
PLA
BEQ M S B X P o s O f f s e t I z Z e r o
LDA a t t a c k S h i p s M S B X P o s O f f s e t A r r a y + $01 , X
MSBXPosOffsetIzZero
STA u p p e r P l a n e t A t t a c k S h i p s M S B X P o s A r r a y + $01 , Y
PLA
STA u p p e r P l a n e t A t t a c k S h i p s X P o s A r r a y + $01 , Y
PLA
Listing 7.8: PLA remove the top item from the stack and stores it in A.
Now that we have set up the positional data for the new enemy we load all its other
features from the data pointed to by Byte 8-9:
152
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Table 7.1: Actual use of Bytes 6, 7, and 8. Note that the value in Byte 6 doesn't matter,
as long as it's non-zero.
An irritating early hurdle in Iridis Alpha's gameplay is the behaviour of the lickerships
in the game's third level. When shot the enemies turn into lickerships that seek out the
player and then stick to them, sapping the player's energy until they lose a life. This
behavious is de\=ned by Bytes 22 and 23.
l i c k e r S h i p W a v e D a t a = $1118
..
; Byte 22 ( Index $16 ) : Stickiness factor , does the enemy stick to the
player
; sapping their energy if they ' re near them?
.BYTE $01
; Byte 23 ( Index $17 ) : Does the enemy gravitate quickly toward the
player when its
153
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
MaybeQuicklyGravitatesToGilby
; Byte 23: Does the enemy gravitate quickly towards the gilby when
it is shot?
LDY \# $17
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ M a y b e S t i c k y A t t a c k S h i p B e h a v i o u r
...
; Figure out the relative position of the gilby and
; store it in p o s i t i o n R e l a t i v e T o G i l b y
...
; Now decide whether to move up or down.
CMP p o s i t i o n R e l a t i v e T o G i l b y
BEQ N o V e r t i c a l M o v e m e n t R e q u i r e d
BMI M ov eD ow n To Gi lb y
MoveUpToGilby
DEC y P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p s ,X
DEC y P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p s ,X
M ov eD ow n To Gi lb y
INC y P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p s ,X
NoVerticalMovementRequired
LDA i n d e x F o r A c t i v e S h i p s W a v e D a t a ,X
TAX
Sticking to the gilby involves the same logic but along the horizontal axis. After all if
we're sticking to the gilby we want to stay in the centre of the screen.
MaybeStickyAttackShipBehaviour
; Byte 22: Does the enemy have the stickiness behaviour?
LDY \# $16
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ M a y b e S w i t c h T o A l t e r n a t e E n e m y P a t t e r n
...
; Figure out the relative position of the gilby and
; store it in p o s i t i o n R e l a t i v e T o G i l b y
...
CMP p o s i t i o n R e l a t i v e T o G i l b y
BMI M o v e R i g h t T o G i l b y
M ov eL ef t To Gi lb y
DEC x P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p ,X
DEC x P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p ,X
MoveRightToGilby
154
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
INC x P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p ,X
NoHorizontalMovementRequired
LDA i n d e x F o r A c t i v e S h i p s W a v e D a t a ,X
TAX
When an enemy is struck this byte contains the multiplier applied to the player's energy
boost:
IncreaseEnergyTopOnly
LDY \# $23
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ N o r m a l T o p E n e r g y I n c r e a s e
STA e n e r g y C h a n g e C o u n t e r
EnergyTopIncreaseLoop
JSR I n c r e a s e E n e r g y T o p
DEC e n e r g y C h a n g e C o u n t e r
BNE E n e r g y T o p I n c r e a s e L o o p
RTS
NormalTopEnergyIncrease
JMP I n c r e a s e E n e r g y T o p
; Returns
The above is for the top planet energy counter, the logic for the bottom planet is iden-
tical:
IncreaseEnergyBottomOnly
LDY \# $23
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ N o r m a l B o t t o m E n e r g y I n c r e a s e
STA e n e r g y C h a n g e C o u n t e r
EnergyBottomIncreaseLoop
JSR I n c r e a s e E n e r g y B o t t o m
DEC e n e r g y C h a n g e C o u n t e r
BNE E n e r g y B o t t o m I n c r e a s e L o o p
RTS
NormalBottomEnergyIncrease
JMP I n c r e a s e E n e r g y B o t t o m
; Returns
155
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Actually writing the updated energy level to the screen uses the tileset above in the
routine IncreaseEnergyTop:
IncreaseEnergyTop
STX t e m p o r a r y S t o r a g e F o r X R e g i s t e r
LDX currEnergyTop
DEC SCREEN\.RAM + LINE22\.COL3 ,X
LDA SCREEN\.RAM + LINE22\.COL3 ,X
CMP \# $7F
BNE b547B
; Note the reference to the index $80 of the first tile in the set
above.
LDA \# $80
STA SCREEN\.RAM + LINE22\.COL3 ,X
INX
STX currEnergyTop
CPX \# $08
BEQ G i l b y D i e s F r o m E x c e s s E n e r g y
LDA \# $87
STA SCREEN\.RAM + LINE22\.COL3 ,X
BNE b547B
Byte 35 also determines the multiplier applied to the energy sapped from the player in
the event of a collision with the enemy:
UpdateEnergyLevelsAfterCollision
; Check if the enemy saps energy from the gilby?
LDY \# $23
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ L o a d E x p l o s i o n D a t a
LDA \# !` s h i p C o l l i d e d W i t h G i l b y S o u n d
STA primarySoundEffectLoPtr
LDA \# ?` s h i p C o l l i d e d W i t h G i l b y S o u n d
STA primarySoundEffectHiPtr
JSR ResetRepetitionForPrimarySoundEffect
LDA \# $0E
STA gilbyEx ploding
LDA \# $02
STA s t a r F i e l d I n i t i a l S t a t e A r r a y - $01
LDA currentGilbySpeed
EOR \# $FF
CLC
156
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
ADC \# $01
STA c u r r e n t G i l b y S p e e d
LDA s e t T o Z e r o I f O n U p p e r P l a n e t
BEQ E n e r g y U p d a t e T o p P l a n e t
LDA extraAmountToDecreaseEnergyByBottomPlanet
BNE LoadExplosionData
; Y is still $23.
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
JSR AugmentAmountToDecreaseEnergyByBountiesEarned
STA extraAmountToDecreaseEnergyByBottomPlanet
BNE LoadExplosionData
EnergyUpdateTopPlanet
LDA e x t r a A m o u n t T o D e c r e a s e E n e r g y B y T o p P l a n e t
BNE L o a d E x p l o s i o n D a t a
; Y is still $23.
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
JSR A u g m e n t A m o u n t T o D e c r e a s e E n e r g y B y B o u n t i e s E a r n e d
STA e x t r a A m o u n t T o D e c r e a s e E n e r g y B y T o p P l a n e t
LoadExplosionData
LDY \# $1E
JMP U p d a t e W a v e D a t a P o i n t e r s F o r C u r r e n t E n e m y
; Returns
This is used to augment the score received for hitting the enemy.
157
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Bytes 9 to 11 were intended to carry out a clever plan that was probably too clever for
its own good and as a result ended up being dropped. The idea was that Bytes 9 and
10 would point to Bytes 18 to 21 in another set of level data, enabling the enemy to have
a second phase of movement behaviour in addition to its native movement.
UpdateAttackShipDataForNewShip
; Byte 10:
LDY \#10
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ M a y b e Q u i c k l y G r a v i t a t e s T o G i l b y
UnusedRoutine
; As above , this section is never reached because Byte 10 is never
set.
DEC s o m e K i n d O f R a t e L i m i t i n g F o r A t t a c k W a v e s ,X
BNE M a y b e Q u i c k l y G r a v i t a t e s T o G i l b y
158
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
INY
LDA i n d e x F o r A c t i v e S h i p s W a v e D a t a ,X
TAX
TYA
STA i n d e x F o r Y P o s M o v e m e n t F o r U p p e r P l a n e t A t t a c k S h i p s , X
JMP M a y b e Q u i c k l y G r a v i t a t e s T o G i l b y
Since Bytes 9 to 11 are zero in all level data, the routine is never run. On balance the
idea does seem unnecessarily complex: why not just load a completely new set of level
data with modi\=ed behaviour and the same sprite? This uses up more space overall
but it is certainly easier to manage. Indeed, much of the level data is given over to
chaining level data in this way. Let's look at that now.
159
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Byte 15 de\=nes a value that is periodically decremented and when it reaches zero the
level data pointed to by Bytes 16 and 17 is loaded. This is used in the very \=rst level to
switch the main sprite color of the flying UFOs.
GetWaveDataForNewShip
..
; Byte 15; Update Rate for Attack Waves
LDY \#15
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
STA u p d a t e R a t e F o r A t t a c k S h i p s ,X
DEC u p d a t e R a t e F o r A t t a c k S h i p s ,X
BNE U p d a t e A t t a c k S h i p D a t a F o r N e w S h i p
; Byte 14: Controls the rate at which new enemies are added.
; This is only set when the current ship data is d e f a u lt E x p l o s i o n
; so in most cases we will go straight to S w i t c h T o A l t e r n a t i n g W a v e D a t a
.
LDY \#14
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ S w i t c h T o A l t e r n a t i n g W a v e D a t a
This now pulls in Bytes 16 and 17 from the level data and uses them as the address of
the level data to switch to:
SwitchToAlternatingWaveData
LDY \#16
; -------------------------------------------------------
; UpdateWaveDataPointersForCurrentEnemy
; -------------------------------------------------------
UpdateWaveDataPointersForCurrentEnemy
; Byte 16 Y has been set to 16 above , so we ' re pulling in the
160
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
pointer
; to the second tranche of wave data for this level.
; Or Y has been set by the caller.
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
PHA
INY
; Byte 17
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
STA a c t i v e S h i p s W a v e D a t a H i P t r A r r a y ,X
STA currentShipWaveDataHiPtr
PLA
STA currentShipWaveDataLoPtr
STA a c t i v e S h i p s W a v e D a t a L o P t r A r r a y ,X
The effect therefore is that the main sprite color changes (although the sprite stays
the same) and when the periodic decrementing of Byte 15 reaches zero again it will
switch back to planet1Level1Data. This switching back and forth will continue for as
long as the enemy does not collide with anything or is not hit.
161
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Bytes 28 and 29 de\=ne the level data to load when struck by a bullet. In the case of our
flying UFOs this is the spinning rings.
UpdateScoresAfterHittingShipWithBullet
..
; Byte 29: Load the explosion animation , if there is one. For most
; enemies this is the spinning rings defined by spinningRings .
LoadExplosionAnimation
162
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
LDY \#29
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ C h e c k F o r C o l l i s i o n s B e f o r e U p d a t i n g C u r r e n t S h i p s W a v e D a t a
; There 's a Hi Ptr for the explosion animation , so decrement
; Y to point it at the Lo Ptr and load the ptrs as the new
; wave data for the enemy.
DEY
JMP U p d a t e W a v e D a t a P o i n t e r s F o r C u r r e n t E n e m y
; Returns
This is also the path used to load the licker ships after the relatively inoffensive flying
blue dots have been struck by a bullet.
Bytes 30 and 31 de\=ne the level data to load when the enemy has collided with the
player. In the case of our flying UFOs this is the default explosion animation.
163
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
These bytes are only inspected when responding to a collision. The routine Update-
EnergyLevelsAfterCollision \=rst establishes if the enemy saps energy from the
gilby by inspecting Byte 35. Once the updated player energy is calculated the collision
explosion data is loaded.
UpdateEnergyLevelsAfterCollision
; Byte 35: Check if the enemy saps energy from the gilby?
LDY \#35
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ L o a d E x p l o s i o n D a t a
LDA \# !` s h i p C o l l i d e d W i t h G i l b y S o u n d
STA primarySoundEffectLoPtr
LDA \# ?` s h i p C o l l i d e d W i t h G i l b y S o u n d
STA primarySoundEffectHiPtr
JSR ResetRepetitionForPrimarySoundEffect
LDA \# $0E
STA gilbyEx ploding
LDA \# $02
STA s t a r F i e l d I n i t i a l S t a t e A r r a y - $01
LDA currentGilbySpeed
EOR \# $FF
CLC
ADC \# $01
STA currentGilbySpeed
LDA s e t T o Z e r o I f O n U p p e r P l a n e t
BEQ E n e r g y U p d a t e T o p P l a n e t
LDA extraAmountToDecreaseEnergyByBottomPlanet
BNE LoadExplosionData
; Y is still 35 .
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
JSR AugmentAmountToDecreaseEnergyByBountiesEarned
STA extraAmountToDecreaseEnergyByBottomPlanet
BNE LoadExplosionData
EnergyUpdateTopPlanet
164
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
LDA extraAmountToDecreaseEnergyByTopPlanet
BNE LoadExplosionData
; Y is still 35 .
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
JSR AugmentAmountToDecreaseEnergyByBountiesEarned
STA extraAmountToDecreaseEnergyByTopPlanet
LoadExplosionData
; Byte 30: Hi / Lo Ptr for the collision explosion level data.
LDY \#30
JMP U p d a t e W a v e D a t a P o i n t e r s F o r C u r r e n t E n e m y
; Returns
Rather than exploding when \=rst hit, some enemy waves are con\=gured to metamor-
phose into something else, so they end up requiring multiple hits before they oblige
the player by \=nally exploding.
The level data accomodates up to four incarnations in total for an enemy wave.
GetNewShipDataFromDataStore
LDA h a s R e a c h e d S e c o n d W a v e A t t a c k S h i p s ,X
BEQ No2ndWaveData
LDA \# $00
STA h a s R e a c h e d S e c o n d W a v e A t t a c k S h i p s ,X
DEY
JMP U p d a t e W a v e D a t a P o i n t e r s F o r C u r r e n t E n e m y
No2ndWaveData
165
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
LDA h a s R e a c h e d T h i r d W a v e A t t a c k S h i p s ,X
BEQ No3rdWaveData
LDA \# $00
STA h a s R e a c h e d T h i r d W a v e A t t a c k S h i p s ,X
DEY
JMP U p d a t e W a v e D a t a P o i n t e r s F o r C u r r e n t E n e m y
No3rdWaveData
LDA joystickInput
AND \# $10
BNE No4thWaveData
; Fire is pressed.
; Byte 33: Check if we should load fourth wave stage data for this
enemy.
LDY \#33
LDA ( c u r r e n t S h i p W a v e D a t a L o P t r ) ,Y
BEQ No4thWaveData
DEY
JMP U p d a t e W a v e D a t a P o i n t e r s F o r C u r r e n t E n e m y
; Returns
In the table on the following page we can see the relatively spare use made of this
functionality. Most of the planets only use one of the waves in 6 or 7 levels. What's
notable is that that only one level makes use of both Bytes 24-25 and Bytes 26-27
together: Level 12 on the Om Planet. This is also the only level that attempts to make
use of Bytes 32-23.
166
CHAPTER 7. ENEMIES AND THEIR DISCONTENTS
Mushroom Planet
1 nullPtr planet4Level1Data2ndStage nullPtr
5 nullPtr planet4Level5Data2ndStage nullPtr
9 nullPtr planet4Level9Data2ndStage nullPtr
18 nullPtr planet4Level18Data nullPtr
19 nullPtr planet4Level19Data nullPtr
20 copticExplosion nullPtr nullPtr
Om Planet
2 nullPtr planet5Level2Data nullPtr
3 nullPtr planet5Level3Data nullPtr
8 nullPtr planet5Level8Data nullPtr
10 defaultExplosion nullPtr nullPtr
12 planet1Level5Data planet5Level12Data planet5Level12Data2ndStage
17 nullPtr planet5Level17Data nullPtr
20 copticExplosion nullPtr nullPtr
Bytes 24-25: Pointer for second wave of attack ships.
Bytes 26-27: Pointer for third wave of attack ships.
Bytes 32-33: Pointer for fourth wave of attack ships.
Figure 7.20: Use of the Enemy Phases Bytes by Each Planet. Levels that make no use of them are excluded.
167
Congoatulations Hotshot
168
CHAPTER 8. CONGOATULATIONS HOTSHOT
The entry sequence is an animated cascade of colored bars that appears to roll down
from the top of the screen. You might assume we achieve this effect by simply drawing
a series of colored text-based lines in a tight loop. Not the case:
EnterBonusPhaseInterruptHandler
...
LDY b a c k g r o u n d C o l o r I n d e x
LDA e n t e r B P R a i n b o w C o l o r s ,Y
STA $D021 ; Background Color 0
What we actually do is update the screen's background color as the raster travels down
the screen. The above three lines do this about 30 times every single paint of the
screen, updating the background color that gets painted as we go. The result is that
each colored bar reflects the background color of the screen at the time the raster is
passing it. The trick is to keep updating the background color at gradually increasing
intervals.
After we update the background color we set the raster interrupt to the next position
we're interested in:
169
CHAPTER 8. CONGOATULATIONS HOTSHOT
EnterBonusPhaseInterruptHandler
...
LDY b a c k g r o u n d C o l o r I n d e x
LDA e n t e r B P R a i n b o w C o l o r s ,Y
STA $D021 ; Background Color 0
The value we get from bpRasterPositionArray reflects our intention of painting in-
creasingly tall bars:
bpRasterPositionArray .BYTE $01 , $01 , $01 , $01 , $02 , $02 , $02 , $02
.BYTE $03 , $03 , $03 , $03 , $04 , $04 , $04 , $04
.BYTE $05 , $05 , $05 , $05 , $06 , $06 , $06 , $06
.BYTE $07 , $07 , $07 , $07 , $07 , $07 , $00
Each value in here gets added to the current interrupt position (ADC $D012). The further
we go down the screen the taller the bars become.
Figure 8.3: Updating the background color at a 5 line interval as given by bpRasterPositionArray.
In addition to drawing incrementally larger bars we're also shifting down by one row
the color each one is painted at each pass. This is responsible for creating the visual
effect of each bar moving independently down the screen.
170
CHAPTER 8. CONGOATULATIONS HOTSHOT
Figure 8.4: The color sequences shifts down one position after each full screen paint.
entryScreenRainbowColors
.BYTE RED , ORANGE , YELLOW , GREEN , LTBLUE , PURPLE , BLUE , YELLOW
.BYTE BLACK , CYAN , BLACK , GREEN , BLACK , PURPLE , BLACK , RED
.BYTE BLACK , BLUE , BLACK , BLUE , BLACK , BLUE , PURPLE , LTBLUE
.BYTE GREEN , YELLOW , ORANGE , RED
UpdateEntryScreenRainbow
...
LDA e n t r y S c r e e n R a i n b o w C o l o r s ,X
STA e n t e r B P R a i n b o w C o l o r s ,Y
TYA
STA p r e v i o u s R o u n d R a i n b o w C o l o r s ,X
All in all, this is a neat effect and would only have been possible at the speeds achieved
by using the raster interrupt in this way. Simply painting the screen with colored text
would have been much slower.
171
CHAPTER 8. CONGOATULATIONS HOTSHOT
Every time the player enters the bonus phase we'll procedurally generate a new map.
The way we'll manage this is by treating the map a stack of 256 rows and de\=ning our
map simply as an array of the rows that make up the map.
172
CHAPTER 8. CONGOATULATIONS HOTSHOT
Figure 8.6: The \=rst seven maps used by the bonus phase round.
173
CHAPTER 8. CONGOATULATIONS HOTSHOT
This means that in order to generate a new map all we have to do is come up with an
array of bytes where each byte de\=nes a row in the map. By way of example, this is
what the array that de\=nes the map for the \=rst bonus phase round looks like:
bonusPhaseMapDefinition
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $14 , $14 , $00 , $15 , $16 , $17 , $00 , $00 , $14 , $14
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $14 , $14 , $00 , $15 , $16 , $17 , $00 , $00 , $14 , $14
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $00
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $10 , $10 , $10 , $10 , $10 , $10
This map de\=nition starts from the bottom up, it does this because we are scrolling
upward so it makes sense to begin with what the player will see \=rst. The start of the
map is a simple sequence of zeroes:
bonusPhaseMapDefinition
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
This is the row image each line translates to, a regular square pattern initially followed
by rows with some other features.
174
CHAPTER 8. CONGOATULATIONS HOTSHOT
Index Image
$00
$00
$00
$00
$00
$00
$00
$00
Figure 8.7: Snapshot of the map created by the \=rst line de\=nition.
Further along in the de\=nition we reach a segment where some new features are intro-
duced.
bonusPhaseMapDefinition
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
We can see what each of $11,$15,$16 translate to when we map them to their corre-
sponding images:
175
CHAPTER 8. CONGOATULATIONS HOTSHOT
Index Image
$16
$16
$16
$15
$11
$11
$11
Figure 8.8: Snapshot of the map created by the \=rst line de\=nition.
So how do we get from a value like $15 to an image that represents a row on the screen?
Index Image
$15
We do this by de\=ning each row with yet another array of bytes. In all we de\=ne 32
rows of different types so when we come up with a map for the level we are simply
referencing each of these rows by their index in the array. Here is the data structure
de\=ning each of the 32 rows:
bonusPhaseMapRowDefinitions
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 ; 00
.BYTE $0D , $0D , $0E , $0E , $0E , $0E , $0E , $00 , $00 , $0B , $0B , $00 , $00 , $0D , $0D , $0D , $0D , $0D , $0E , $0E
.BYTE $10 , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $11 , $10 , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $11
.BYTE $0E , $10 , $0B , $0B , $0B , $0B , $0B , $0B , $11 , $0D , $0E , $10 , $0B , $0B , $0B , $0B , $0B , $0B , $11 , $0D
.BYTE $0E , $0E , $10 , $0B , $0B , $0B , $0B , $11 , $0D , $0D , $0E , $0E , $10 , $0B , $0B , $0B , $0B , $11 , $0D , $0D
.BYTE $0E , $0E , $0E , $00 , $00 , $00 , $00 , $0D , $0D , $0D , $0E , $0E , $0E , $00 , $00 , $00 , $00 , $0D , $0D , $0D
.BYTE $0E , $0E , $0E , $00 , $0A , $0A , $00 , $0D , $0D , $0D , $0E , $0E , $0E , $00 , $0A , $0A , $00 , $0D , $0D , $0D
.BYTE $0E , $0E , $0E , $00 , $09 , $09 , $00 , $0D , $0D , $0D , $0E , $0E , $0E , $00 , $09 , $09 , $00 , $0D , $0D , $0D
.BYTE $0E , $0E , $12 , $0C , $0C , $0C , $0C , $13 , $0D , $0D , $0E , $0E , $12 , $0C , $0C , $0C , $0C , $13 , $0D , $0D
.BYTE $0E , $07 , $00 , $00 , $15 , $15 , $00 , $00 , $05 , $0D , $0E , $07 , $00 , $00 , $15 , $15 , $00 , $00 , $05 , $0D
.BYTE $07 , $0F , $00 , $00 , $15 , $15 , $00 , $00 , $0F , $05 , $07 , $0F , $00 , $00 , $15 , $15 , $00 , $00 , $0F , $05
.BYTE $17 , $17 , $00 , $00 , $18 , $17 , $00 , $00 , $18 , $18 , $17 , $17 , $00 , $00 , $18 , $17 , $00 , $00 , $18 , $18
.BYTE $0F , $0F , $0F , $00 , $00 , $00 , $00 , $0F , $0F , $0F , $0F , $0F , $0F , $00 , $00 , $00 , $00 , $0F , $0F , $0F
.BYTE $00 , $00 , $09 , $09 , $00 , $00 , $09 , $09 , $00 , $00 , $00 , $00 , $09 , $09 , $00 , $00 , $09 , $09 , $00 , $00
.BYTE $00 , $00 , $0A , $0A , $00 , $00 , $0A , $0A , $00 , $00 , $00 , $00 , $0A , $0A , $00 , $00 , $0A , $0A , $00 , $00
.BYTE $0F , $0F , $0F , $0F , $0F , $0F , $0F , $0F , $0F , $00 , $00 , $0F , $0F , $0F , $0F , $0F , $0F , $0F , $0F , $0F ; 0F
.BYTE $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14
.BYTE $0F , $0E , $0E , $0E , $0E , $0E , $0E , $0E , $0E , $0E , $0D , $0D , $0D , $0D , $0D , $0D , $0D , $0D , $0D , $0F
.BYTE $0B , $0B , $0B , $00 , $0B , $0B , $0B , $0B , $0B , $0F , $0F , $0B , $0B , $0B , $0B , $0B , $00 , $0B , $0B , $0B
.BYTE $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15 , $15
.BYTE $00 , $00 , $00 , $0F , $0F , $0F , $00 , $00 , $00 , $0F , $0F , $00 , $00 , $00 , $0F , $0F , $0F , $00 , $00 , $00
176
CHAPTER 8. CONGOATULATIONS HOTSHOT
.BYTE $00 , $10 , $0B , $11 , $00 , $00 , $10 , $0B , $11 , $00 , $00 , $10 , $0B , $11 , $00 , $00 , $10 , $0B , $11 , $00 ; 15
.BYTE $00 , $0E , $00 , $0D , $00 , $00 , $0E , $00 , $0D , $00 , $00 , $0E , $00 , $0D , $00 , $00 , $0E , $00 , $0D , $00
.BYTE $00 , $12 , $0C , $13 , $00 , $00 , $12 , $0C , $13 , $00 , $00 , $12 , $0C , $13 , $00 , $00 , $12 , $0C , $13 , $00
.BYTE $0F , $0E , $00 , $00 , $00 , $00 , $0D , $0F , $0F , $0F , $0F , $0F , $0F , $0E , $0B , $0B , $0B , $0B , $0D , $0F
.BYTE $0F , $0E , $0B , $0B , $0B , $0B , $0D , $0F , $0F , $0F , $0F , $0F , $0F , $0E , $00 , $00 , $00 , $00 , $0D , $0F
.BYTE $0F , $0E , $00 , $00 , $00 , $00 , $0D , $0F , $0F , $0F , $0F , $0F , $0F , $0E , $00 , $00 , $00 , $00 , $0D , $0F
.BYTE $15 , $15 , $00 , $00 , $16 , $16 , $00 , $00 , $15 , $15 , $00 , $00 , $16 , $16 , $00 , $00 , $15 , $15 , $00 , $00
.BYTE $00 , $00 , $16 , $16 , $00 , $00 , $15 , $15 , $00 , $00 , $16 , $16 , $00 , $00 , $15 , $15 , $00 , $00 , $16 , $16
.BYTE $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B , $0B
.BYTE $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C , $0C
.BYTE $09 , $09 , $09 , $09 , $09 , $09 , $09 , $09 , $09 , $15 , $15 , $09 , $09 , $09 , $09 , $09 , $09 , $09 , $09 , $09
.BYTE $0A , $0A , $0A , $0A , $0A , $0A , $0A , $0A , $0A , $15 , $15 , $0A , $0A , $0A , $0A , $0A , $0A , $0A , $0A , $0A
bonusPhaseMapRowDefinitions
.BYTE $00 , $10 , $0B , $11 , $00 , $00 , $10 , $0B , $11 , $00 , $00 , $10 , $0B , $11 , $00 , $00 , $10 , $0B , $11 , $00
Each byte in this de\=nition represents a square cell of four bytes. So for example, $00
translates to:
And the \=rst \=ve bytes (which are repeated four times to complete the full row) trans-
late as follows:
So how do we go from a single byte like $00 to a four-byte square? Would you be
surprised if I told you it involved another array of bytes? In fact it involves two arrays
of bytes.
cellFirstColumnArray
.BYTE $40 , $41 , $44 , $47 , $48 , $49 , $4F , $4D
.BYTE $50 , $51 , $54 , $56 , $5B , $59 , $5C , $5D
.BYTE $60 , $61 , $64 , $65 , $68 , $69 , $47 , $47
.BYTE $4E , $4E , $57 , $57 , $5D , $5D , $20 , $20
.BYTE $5D , $45 , $4B , $47 , $4C , $5D , $4E , $52
.BYTE $7C , $7D , $6C , $6D , $70 , $71 , $74 , $75
177
CHAPTER 8. CONGOATULATIONS HOTSHOT
.. using the two arrays above. The value $00 is treated as in index into each array, so
it points us to the \=rst byte in each. However we're not interested in just the \=rst value
in each array but the \=rst two. So the bytes that we will use to construct the 4-byte
square are:
cellFirstColumnArray
.BYTE $40 , $41
cellSecondColumnArray
.BYTE $42 , $43
Each of these bytes is a reference to a byte in the bonus phase character set. When we
set out the characters in a table as they are eventually laid out we begin to get a sense
of what we must do to turn them into our 4-byte cell:
178
CHAPTER 8. CONGOATULATIONS HOTSHOT
$40 $42
$41 $43
Figure 8.10: Characters making up the four-byte cell referenced by $00.
What we have done is take the two values at index $00 in cellFirstColumnArray and
used each as the \=rst column across two rows. Then we've taken the two values from
cellSecondColumnArray and used them as the second column across two rows.
Let's repeat this process for the values given by index $10 highlighted in red.
cellFirstColumnArray
.BYTE $40 , $41 , $44 , $47 , $48 , $49 , $4F , $4D
.BYTE $50 , $51 , $54 , $56 , $5B , $59 , $5C , $5D
.BYTE $60 , $61 , $64 , $65 , $68 , $69 , $47 , $47
...
cellSecondColumnArray
.BYTE $42 , $43 , $46 , $47 , $4A , $48 , $4E , $4F
.BYTE $51 , $53 , $56 , $57 , $5A , $5B , $5E , $5C
.BYTE $61 , $63 , $66 , $67 , $6A , $6B , $47 , $47
...
179
CHAPTER 8. CONGOATULATIONS HOTSHOT
$60 $61
$61 $63
Figure 8.11: Characters making up the four-byte cell referenced by $10.
in the row:
The code that looks after all this is a routine we call BonusPhaseFillTopLineAfterScrollUp
which is called every time the player scrolls up. There's an equivalent
BonusPhaseFillBottomLineAfterScrollDown for when the player scrolls down. They're
nearly identical.
180
CHAPTER 8. CONGOATULATIONS HOTSHOT
BonusPhaseFillBottomLineAfterScrollDown
LDX o f f s e t F o r S c r o l l D o w n
LDY bonusPhaseMapDefinition , X
LDA bonusPhaseMapLoPtrArray , Y
STA bonusPhaseMapLoPtr
LDA bonusPhaseMapHiPtrArray , Y
STA bonusPhaseMapHiPtr
B o n u s P h a s e F i l l To p L i n e A f t e r S c r o l l U p LDY \#$00
LDX o f f s e t F o r S c r o l l U p LDX \#$00
LDY bonusPhaseMapDefinition , X FillRowLoop
LDA bonusPhaseMapLoPtrArray , Y LDA ( bonusPhaseMapLoPtr ) , Y
STA bonusPhaseMapLoPtr STY mapOffsetTemp
LDA bonusPhaseMapHiPtrArray , Y ASL
STA bonusPhaseMapHiPtr CLC
ADC scrollLineOffset
LDY \#$00 TAY
LDX \#$00 LDA cellFirstColumnArray ,Y
FillRowLoop STA SCREEN RAM + LINE18 COL0 , X
LDA ( bonusPhaseMapLoPtr ) , Y LDA cellSecondColumnArray , Y
STY mapOffsetTemp STA SCREEN RAM + LINE18 COL1 , X
ASL LDY mapOffsetTemp
CLC INX
ADC scrollLineOffset INX
TAY INY
LDA cellFirstColumnArray ,Y CPY \#$14
STA SCREEN RAM , X BNE FillRowLoop
LDA cellSecondColumnArray , Y
STA SCREEN RAM + LINE0 COL1 , X LDA s c r o l l L i n e O f f s e t
LDY mapOffsetTemp BEQ FillRowLoop
INX
INX DEC offsetForScrollDown
INY DEC offsetForScrollUp
CPY \#$14 LDA offsetForScrollDown
BNE FillRowLoop CMP \#$FF
BNE ReturnEarly
LDA s c r o l l L i n e O f f s e t
BNE R e t u r n E a r l y LDA \#$00
STA offsetForScrollDown
INC o f f s e t F o r S c r o l l U p LDA \#$0A
INC o f f s e t F o r S c r o l l D o w n STA offsetForScrollUp
ReturnEarly ReturnEarly
RTS RTS
The thing to note about this routine is that it only \=lls one actual line of characters at
a time, whereas as the 'rows' we've de\=ned are two lines deep. It decides which of the
two lines its writing by using the scrollLineOffset variable to determine which one
its writing.
So now that we understand how the individual rows of the map are generated, the
question arises: how do we procedurally generate entire maps? Do we just pick random
rows and join them together? This wouldn't work well, since some rows aren't going
to go well together. The solution is to de\=ne entire map segments using the building
blocks above and let those be the building blocks we use when constructing an entire
map.
If we look at our de\=nition for the \=rst bonus phase again we can see it consists of
arrays of 10 bytes with each 10-byte array corresponding to a segment in the map and
each byte in the array corresponding to a row in the map. So each 10-byte array below
gives us a full 20 byte high screen of map data.
181
CHAPTER 8. CONGOATULATIONS HOTSHOT
bonusPhaseMapDefinition
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $14 , $14 , $00 , $15 , $16 , $17 , $00 , $00 , $14 , $14
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $14 , $14 , $00 , $15 , $16 , $17 , $00 , $00 , $14 , $14
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $00
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $10 , $10 , $10 , $10 , $10 , $10
The trick is that these segments aren't themselves generated procedurally, we de\=ned
them in advance. We did this in bonusMapSegmentArray given below. We've highlighted
the segments used in our map in red:
bonusMapSegmentArray
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $00 , $15 , $16 , $17 , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $00 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $14 , $00
.BYTE $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11 , $11
.BYTE $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13 , $13
.BYTE $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $12 , $00
.BYTE $14 , $14 , $00 , $15 , $16 , $17 , $00 , $00 , $14 , $14
.BYTE $15 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $16 , $17
.BYTE $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00 , $00
.BYTE $00 , $00 , $0F , $0F , $0F , $0F , $0F , $0F , $00 , $00
.BYTE $01 , $01 , $01 , $01 , $00 , $00 , $01 , $01 , $01 , $01
.BYTE $00 , $00 , $0B , $0B , $0B , $0C , $0C , $0C , $00 , $00
.BYTE $00 , $02 , $03 , $04 , $05 , $06 , $07 , $08 , $09 , $0A
.BYTE $02 , $03 , $04 , $05 , $05 , $05 , $05 , $0B , $0B , $0B
.BYTE $00 , $00 , $01 , $01 , $00 , $00 , $01 , $01 , $00 , $00
.BYTE $00 , $00 , $0E , $0D , $00 , $00 , $0E , $0D , $00 , $00
.BYTE $00 , $02 , $03 , $04 , $05 , $08 , $09 , $0A , $0B , $00
.BYTE $00 , $00 , $00 , $1A , $1A , $1A , $18 , $18 , $18 , $18
.BYTE $00 , $00 , $00 , $1A , $1A , $1A , $19 , $19 , $19 , $19
182
CHAPTER 8. CONGOATULATIONS HOTSHOT
.BYTE $00 , $00 , $18 , $18 , $00 , $00 , $00 , $00 , $19 , $19
.BYTE $00 , $00 , $1B , $1B , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $15 , $16 , $17 , $1D , $1D , $15 , $16 , $17 , $1D , $1D
.BYTE $14 , $14 , $1E , $1E , $00 , $00 , $15 , $16 , $17 , $00
.BYTE $00 , $0B , $0B , $0B , $15 , $16 , $17 , $15 , $16 , $17
.BYTE $00 , $00 , $1D , $1D , $1D , $1D , $1E , $1E , $1E , $1E
.BYTE $00 , $00 , $20 , $1F , $20 , $1F , $00 , $00 , $11 , $11
.BYTE $00 , $00 , $20 , $1F , $20 , $1F , $20 , $1F , $20 , $1F
.BYTE $00 , $1E , $1E , $1E , $20 , $1F , $1D , $1D , $1D , $00
.BYTE $00 , $0C , $0C , $0C , $15 , $16 , $17 , $00 , $00 , $00
.BYTE $00 , $02 , $03 , $04 , $05 , $08 , $09 , $0A , $0B , $00
.BYTE $00 , $00 , $06 , $06 , $06 , $11 , $11 , $11 , $00 , $00
.BYTE $00 , $00 , $0F , $0F , $15 , $16 , $17 , $15 , $16 , $17
By way of example this is what the last segment in the list above looks like when ren-
dered as a section of our map:
Index Image
$17
$16
$15
$17
$16
$15
$0F
$0F
$00
$00
183
CHAPTER 8. CONGOATULATIONS HOTSHOT
For some reason the gilby sprite in the bonus phase is made impossibly ugly.
8.4 IBalls
184
A Hundred Thousand Billion Theme
Tunes
The theme music in Iridis Alpha is procedurally generated. There isn't a chunk of music
data that the game plays every time you visit the title screen. Instead a new tune is
generated for every visit. There's a distinction to be made here between procedural
and random. The music isn't random: the \=rst time you launch Iridis Alpha, and every
subsequent time you launch it, you will hear the same piece of music. But as you let
the game's attract mode cycle through and return to the title screen you will hear a
new, different piece of music. Iridis Alpha has an in\=nite number of these tunes and it
plays them in the same order every time you launch it and as it loops through the title
sequence waiting for you to play.
Because the music is generated procedurally, and not randomly, you will hear the same
sequence of tunes every time you launch the game so it appears to you as if the music
was composed in advance and stored in the game waiting its turn. This is not the case.
Each piece of music is generated dynamically using the same algorithm but because
the logic is chaotic enough, the smallest difference in the initial values fed into it will
result in a completely different tune being generated.
The routine responsible for creating this music is remarkably short so I've reproduced
it here in full before we start to dive in and try to understand what's going on.
PlayTitleScreenMusic
DEC b a s e N o t e D u r a t i o n
BEQ M a y b e S t a r t N e w T u n e
RTS
MaybeStartNewTune
185
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
LDA p r e v i o u s B a s e N o t e D u r a t i o n
STA b a s e N o t e D u r a t i o n
DEC n u m b e r O f N o t e s T o P l a y I n T u n e
BNE M ay be P la yV oi c e1
LDX n o t e s P l a y e d S i n c e L a s t K e y C h a n g e
LDA t i t l e M u s i c N o t e A r r a y ,X
STA o f f s e t F o r N e x t V o i c e 1 N o t e
JSR S e l e c t N e w N o t e s T o P l a y
M ay be Pl a yV oi ce 1
DEC v o i c e 1 N o t e D u r a t i o n
BNE M ay be P la yV oi c e2
LDA \# $30
STA v o i c e 1 N o t e D u r a t i o n
LDX voice1IndexToMusicNoteArray
LDA t i t l e M u s i c N o t e A r r a y ,X
CLC
ADC offsetForNextVoice1Note
TAY
STY offsetForNextVoice2Note
INX
TXA
AND \# $03
STA v o i c e 1 I n d e x T o M u s i c N o t e A r r a y
M ay be Pl a yV oi ce 2
DEC v o i c e 2 N o t e D u r a t i o n
BNE M ay be P la yV oi c e3
LDA \# $0C
186
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
STA voice2NoteDuration
LDX voice2IndexToMusicNoteArray
LDA t i t l e M u s i c N o t e A r r a y ,X
CLC
ADC offsetForNextVoice2Note
; Use this new value to change the key of the next four
; notes played by voice 3 .
STA o f f s e t F o r N e x t V o i c e 3 N o t e
TAY
JSR PlayNot eVoice2
INX
TXA
AND \# $03
STA v o i c e 2 I n d e x T o M u s i c N o t e A r r a y
M ay be Pl a yV oi ce 3
DEC v o i c e 3 N o t e D u r a t i o n
BNE R e t u r n F r o m T i t l e S c r e e n M u s i c
LDA \# $03
STA v o i c e 3 N o t e D u r a t i o n
ReturnFromTitleScreenMusic
RTS
187
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
The rudiments of playing music on the Commodore 64 are simple. It has a powerful-
for-its-time sound chip that has 3 tracks or 'voices'. You can play any note across 8
octaves on each of these voices together or separately. There are a whole bunch of
settings you can apply to each voice to determine the way the note sounds. We'll cover
a couple of these settings here but when it comes to playing music these extra settings
aren't so important. They're much more useful when generating sound effects.
Playing a note on one of the voices consists of loading a two-byte value into the loca-
tion (or 'register') associated with that voice. Here's the routine in Iridis used to play a
note for the theme tune on Voice '1':
PlayN oteVoic e1
LDA \# $21
STA $D404 ; Voice 1: Control Register
LDA t i t l e M u s i c L o w B y t e s ,Y
STA $D400 ; Voice 1: Frequency Control - Low - Byte
LDA t i t l e M u s i c H i B y t e s ,Y
STA $D401 ; Voice 1: Frequency Control - High - Byte
RTS
Listing 9.2: Plays a note on Voice 1. The routine is supplied with a value in Y that indexes into two arrays
containing the \=rst (Hi) and second (Lo) byte respectively associated with the selected note.
Once the selected bytes have been loaded into $D400 and $D401 the new note will start
playing. It's as blunt an instrument as that. (Well not quite, we'll cover some other gory
details soon).
The full list of available notes is given in the C64 Progammer's Reference Manual. I've
adapted and reproduced it below.
188
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Octave Note High Byte Low Byte Octave Note High Byte Low Byte Octave Note High Byte Low Byte
0 C $01 $0C 2 G\# $06 $A7 5 E $2A $3E
0 C\# $01 $1C 2 A $07 $0C 5 F $2C $C1
0 D $01 $2D 2 A\# $07 $77 5 F\# $2F $6B
0 D\# $01 $3E 2 B $07 $E9 5 G $32 $3C
0 E $01 $51 3 C $08 $61 5 G\# $35 $39
0 F $01 $66 3 C\# $08 $E1 5 A $38 $63
0 F\# $01 $7B 3 D $09 $68 5 A\# $3B $BE
0 G $01 $91 3 D\# $09 $F7 5 B $3F $4B
0 G\# $01 $A9 3 E $0A $8F 6 C $43 $0F
0 A $01 $C3 3 F $0B $30 6 C\# $47 $0C
0 A\# $01 $DD 3 F\# $0B $DA 6 D $4B $45
0 B $01 $FA 3 G $0C $8F 6 D\# $4F $BF
1 C $02 $18 3 G\# $0D $4E 6 E $54 $7D
1 C\# $02 $38 3 A $0E $18 6 F $59 $83
1 D $02 $5A 3 A\# $0E $EF 6 F\# $5E $D6
1 D\# $02 $7D 3 B $0F $D2 6 G $64 $79
1 E $02 $A3 4 C $10 $C3 6 G\# $6A $73
1 F $02 $CC 4 C\# $11 $C3 6 A $70 $C7
1 F\# $02 $F6 4 D $12 $D1 6 A\# $77 $7C
1 G $03 $23 4 D\# $13 $EF 6 B $7E $97
1 G\# $03 $53 4 E $15 $1F 7 C $86 $1E
1 A $03 $86 4 F $16 $60 7 C\# $8E $18
1 A\# $03 $BB 4 F\# $17 $B5 7 D $96 $8B
1 B $03 $F4 4 G $19 $1E 7 D\# $9F $7E
2 C $04 $30 4 G\# $1A $9C 7 E $A8 $FA
2 C\# $04 $70 4 A $1C $31 7 F $B3 $06
2 D $04 $B4 4 A\# $1D $DF 7 F\# $BD $AC
2 D\# $04 $FB 4 B $1F $A5 7 G $C8 $F3
2 E $05 $47 5 C $21 $87 7 G\# $D4 $E6
2 F $05 $98 5 C\# $23 $86 7 A $E1 $8F
2 F\# $05 $ED 5 D $25 $A2 7 A\# $EE $F8
2 G $06 $47 5 D\# $27 $DF 7 B $FD $2E
Figure 9.1: All available notes on the C64 and their corresponding hi/lo byte values. Note that Iridis Alpha
only uses octaves 3 to 7. The available notes in octaves 1 to 2 are never used.
With 96 notes in total available, Iridis only uses 72 of them, omitting the 2 lowest oc-
taves. We can see this when we look at the note table in the game. This pair of arrays
are where the title music logic plucks the note to be played once it has dynamically
selected one:
Listing 9.3: The lookup table for all of the notes used in the theme music. The two lowest available octaves
are not used by the game. To see this for yourself compare the \=rst entry in
titleMusicHiBytes/titleMusicLowBytes ($08 and $61 giving $0861) with the entry highlighted in red in
the previous table.
So now that we know where the notes are and how to make them go beep we just have
to \=gure out the order that PlayTitleScreenMusic contrives to play them.
189
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
It would certainly help if we could see what the music looks like, so lets do that. Here
is the opening title tune as sheet music in Western notation.
9.1.1 Structure
Even if you can't read sheet music notation some structure should be evident.
For every 4 notes Voice 3 plays, Voice 2 chimes in with a new note that it sustains until
the next one.
190
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Voice 1 does the same for every 16 notes that Voice 3 plays and every 4 notes of Voice
2..
Armed with this insight we can see it reflected in the logic in PlayTitleScreenMusic.
This routine is called regularly by a system interrupt, a periodic wake-up call performed
by the C64 CPU. So multiple times every second it is run and must \=gure out what new
notes, if any, to play on each of the three voices.
MaybePlayVoice1
DEC v o i c e 1 N o t e D u r a t i o n
BNE MaybePlayVoice2
LDA \#$30
STA v o i c e 1 N o t e D u r a t i o n
LDX voice1IndexToMusicNoteArray
LDA titleMusicNoteArray ,X
CLC
ADC offsetForNextVoice1Note
TAY
STY offsetForNextVoice2Note
JSR PlayNoteVoice1
INX
TXA
AND \#$03
STA voice1IndexToMusicNoteArray
191
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
voice1NoteDuration is used to count the interval between notes on Voice 1. It's decre-
mented on each visit and when it reaches zero it gets reset to 48 ($30) and a note is
played. What's being counted here isn't seconds, it's cycles or 'interrupts'. So this
translates to only a few seconds between notes being played.
The same is done for both Voice 2 and Voice 3 but the intervals are shorter: 12 ($0C)
and 3 ($03). This matches the relationship we see in the sheet music, one note in Voice
1 for every sixteen in Voice 3 (48/3=16) and one note in Voice 2 for every four in Voice
3 (12/3=4).
M ay be Pl a yV oi ce 2
DEC v o i c e 2 N o t e D u r a t i o n
BNE M ay be P la yV oi c e3
LDA \# $0C
STA voice2NoteDuration
LDX voice2IndexToMusicNoteArray
LDA t i t l e M u s i c N o t e A r r a y ,X
CLC
ADC offsetForNextVoice2Note
; Use this new value to change the key of the next four
; notes played by voice 3 .
STA o f f s e t F o r N e x t V o i c e 3 N o t e
TAY
JSR PlayNot eVoice2
INX
TXA
AND \# $03
STA v o i c e 2 I n d e x T o M u s i c N o t e A r r a y
M ay be Pl a yV oi ce 3
DEC v o i c e 3 N o t e D u r a t i o n
BNE R e t u r n F r o m T i t l e S c r e e n M u s i c
LDA \# $03
STA v o i c e 3 N o t e D u r a t i o n
192
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
; position in t i t l e M u s i c N o t e A r r a y .
INX
TXA
; Since it 's only 4 bytes long ensure we wrap
; back to 0 if it 's greater than 3 .
AND \# $03
STA v o i c e 3 I n d e x T o M u s i c N o t e A r r a y
193
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Since each tune is dynamically generated there's nowhere for us to pull them
from. We could record the tunes as audio \=les and maybe extract something
useful that way. A feature of Vice, the C64 emulator, allows us to do something
much simpler. We can log every note that's played to a text \=le and use that
trace to reconstruct the tunes.
The moncommands.txt \=le contains a series of debugger directives that tells x64 to log
every value stored to the music registers at $D400-$D415. This will capture all notes played
on all three voices as well as any updates made to the other sound parameters and write
them to IridisAlphaTitleMusicAll.txt:
log on
logname "" I r i d i s A l p h a T i t l e M u s i c A l l . t x t ""
tr store D400 D415
This examples gives us the value in A written to each register for Voice 1. For example, $61
has been written to $D400 and $08 has been written to $D401.
We can now write a short Python notebook that parses this \=le and for each tune constructs
three arrays, each representing a voice, with the sequence of notes played to each. For
example, in the extract above we can extract $0861 as the note 'C' in octave 3 played on
Voice 1 ($D400-$D401). (Refer to the tables above to see why $0861 translates to 'C-3'.
With the sequence of notes in three arrays, each representing one of the 3 voices, it is
a simple matter to transform this into ABC format, a music notation frequently used for
traditional music.
194
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
\%abc−2 . 2
\%\%pagewidth 35cm
\%\%header "" Example page : $P ""
\%\%f o o t e r "" $T ""
\%\%g u t t e r .5cm
\%\%b a r s p e r s t a f f 16
\%\%t i t l e f o r m a t R−P−Q−T C1 O1 , T+T N1
\%\%composerspace 0
X : 2 \% s t a r t of header
T : I r i d i s Alpha T i t l e Theme
T : 1 of 100 ,000 ,000 ,000 ,000
C: ( Sid. )
O: Jeff Minter
R : P r o c e d u r a l l y Generated
L : 1/8
K : D \% s c a l e : C major
V : 1 name= "" Voice 1 ""
C,16 | | | | G, 1 6 | | | | C16 | | | | G, 1 6 |
| | | G, 1 6 | | | | D16 | | | | G16 | |
| | D16 | | | | C16 | | | | G16 | | |
| c16 | | | | G16 | | | | : |
V : 2 name= "" Voice 2 ""
C,4 | G, 4 | C4 | G, 4 | G, 4 | D4 | G4 | D4 | C4 | G4 | c4 |
G4 | G, 4 | D4 | G4 | D4 | G, 4 | D4 | G4 | D4 | D4 | A4 | d4
| A4 | G4 | d4 | g4 | d4 | D4 | A4 | d4 | A4 | C4 | G4 |
c4 | G4 | G4 | d4 | g4 | d4 | c4 | g4 | c ' 4 | g4 | G4 |
d4 | g4 | d4 | : |
V : 3 name="" Voice 3""
C , 1 G , 1 C1G , 1 | G , 1 D1G1D1 | C1G1c1G1 | G , 1 D1G1D1 | G , 1 D1G1D1 | D1A1d1A1 | G1d1g1d1 | D1A1d1A1 | C1G1c1G1 | G1d1g1d1 |
c1g1c ' 1 g1 | G1d1g1d1 | G , 1 D1G1D1 | D1A1d1A1 | G1d1g1d1 | D1A1d1A1 | G , 1 D1G1D1 | D1A1d1A1 | G1d1g1d1 | D1A1d1A1
| D1A1d1A1 | A1e1a1e1 | d1a1d ' 1 a1 | A1e1a1e1 | G1d1g1d1 | d1a1d ' 1 a1 | g1d ' 1 g ' 1 d ' 1 | d1a1d ' 1 a1 | D1A1d1A1
| A1e1a1e1 | d1a1d ' 1 a1 | A1e1a1e1 | C1G1c1G1 | G1d1g1d1 | c1g1c ' 1 g1 | G1d1g1d1 | G1d1g1d1 | d1a1d ' 1 a1
| g1d ' 1 g ' 1 d ' 1 | d1a1d ' 1 a1 | c1g1c ' 1 g1 | g1d ' 1 g ' 1 d ' 1 | c ' 1 g ' 1 c ' ' 1 g ' 1 | g1d ' 1 g ' 1 d ' 1 | G1d1g1d1
| d1a1d ' 1 a1 | g1d ' 1 g ' 1 d ' 1 | d1a1d ' 1 a1 | : |
We can then use the tool `abcm2ps` to transform this into an SVG image \=le giving the
music in standard Western notation.
9.1.2 Phrasing
Now that we've identi\=ed the underlying 4-bar structure of the arrangement. We can
take a closer look at the phrasing of the invidividual parts. Voice 3 has a simple repet-
itive structure for each 4-bar phrase:
195
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Bars 2 and 4 are repeated. Each bar consists of the same tonic formula: three notes
rising two notes at a time, falling back on the \=nal note. The difference between bars
1 and 3 is a simple key change.
; This seeds the title music. Playing around with these first
; four bytes alters the first few seconds of the title music.
; The routine for the title music uses these 4 bytes to determine
; the notes to play.
; This array is periodically replenished from t i t l e M u s i c S e e d A r r a y by
; SelectNewNotesToPlay .
t i t l e M u s i c N o t e A r r a y .BYTE $00 , $07 , $0C , $07
Notice how the values populated in titleMusicNoteArray at start-up match the struc-
ture of our basic tonic formula, e.g. C3-G3-C4-G3.
Playing the 4 note phrase we've stored in this array is done here:
The variable that's doing a bit of extra work here is offsetForNextVoice3Note. This is
what's shifting the notes for subsequent bars from the base position of C3-G3-C4-G3
to G3-D4-G4-D4. This value has to get updated after every four notes, otherwise we
just keep playing the same four notes over and over again.
The obvious place to do this is when play a note on Voice 2, which is something we're
already doing every 4 notes in Voice 3.
196
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
M ay be Pl a yV oi ce 2
DEC v o i c e 2 N o t e D u r a t i o n
BNE M ay be P la yV oi c e3
LDA \# $0C
STA voice2NoteDuration
LDX voice2IndexToMusicNoteArray
LDA t i t l e M u s i c N o t e A r r a y ,X
CLC
ADC offsetForNextVoice2Note
; Use this new value to change the key of the next four
; notes played by voice 3 .
STA o f f s e t F o r N e x t V o i c e 3 N o t e
TAY
JSR PlayNot eVoice2
INX
TXA
AND \# $03
STA v o i c e 2 I n d e x T o M u s i c N o t e A r r a y
As we can see the mechanics of playing a note for Voice 2 are otherwise the same
as Voice 3. We're playing the same phrase encoded in titleMusicNoteArray that is
played by Voice 3 but just over a longer period of time. And if you look closely again
at the \=rst four bars of the \=rst title tune you can see that Voice 2 is in fact playing the
exact same 4 notes of the \=rst bar of Voice 3.
The same thing happens for Voice 1: it is playing the same notes as the \=rst bar of
Voice 3 but over 16 bars (1 every 4 bars).
So ultimately what we have underlying every tune generated by Iridis Alpha is a 16-bar
structure where the same 4 notes are played by Voice 3 in its \=rst bar, Voice 2 in its
\=rst 4 bars, and Voice 1 over the full 16 bars. This structure recurs every 16 bars, each
time using the 4 initial notes from Voice 3.
197
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Figure 9.5: A full 16 bar passage showing the nested structure of Voices 1 and 2
This is a nested structure with the initial musical phrase that occurs every 4 bars in
Voice 3 being picked up by Voice 2 and the one that occurs at every 16th bar being
picked up by Voice 1.
The second, \=ner-grained structure of each tune lies in Voice 3 and consists of se-
lecting a fundamental 4 note pattern (as we discussed above) and applying that same
pattern to the key change between each 4 note phrase!
Figure 9.6: The G3-C4-G4-C4 pattern used to construct the 4 note pattern is also used to construct the key
changes in each 4-bar sequence (red-blue-green-blue).
This is why we observed the repeating structure of Bars 2 and 4 earlier! It's the same
pattern used to construct the 4 note formula.
But how do we choose the key for the start of each 4-bar pattern? By applying the same
pattern to the start of each 4-bar section!
198
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Figure 9.7: The start of each 4 bar pattern in a 16 bar cycle uses each of the 4-note patterns from the \=rst 4
bars.
If we look at two other procedurally generated tunes we can see the same pattern:
199
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
We've established how each tune is built entirely off the same 4-byte sequence, all the
way from selecting notes to play to \=lling out the larger structure of the tune at almost
every level. What remains is to see how this 4-byte sequence is selected. We know it's
not entirely random since if it was, none of us would ever hear the same tune.
The selection of our 4-byte structure for each tune happens in SelectNewNotesToPlay.
Once a seed value has been plucked, this is used as an index into titleMusicSeedArray
and the next four values are populated into our magic 4-byte sequence that determines
everything titleMusicNoteArray.
SelectNewNotesToPlay
; Get a random value between 0 and 15 .
JSR P u t P r o c e d u r a l B y t e I n A c c u m u l a t o r
AND \# $0F
; Jump to I n i t i a l i z e S e e d L o o p if it 's zero.
BEQ I n i t i a l i z e S e e d L o o p
200
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Listing 9.8: Put a seed byte in the accumulator and multiply this by 4 if it's not zero. This gives us what we
need for the next step.
InitializeSeedLoop
; Put our random number in Y and use it as index into
; the seed array.
TAY
; Initialize X to 0 , we will use this to iterate up to
; 4 bytes for pulling from t i t l e M u s i c S e e d A r r a y .
LDX \# $00
Listing 9.9: Use our seed value to pull 4 bytes from titleMusicSeedArray and store them in
titleMusicNoteArray
Listing 9.10: Our seed bank for 4-byte sequences. It's 64 bytes long giving 16 possibles sequences in all.
The real source of variety here is this 'seed value' that we pluck at the very start of
the process. This is done by PutProceduralByteInAccumulator at the very start of
SelectNewNotesToPlay.
201
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
; -------------------------------------------------
; PutProceduralByteInAccumulator
; This function is self - modifying . Every time it
; is called it increments the address that
; s o u r c e O f S e e d B y t e s points to. Since s o u r c e O f S e e d B y t e s
; intially points to $9A00 , it will point to $9A01
; after the first time it 's called , $9A02 after the
; second time it 's called - and so on.
; --------------------------------------------------
PutProceduralByteInAccumulator
srcOfProceduralBytes =*+ $01
LDA s o u r c e O f S e e d B y t e s
INC s r c O f P r o c e d u r a l B y t e s
RTS
*= $9A00
sourceOfSeedBytes
.BYTE $E0 , $D3 , $33 , $1F , $BF , $EC , $EF , $3E
.BYTE $FA , $70 , $DA , $26 , $87 , $C2 , $C9 , $9C
.BYTE $F7 , $FB , $C8 , $85 , $C1 , $A9 , $64 , $AD
.BYTE $6B , $DE , $8B , $8F , $05 , $5E , $54 , $51
.BYTE $78 , $0A , $6E , $6F , $FD , $0C , $A5 , $32
.BYTE $F5 , $56 , $44 , $75 , $38 , $D6 , $23 , $98
.BYTE $61 , $D5 , $49 , $C6 , $F2 , $95 , $BA , $08
.BYTE $C3 , $3D , $F4 , $F0 , $21 , $48 , $84 , $02
.BYTE $7E , $5B , $68 , $55 , $04 , $92 , $AE , $34
.BYTE $72 , $F6 , $71 , $A1 , $39 , $4F , $74 , $E5
.BYTE $E8 , $31 , $9A , $C7 , $E3 , $86 , $6D , $14
.BYTE $60 , $CD , $50 , $FF , $82 , $52 , $66 , $9E
.BYTE $E9 , $53 , $25 , $93 , $07 , $77 , $2E , $D7
.BYTE $1A , $62 , $80 , $B7 , $0D , $1B , $15 , $46
.BYTE $CE , $AA , $47 , $24 , $8D , $E1 , $18 , $67
.BYTE $6A , $4A , $F1 , $B9 , $D0 , $91 , $BC , $EE
.BYTE $B5 , $D1 , $7B , $A0 , $DB , $36 , $45 , $E7
.BYTE $11 , $22 , $81 , $FC , $58 , $30 , $28 , $CB
.BYTE $8C , $B1 , $0B , $A7 , $DC , $B4 , $9D , $57
.BYTE $B3 , $ED , $3C , $43 , $16 , $8A , $EA , $D8
.BYTE $0E , $89 , $1D , $1E , $DF , $9F , $BD , $BB
.BYTE $F9 , $D9 , $01 , $3B , $7A , $BE , $69 , $B8
.BYTE $5A , $A6 , $E2 , $96 , $F8 , $AC , $6C , $12
.BYTE $2D , $19 , $2A
As you might have guessed by now, given that there are only 16 possible sequences to
choose from the seed bank there must actually be a lot less than a hundred thousand
billion possible them tunes. With only 16 sequences there may even be only 16!
202
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
Well yes, there are certainly a lot closer to just 16 than a hundred thousand billion.
The variety of values we get from sourceOfSeedBytes is not really of any account in
the number of tunes we can generate. We're just using it to get pseudo-random but
relatively predicatble values between 0 and 15 and using that to choose one of the 16
4-byte 'tune seeds'.
There's an additional bit of variability that gives us more than just 16 tunes though. This
is the value we add to the note's index before we play it:
M ay be Pl a yV oi ce 1
DEC v o i c e 1 N o t e D u r a t i o n
BNE M ay be P la yV oi c e2
LDA \# $30
STA v o i c e 1 N o t e D u r a t i o n
LDX voice1IndexToMusicNoteArray
LDA t i t l e M u s i c N o t e A r r a y ,X
CLC
ADC offsetForNextVoice1Note
TAY
STY offsetForNextVoice2Note
When we select a new tune the value offsetForNextVoice1Note may be carrying over
a value from the previous tune so rather than be a consistent value every time the 'tune
seed' is selected, it will vary in value. The result is that the logic will select a different
note-group even though it used the same 4-byte 'tune seed'.
LDX n o t e s P l a y e d S i n c e L a s t K e y C h a n g e
LDA t i t l e M u s i c N o t e A r r a y ,X
STA o f f s e t F o r N e x t V o i c e 1 N o t e
In practice there are 16 possible voice note sequences and 12 unique possible byte
values to load from titleMusicSeedArray so there are 192 possible tunes.
So the Hundred Thousand Billion is a lie. Iridis will indeed play a hundred thousand
billion times if you leave it running long enough but ultimately even when we account
for variations in key it can only ever play 192 unique title tunes.
203
CHAPTER 9. A HUNDRED THOUSAND BILLION THEME TUNES
204
Another 164 Tunes
It's possible to dig into the making of the title music and how Jeff Minter arrived the
music con\=guration he did thanks to a number of tiny demo programs that survive
from the period when he was developing Iridis Alpha.
It turns out he was inspired by an article in 'Byte' magazine from June 1986 that de-
scribed how to make 'Fractal Music'. This article outline a version of the algorithm that
Jeff ultimately adopted. The 'self-similarity' we encountered in the way the Iridis Al-
pha theme tunes are constructed, a four-note structure repeated across different time
intervals on each of the three voices, \=nds its roots in this article.
205
CHAPTER 10. ANOTHER 164 TUNES
Figure 10.1
206
CHAPTER 10. ANOTHER 164 TUNES
10.1 Taurus:Torus
Figure 10.2
This \=rst demo, released in July 1986(?), has a version of Iridis' music-generating algo-
rithm that is nearly fully formed. However, the music it produces is quite different. In
fact, it is nearer to a tool for listening to and selecting music than anything else.
The four seed values in titleMusicNoteArray that are used to seed all subequently
generated tunes (00 07 0C 07 in Iridis Alpha) can be selected and changed by the
user. They're called 'Oscillators' and each can be any value between 0 and 16, i.e. any
of 0 1 2 3 4 5 6 7 8 9 A B C D E F.
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− ;−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; P l a y Ti t l e S c r e e n M u s i c ; P l a y Ti t l e S c r e e n M u s i c
; ( TORUS : TAURUS ) ; ( I R I D I S ALPHA)
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− ;−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
P l a y Ti t l e S c r e e n M u s i c P l a y Ti t l e S c r e e n M u s i c
DEC baseNoteDuration
BEQ MaybeStartNewTune
RTS
MaybeStartNewTune
LDA pr ev io us Ba s eN ot eD ur at io n
STA baseNoteDuration
JSR SelectNewNotesToPlay
207
CHAPTER 10. ANOTHER 164 TUNES
; We ' l l o n l y s e l e c t a new tune when we ' ve reached the ; We ' l l o n l y s e l e c t a new tune when we ' ve reached
; b e g i n n i n g of a new 16 bar s t r u c t u r e . ; the b e g i n n i n g of a new 16 bar s t r u c t u r e .
INX INX
TXA TXA
AND \#$03 AND \#$03
STA notesPlayedSinceLastKeyChange STA notesPlayedSinceLastKeyChange
BNE MaybePlayVoice1
JSR SelectNewNotesToPlay
MaybePlayVoice1 MaybePlayVoice1
DEC v o i c e 1 N o t e D u r a t i o n DEC v o i c e 1 N o t e D u r a t i o n
BNE MaybePlayVoice2 BNE MaybePlayVoice2
INX INX
TXA TXA
AND \#$03 AND \#$03
STA voice1IndexToMusicNoteArray STA voice1IndexToMusicNoteArray
MaybePlayVoice2 MaybePlayVoice2
DEC v o i c e 2 N o t e D u r a t i o n DEC v o i c e 2 N o t e D u r a t i o n
BNE MaybePlayVoice3 BNE MaybePlayVoice3
; Use t h i s new v a l u e t o change the key of the ne x t f o u r ; Use t h i s new v a l u e t o change the key of the n e x t
four
; notes played by v o i c e 3 . ; notes played by v o i c e 3 .
STA o f f s e t F o r N e x t V o i c e 3 N o t e STA o f f s e t F o r N e x t V o i c e 3 N o t e
TAY TAY
JSR PlayVoice2 JSR PlayNoteVoice2
INX INX
TXA TXA
AND \#$03 AND \#$03
STA voice2IndexToMusicNoteArray STA voice2IndexToMusicNoteArray
MaybePlayVoice3 MaybePlayVoice3
DEC v o i c e 3 N o t e D u r a t i o n DEC v o i c e 3 N o t e D u r a t i o n
BNE ReturnFromTitleScreenMusic BNE ReturnFromTitleScreenMusic
208
CHAPTER 10. ANOTHER 164 TUNES
TXA TXA
; Since i t ' s o n l y 4 by tes long ensure we wrap ; Since i t ' s o n l y 4 b yte s long ensure we wrap
; back t o 0 i f i t ' s g r e a t e r than 3 . ; back t o 0 i f i t ' s g r e a t e r than 3 .
AND \#$03 AND \#$03
STA voice3IndexToMusicNoteArray STA voice3IndexToMusicNoteArray
ReturnFromTitleScreenMusic ReturnFromTitleScreenMusic
RTS RTS
Listing 10.1: The music routine in Torus:Taurus side-by-side with Iridis Alpha.
Not all of the tunes are 64-note based. It does generate some that are truncated.
209
CHAPTER 10. ANOTHER 164 TUNES
Figure 10.7
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− ;−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; P l a y Ti t l e S c r e e n M u s i c ; P l a y Ti t l e S c r e e n M u s i c
; ( TORUS : TAURUS I I ) ; ( I R I D I S ALPHA)
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− ;−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
P l a y Ti t l e S c r e e n M u s i c P l a y Ti t l e S c r e e n M u s i c
LDA UnusedValue1 DEC baseNoteDuration
STA UnusedValue2 BEQ MaybeStartNewTune
210
CHAPTER 10. ANOTHER 164 TUNES
RTS
MaybeStartNewTune MaybeStartNewTune
LDA pr ev io u sB as eN ot eD ur at io n
STA baseNoteDuration
; We ' l l o n l y s e l e c t a new tune when we ' ve reached the ; We ' l l o n l y s e l e c t a new tune when we ' ve
reached the
; b e g i n n i n g of a new 16 bar s t r u c t u r e . ; b e g i n n i n g of a new 16 bar s t r u c t u r e .
INX INX
TXA TXA
AND \#$03 AND \#$03
STA notesPlayedSinceLastKeyChange STA notesPlayedSinceLastKeyChange
BNE MaybePlayVoice1 BNE MaybePlayVoice1
MaybePlayVoice1 MaybePlayVoice1
DEC v o i c e 1 N o t e D u r a t i o n DEC v o i c e 1 N o t e D u r a t i o n
BNE MaybePlayVoice2 BNE MaybePlayVoice2
INX INX
TXA TXA
AND \#$03 AND \#$03
STA voice1IndexToMusicNoteArray STA voice1IndexToMusicNoteArray
MaybePlayVoice2 MaybePlayVoice2
DEC v o i c e 2 N o t e D u r a t i o n DEC v o i c e 2 N o t e D u r a t i o n
BNE MaybePlayVoice3 BNE MaybePlayVoice3
; Use t h i s new v a l u e t o change the key of the ne x t f o u r ; Use t h i s new v a l u e t o change the key of the
n e xt f o u r
; notes played by v o i c e 1 . ; notes played by v o i c e 3 .
STA o f f s e t F o r N e x t V o i c e 1 N o t e STA o f f s e t F o r N e x t V o i c e 3 N o t e
TAY
JSR PlayNoteVoice2
INX
TXA
AND \#$03
STA voice2IndexToMusicNoteArray
TAY
JSR PlayNoteVoice2
INX
TXA
AND \#$03
STA voice2IndexToMusicNoteArray
MaybePlayVoice3 MaybePlayVoice3
DEC v o i c e 3 N o t e D u r a t i o n DEC v o i c e 3 N o t e D u r a t i o n
211
CHAPTER 10. ANOTHER 164 TUNES
R e t u r n F r o m Ti t l e M u s i c ReturnFromTitleScreenMusic
RTS RTS
Listing 10.2: The music routine in Taurus:Torus II side-by-side with Iridis Alpha.
Figure 10.8
212
CHAPTER 10. ANOTHER 164 TUNES
Figure 10.9
213
Made in France
Figure 11.1: Splash screen for the version of 'Made in France' released on Compunet.
I must admit I don't \=nd this pause-mode mini-game of much interest in its own right. I
initially wondered if Jeff Minter had inadvertently invented a precursor to 'Snake', a text-
based game that was once ubiqitous thanks to its inclusion on Nokia mobile phones
in the 1990s, but it turns out that Snake dates back to at least 1976 when the concept
\=rst appeared in an arcade game called 'Blockade'.
Perhaps the most noteworthy thing about 'Made in France' (MIF) is how many times
Minter has made and remade it. His very \=rst attempt at the format was one of his
earliest games. 'Deflex' was coded in 1979 while he was in college and had access to
a Commodore PET. Viewed side by side it's obvious that one is a slightly more colorful
214
CHAPTER 11. MADE IN FRANCE
MIF meanwhile was coded while on a ski holiday in France during the making of Iridis
Alpha. Minter lugged his development kit on holiday to the Alps and continued work
on Iridis Alpha between time on the slopes and in a couple of evenings remade Deflex
into MIF. He shared this version on Compunet prior to the release of Iridis Alpha and
the version that features in the game is unchanged from that original version.
The gameplay of Deflex and MIF has only a couple of differences. In MIF the player is
on a timer and dies if they fail to complete the level before it elapses.
Figure 11.3: The controls seem unintuitive to a player using a C64 emulator. Why use 'M' and 'N' for placing
paddles? The answer lies in the PET and C64 keyboard layouts: the 'm' and 'n' keys each bear the paddle
glyphs.
Main Loop
The code for MIF comes in at a relatively light 900 or so lines of 6502 assembler. The
main loop of the game is primarily concerned with detecting key presses to enter into
the DNA mini-game, see A Pause Mode for Your Pause Mode or exit back to Iridis Alpha
215
CHAPTER 11. MADE IN FRANCE
itself.
M I F \. R u n U n t i l P l a y e r U n p a u s e s
JSR M I F \. I n i t i a l i z e P r o g r e s s B a r
JSR M I F \. D r a w C o u n t d o w n B a r A n d C r e d i t
JSR M I F \. U p d a t e P r o g r e s s B a r
JSR M I F \. S e t U p I n t e r r u p t H a n d l e r
MIF\.MainLoop
LDA lastKey Pressed
CMP \# $40 ; ' No key pressed '
BNE MIF\.MainLoop
LDA \# $00
STA $D015 ; Sprite display Enable
MaybeAsteriskPressed
CMP \# $31 ; '* ' Pressed
BNE M a y b e L a u n c h N e w G a m e
; Launch DNA
LDA \# $01
STA m i f D N A P a u s e M o d e A c t i v e
JSR E n t e r M a i n T i t l e S c r e e n
MaybeLaunchNewGame
LDA mifGameOver
BEQ M a y b e E x i t B a c k T o G a m e
JMP LaunchMIF
M I F \. I n t e r r u p t H a n d l e r
LDA $D019 ; VIC Interrupt Request Register ( IRR )
AND \# $01
; Limits the updates to once per frame.
BNE P e r f o r m G a m e P l a y U p d a t e s
216
CHAPTER 11. MADE IN FRANCE
PLA
TAY
PLA
TAX
PLA
RTI
PerformGamePlayUpdates
JSR U p d a t e S n a k e P o s i t i o n A n d C h e c k I n p u t
JSR M I F \. U p d a t e C o u n t d o w n B a r
JSR MIF\.PlaySound
JSR M I F \. U p d a t e T a r g e t
LDA \# $01
STA $D019 ; VIC Interrupt Request Register ( IRR )
STA $D01A ; VIC Interrupt Mask Register ( IMR )
LDA \# $FE
STA $D012 ; Raster Position
JMP $EA31
Minter has written up a bit more on the history of Deflex and its various incarnations
throughout the years.
217
A Pause Mode for your Pause Mode
218
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
Figure 12.2: The \=rst screen paint in DNA. There are 24 raster interrupts allowing us to paint a long chain of
sprites.
219
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
Any pause mode must surely be in need of a pause mode. Titled 'DNA' this little en-
tertainment is a cousin of Minter's previous work on Psychedelia for the C64 and Col-
orspace for the Atari 800 in 1984 and 1985. It isn't accessed directly from the game
but instead is invoked by pressing the asterisk key while playing 'Made in France'.
Minter \=rst shared it as a tiny 11K demo in a UK Compunet forum in the summer of
1986. It followed shortly after 'Torus', an oscillator-based demo, shared at the same
time and which we cover elsewhere : both are sprite-based light synthesisers where,
like Psychedelia and Colorspace, the player gets to experiment with different con\=gu-
rations that control the behaviour of a frantic assembly of brightly colored, pulsating
sprites.
DNA has an unapologetically daft premise: two chains of flashing eyeballs cascade
down the screen in an unsettling, blinking helix con\=guration. Your object as player
is to twiddle the available knobs to see if you can get them to do anything interesting
while listening to your favorite music.
For its time DNA's most noteworthy feature was the number of sprites being written
to the screen. There are 48 eyeballs displayed at any one time, in addition to a pair of
parallax star\=elds drifting past in the background. As you may have gathered by now,
the C64 can only support 8 sprites in total so this would have seemed like wizardry to
the uninitiated. If you're not dipping into this chapter at random, but have read any of
the previous chapters in this book you may already be able to guess the secret to this
unsettling feat: raster interrupts.
As elsewhere in Iridis Alpha the trick to \=lling the screen with sprites is to write a tight
piece of code that can run periodically during a single paint of the screen, adding a
layer of sprites to each horizontal section. We tell the C64 where we want the raster to
interrupt its progress and call our code. This code will then paint as many sprites as
possible on the horizontal layer. DNA takes the approach of painting a pair of eyeballs
at 8 pixel vertical intervals, so each horizontal layer is 8 pixels tall. These intervals are
de\=ned in dnaSpritesYPositionsArray:
dnaSpritesYPositionsArray .BYTE $30 , $38 , $40 , $48 , $50 , $58 , $60 , $68
.BYTE $70 , $78 , $80 , $88 , $90 , $98 , $A0 , $A8
.BYTE $B0 , $B8 , $C0 , $C8 , $D0 , $D8 , $E0 , $E8
.BYTE END\.SENTINEL
While the Y co-ordinates of the sprites are set in stone, their X co-ordinates must be
calculated on the fly and indeed the purpose of the twiddling knobs is to control the
way these co-ordinates are generated. Initially the X positions are all set to 192 ($C0):
dnaSpritesXPositionsArray .BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0
220
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
With every full paint of the screen calculates a new X co-ordinate and puts it at the
head of the array. Depending on the value given as the 'Speed' setting, it then shifts all
the others in the array to the right by one position.
D N A \. P r o p a g a t e P r e v i o u s X P o s T o T h e R i g h t
LDX \# $27
PropagateToRightLoop
LDA d n a S p r i t e s X P o s i t i o n s A r r a y - $01 , X
STA d n a S p r i t e s X P o s i t i o n s A r r a y ,X
DEX
BNE P r o p a g a t e T o R i g h t L o o p
RTS
This very quickly \=lls the array. We can see this in operation from the below snippet
in DNA MainAnimationRoutine. In order to calculate the x position for the right hand
chain, it shows the value in dnaCurrentPhase being added to the value dnaCurrent-
SpritesPosArrayIndex to pick out a value in dnaSpritesXPositionsArray offset by
the amount of the 'Phase' setting:
221
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
LDX d n a C u r r e n t S p r i t e s P o s A r r a y I n d e x
..
; Add in the phase to our index to the X position
; of the sprite on the right hand chain. If the
; result is greater than the number of values
; in the array ( $27 ) subtract it out again.
; This means the ' Phase ' setting acts as an
; offset into the X Position array picking up
; previous values of X Pos from the left hand
chain.
TXA
..
ADC d na Cu rr e nt Ph as e
CMP \# $27
BMI U p d a t e X P o s W i t h P h a s e
In the table below we've selected a few frames from the \=rst second or two showing
how the array \=lls up and how values from the array are selected for the X position
of the left and right chains. Notice how the 'Phase' setting of 5 effectively means the
right-hand chain lags 5 'eyeballs' behind the left chain in terms of the positions.
222
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
dnaSpritesXPositionsArray dnaSpritesXPositionsArray
.BYTE $58,$5A,$60,$67,$71,$7C,$89,$96, .BYTE $58 , $5A , $60 , $67 , $71 , $7C,$89,$96,
.BYTE $A3,$AE,$B6,$BB,$BC,$B9,$B3,$A9, .BYTE $A3,$AE,$B6,$BB,$BC,$B9,$B3,$A9,
.BYTE $9D,$8F,$80,$C0,$C0,$C0,$C0,$C0, .BYTE $9D,$8F,$80,$C0,$C0,$C0,$C0,$C0,
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , .BYTE $C0,$C0,$C0,$C0,$C0,$C0 , $C0 , $C0 ,
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 .BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0
dnaSpritesXPositionsArray dnaSpritesXPositionsArray
.BYTE $6F,$68,$61,$5C,$59,$58,$5A,$60, .BYTE $6F , $68 , $61 , $5C , $59 , $58,$5A,$60,
.BYTE $67,$71,$7C,$89,$96,$A3,$AE,$B6, .BYTE $67,$71,$7C,$89,$96,$A3,$AE,$B6,
.BYTE $BB,$BC,$B9,$B3,$A9,$9D,$8F,$80, .BYTE $BB,$BC,$B9,$B3,$A9,$9D,$8F,$80,
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , .BYTE $C0,$C0,$C0,$C0,$C0 , $C0 , $C0 , $C0 ,
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 .BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0
dnaSpritesXPositionsArray dnaSpritesXPositionsArray
.BYTE $81,$7C,$76,$6F,$68,$61,$5C,$59, .BYTE $81 , $7C , $76 , $6F , $68 , $61,$5C,$59,
.BYTE $58,$5A,$60,$67,$71,$7C,$89,$96, .BYTE $58,$5A,$60,$67,$71,$7C,$89,$96,
.BYTE $A3,$AE,$B6,$BB,$BC,$B9,$B3,$A9, .BYTE $A3,$AE,$B6,$BB,$BC,$B9,$B3,$A9,
.BYTE $9D,$8F,$80 , $C0 , $C0 , $C0 , $C0 , $C0 , .BYTE $9D,$8F,$80,$C0,$C0,$C0,$C0,$C0,
.BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 .BYTE $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0 , $C0
Figure 12.3: Examples of the x co-ordinates (highlighted) in dnaSpritesXPositionsArray used by the left
and right chain when 'phase' is set to 5.
We said a new value for the top eye-ball in each chain is calculated every paint, but
whether the existing values get propagated to the right in dnaSpritesXPositionsArray
depends on the 'Speed' setting:
UpdateXPosArrays
DEC actualSpeed
BNE C a l c u l a t e N e w X P o s F o r H e a d
The 'Speed' setting is effectively a counter that is decremented at every paint, once it
reaches zero it gets reset back to its initial value (in this case '1') and the X Positions
223
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
How is the new value for the eyeball at the head of each chain calculated? We get
that it's updated at every screen paint but what determines it? The calculation is a
two-phase process but both phases depend on the same array of values stored in
newXPosOffsetsArray. This is a rattle-bag of X positions describing a more or less
continuous curve:
newXPosOffsetsArray .BYTE $40 , $46 , $4C , $53 , $58 , $5E , $63 , $68
.BYTE $6D , $71 , $75 , $78 , $7B , $7D , $7E , $7F
.BYTE $80 , $7F , $7E , $7D , $7B , $78 , $75 , $71
.BYTE $6D , $68 , $63 , $5E , $58 , $52 , $4C , $46
.BYTE $40 , $39 , $33 , $2D , $27 , $21 , $1C , $17
.BYTE $12 , $0E , $0A , $07 , $04 , $02 , $01 , $00
.BYTE $00 , $00 , $01 , $02 , $04 , $07 , $0A , $0E
.BYTE $12 , $17 , $1C , $21 , $27 , $2D , $33 , $39
.BYTE END\.SENTINEL
Listing 12.4: Notice that the values start at $40 rise gradually to $80 back to $00 and then back up to $40
again.
The other factors in this calculation are the 'Frequency' selected for each chain, the
left-hand chain (Wave 1) and the right-hand chain (Wave 2). The values that do the
most work are xPosOffsetForWave1 and xPosOffsetForWave2.
D N A \. U p d a t e S e t t i n g s B a s e d O n F r e q u e n c y
LDA d n a W a v e 1 F r e q u e n c y
AND \# $1F
TAX
LDA t i m e s T o N e x t U p d a t e F o r F r e q u e n c y ,X
STA i n i t i a l T i m e T o N e x t U p d a t e F o r W a v e 1
STA t i m e T o N e x t U p d a t e C o u n t e r F o r W a v e 1
LDA x P o s O f f s e t s F o r F r e q u e n c y ,X
STA x P o s O f f s e t F o r W a v e 1
LDA d n a W a v e 2 F r e q u e n c y
AND \# $1F
TAX
LDA t i m e s T o N e x t U p d a t e F o r F r e q u e n c y ,X
STA i n i t i a l T i m e T o N e x t U p d a t e F o r W a v e 2
STA t i m e T o N e x t U p d a t e C o u n t e r F o r W a v e 2
LDA x P o s O f f s e t s F o r F r e q u e n c y ,X
STA x P o s O f f s e t F o r W a v e 2
JMP D N A \. U p d a t e D i s p l a y e d S e t t i n g s
; Returns
Listing 12.5: The value selected for 'Frequency' for Wave 1 and 2 translates to
settings used in calculating the next X position for each chain.
As we can see above these are populated from xPosOffsetsForFrequency using the
'Frequency' as an index.
224
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
..
LDA x P o s O f f s e t s F o r F r e q u e n c y ,X
STA x P o s O f f s e t F o r W a v e 1
..
LDA x P o s O f f s e t s F o r F r e q u e n c y ,X
STA x P o s O f f s e t F o r W a v e 2
In each case the value populated here is added to a value selected from newXPosOffsetsArray
to give the new X position for the eyeball at the head of the chain. Well, in a way that's
only half true. Separate positions are indeed calculated for the left and right chain,
but at the end of the day the position calculate for the right-hand chain (Wave 2) is
purely notional. It's just used as an initial value that the value calculated for the left-
hand chain is layered on to. This allows the two chains to 'interfere' with each other,
as though the position of one affects the other - and reduces the chance of the two
overlapping too much:
UpdateHeadOfWave
; Take the new X Pos value we calculated for Wave 2 , add the position
; we calculated for Wave 1 and make that the new position for the
lead
; sprite at the head of d n a S p r i t e s X P o s i t i o n s A r r a y .
LDA n o t i o n a l N e w X P o s F o r W a v e 2
CLC
ADC n ew XP os F or Wa ve 1
STA d n a S p r i t e s X P o s i t i o n s A r r a y
When we compare the routines responsible for calculating the X Pos for Wave 2 and
Wave 1 side by side on the next page we can see many similarities between the two.
They are both maintaining and updating an index into newXPosOffsetsArray and us-
ing that to come up with a new X position. They both have to deal towards the end
of the routine with the need to ensure the index does not exceed $40 and subtract
$40 from it if it does. The output of CalculateValueOfNewXPosForWave2 is essentially
the value stored in notionalNewXPosForWave2 and this is what is used as the input
above, in conjunction with newXPosForWave1, to come up with the \=nal value for the X
position for Wave 1. It is this value that gets added to the head of the working array
dnaXPosDataHeadArray by DNA PropagatePreviousXPosToTheRight.
225
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
CalculateValueOfNewXPosForWave1
DEC actualSpeed
BNE CalculateNewXPosForHead
Listing 12.7: Calculate a notional X Pos value for Wave 2. Listing 12.8: Calculate the new X Pos value for the head
This routine is called by the one on the right its return sprites as well as propagate the values in
value is notionalNewXPosForWave2. dnaXPosDataHeadArray to the right when required.
226
CHAPTER 12. A PAUSE MODE FOR YOUR PAUSE MODE
227
An Oscillator in 4 Parts
Figure 13.1: The Torus oscillator animation and Iridis' bonus animation.
The Torus demo is also the laboratory where the elegant animation used when award-
ing a bonus was developed. The code handling each is identical and was only very
lightly modi\=ed for the \=nal game.
228
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; AnimateGilbiesForNewBonus
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
RunMainInterruptHandler AnimateGilbiesForNewBonus
LDY \#$00 LDY \#$00
LDA \#$F0 LDA \#$F0
STA $D012 ; Raster Position STA $D012 ; Raster Position
DEC counterBetweeXPosUpdates DEC counterBetweeXPosUpdates
BNE MaybeUpdateYPos BNE MaybeUpdateYPos
UpdateXPos LDA i n i t i a l C o u n t e r B e t w e e n X P o s U p d a t e s
LDA i n i t i a l C o u n t e r B e t w e e n X P o s U p d a t e s STA counterBetweeXPosUpdates
STA counterBetweeXPosUpdates
LDA incrementForXPos
LDA incrementForXPos CLC
CLC ADC i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
ADC i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y STA i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
STA i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
MaybeUpdateYPos
MaybeUpdateYPos DEC counterBetweenYPosUpdates
DEC counterBetweenYPosUpdates BNE MaybeResetOsc3WorkingValue
BNE MaybeUpdateXPosOffset
LDA i n i t i a l C o u n t e r B e t w e e n Y P o s U p d a t e s
LDA i n i t i a l C o u n t e r B e t w e e n Y P o s U p d a t e s STA counterBetweenYPosUpdates
STA counterBetweenYPosUpdates
LDA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y
LDA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y CLC
CLC ADC incrementForYPos
ADC incrementForYPos STA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y
STA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y
MaybeResetOsc3WorkingValue
MaybeUpdateXPosOffset DEC o s c i l l a t o r 3 W o r k i n g V a l u e
DEC cyclesBetweenXPosOffsetUpdates BNE MaybeResetOsc4WorkingValue
BNE MaybeUpdateYPosOffset
LDA o s c i l l a t o r 3 V a l u e
LDA o s c i l l a t o r 3 V a l u e STA o s c i l l a t o r 3 W o r k i n g V a l u e
STA cyclesBetweenXPosOffsetUpdates INC i n d e x F o r X P o s O f f e t s e t I n S p r i t e P o s i t i o n A r r a y
INC i n d e x F o r X P o s O f f e t s e t I n S p r i t e P o s i t i o n A r r a y
MaybeResetOsc4WorkingValue
MaybeUpdateYPosOffset DEC o s c i l l a t o r 4 W o r k i n g V a l u e
DEC cyclesBetweenYPosOffsetUpdates BNE I n i t i a l i z e S p r i t e A n i m a t i o n
BNE S t o r e I n i t i a l I n d e x V a l u e s
LDA o s c i l l a t o r 4 V a l u e
LDA o s c i l l a t o r 4 V a l u e STA o s c i l l a t o r 4 W o r k i n g V a l u e
STA cyclesBetweenYPosOffsetUpdates INC i n x e d F o r Y P o s O f f s e t I n S p r i t e P o s i t i o n A r r a y
INC i n d e x F o r Y P o s O f f s e t I n S p r i t e P o s i t i o n A r r a y
InitializeSpriteAnimation
StoreInitialIndexValues LDA i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
; S t o r e the i n i t i a l v a l u e s f o r our i n d i c e s PHA
; on the s t a c k . LDA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y
LDA i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y PHA
PHA LDA i n d e x F o r X P o s O f f e t s e t I n S p r i t e P o s i t i o n A r r a y
LDA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y PHA
PHA LDA i n x e d F o r Y P o s O f f s e t I n S p r i t e P o s i t i o n A r r a y
LDA i n d e x F o r X P o s O f f e t s e t I n S p r i t e P o s i t i o n A r r a y PHA
Listing 13.1: Animation in Torus Demo Listing 13.2: ... and Iridis Alpha.
229
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
A Testing Hack
In the above the `canAwardBonus` byte is the \=rst letter in the name of the player with the
top score in the Hi-Score table. By default this is 'YAK':
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
; The hi gh score t a b l e .
; −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
h i S c o r e Ta b l e P t r .TEXT "" 0068000 ""
canAwardBonus .TEXT "" YAK ""
. F I L L 1 0 , $00
.TEXT "" 0065535RATT""
. F I L L 1 0 , $00
But if we change 'Y' to $1C like so, we can activate the hack:
Note that $1C is charset code for a bull's head symbol in Iridis Alpha, so it is also possible
to enter this as the initial of a high scorer name if we get a score that puts us to the top of
the table:
.BYTE $66 , $C3 , $7E , $5A , $7E , $7E , $3C , $00 ; .BYTE $66 , $C3 , $7E , $5A , $7E , $7E , $3C , $00
; CHARACTER $1c
; 01100110 ** **
; 11000011 ** **
; 01111110 ******
; 01011010 * ** *
; 01111110 ******
; 01111110 ******
; 00111100 ****
; 00000000
I'm guessing this was used for testing the animation routine and left in as an Easter egg.
To start getting a handle on how the oscillation animation works, lets plot the \=rst 24
animations that the Torus demo uses when left to its own devices. We get a variety of
different trajectories, some relatively simple, some quite convoluted.
230
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
Figure 13.2: The \=rst 24 oscillation patterns generated by the Torus demo.
231
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
When we look into the code we \=nd this petting zoo of animations is principally driven
by a simple sequence of bytes stored in spritePositionArray.
spritePositionArray .BYTE $40 , $46 , $4C , $52 , $58 , $5E , $63 , $68
.BYTE $6D , $71 , $75 , $78 , $7B , $7D , $7E , $7F
.BYTE $80 , $7F , $7E , $7D , $7B , $78 , $75 , $71
.BYTE $6D , $68 , $63 , $5E , $58 , $52 , $4C , $46
.BYTE $40 , $39 , $33 , $2D , $27 , $21 , $1C , $17
.BYTE $12 , $0E , $0A , $07 , $04 , $02 , $01 , $00
.BYTE $00 , $00 , $01 , $02 , $04 , $07 , $0A , $0E
.BYTE $12 , $17 , $1C , $21 , $27 , $2D , $33 , $39
.BYTE $FF
We can get a sense of how this rising and falling sequence of values can be used to
plot a course across the screen if we treat each as an x and y value on a graph of carte-
sian co-ordinates. In the twenty four instances below we start by treating the value as
providing both the x and y position. In each subsequent one we skip an increasing
number of positions ahead in the sequence to get the y value, producing a variety of
elliptical orbits around the screen.
232
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
Figure 13.3: Using the x/y offset in spritePositionArray where y is the value after x in the array.
233
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
234
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
To get beyond simple ellipsoids we need to do more than pick a different value in the
array for our x and y offsets. Here we experiment with something a little more in-
volved. We update the x and y positions at different intervals and when skipping ahead
in spritePositionArray for a new value for x and y we use a pre-selected, random
number of bytes to skip past.
This is starting to look more like the actual results we observed and it is where the 4
values selectable by the player using keys z, x, c, and v in the Torus demo come in.
In addition to controlling the music generation procedure, as we've already seen, they
also determine the way the values in spritePositionArray are selected for position
the sprite in each new frame. This is based on letting them determine the frequency
with which the position of the x and y values of each sprite is changed and how far to
skip ahead in spritePositionArray when selecting a new value from it for the x and
y position.
; Update O s c i l l a t o r 1
- Intervals between updating X position. LDA o s c i l l a t o r 1 V a l u e
CLC
- The amount to increment the index into ADC \#$01
Z Oscillator 1 AND \#$0F
spritePositionArray STA o s c i l l a t o r 1 V a l u e
MaybeXKeyPressed
CMP \#$17
BNE MaybeCKeyPressed
MaybeCKeyPressed
CMP \#$14
- How often to increase the index that seeks BNE MaybeVKeyPressed
MaybeVKeyPressed
- How often to increase the index that seeks CMP \#$1F
BNE MaybeF1Pressed
ahead to get a value
V Oscillator 4 ; Update O s c i l l a t o r 4
from spritePositionArrayfor adding LDA o s c i l l a t o r 4 V a l u e
CLC
to the next Y position. ADC \#$01
AND \#$0F
STA o s c i l l a t o r 4 V a l u e
235
CHAPTER 13. AN OSCILLATOR IN 4 PARTS
In RunMainInterruptHandler) we can see how each of these values set by the player
is used to maintain an accounting of the different sprite positions for each of the 8
sprites:
RunMainInterruptHandler
LDY \#$00
LDA \#$F0
STA $D012 ; Raster Position
DEC counterBetweeXPosUpdates
BNE MaybeUpdateYPos
UpdateXPos
LDA i n i t i a l C o u n t e r B e t w e e n X P o s U p d a t e s
STA counterBetweeXPosUpdates
LDA incrementForXPos
CLC
ADC i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
STA i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
MaybeUpdateYPos
DEC counterBetweenYPosUpdates
BNE MaybeUpdateXPosOffset
LDA i n i t i a l C o u n t e r B e t w e e n Y P o s U p d a t e s
STA counterBetweenYPosUpdates
LDA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y
CLC
ADC incrementForYPos
STA i n d e x F o r Y P o s I n S p r i t e P o s i t i o n A r r a y
MaybeUpdateXPosOffset
DEC cyclesBetweenXPosOffsetUpdates
BNE MaybeUpdateYPosOffset
LDA o s c i l l a t o r 3 V a l u e
STA cyclesBetweenXPosOffsetUpdates
INC i n d e x F o r X P o s O f f e t s e t I n S p r i t e P o s i t i o n A r r a y
MaybeUpdateYPosOffset
DEC cyclesBetweenYPosOffsetUpdates
BNE S t o r e I n i t i a l I n d e x V a l u e s
LDA o s c i l l a t o r 4 V a l u e
STA cyclesBetweenYPosOffsetUpdates
INC i n d e x F o r Y P o s O f f s e t I n S p r i t e P o s i t i o n A r r a y
Before animating each of the 8 sprites we use the values set by the player to prepare
the variables that will be applied to positioning each sprite. For example the value
selected with the Z key has been used to set initialCounterBetweenXPosUpdates
and incrementForXPos. In the \=rst lines above in UpdateXPos we use them to set up
indexForXPosInSpritePositionArray. THis is then used in SpriteAnimationLoop to
selec the X position of the current sprite:
SpriteAnimationLoop
LDA i n d e x F o r X P o s I n S p r i t e P o s i t i o n A r r a y
AND \#$3F
TAX
LDA s p r i t e P o s i t i o n A r r a y , X
STA c u r r S p r i t e X P o s
You can follow the same lineage between the setting of each value in our table above
with the rest of the SpriteAnimationLoop routine.
236
Iridis Oops!
What happened is that the master tape had dropped the very last byte in the second
section of game data on the tape.
237
CHAPTER 14. IRIDIS OOPS!
$BFFF 00 BRK
$C000 07 A9 SLO $A9
238
CHAPTER 14. IRIDIS OOPS!
$C002 08 PHP
$C003 8 D F8 BF STA $BFF8
When you start a new game, enemies from the previous game show up in the \=rst
wave. For most people starting out, this will take the form of a few residual 'licker
ships' zapping them just as they're getting started.
This bug happens because the 'wave' data isn't cleared down when a new game starts.
So whatever is in there from the previous game gets used until they're flushed out by
being killed and replaced with the level's proper enemy data.
This isn't a problem for the \=rst game after Iridis Alpha is loaded because the \=rst
level's data is hardcoded in there.
The \=x is simple enough, we initialize the active wave data stored in `activeShipsWave-
DataLoPtrArray` and `activeShipsWaveDataHiPtrArray` with the \=rst level's data when-
ever we start a new game.
LDA \# $0F
STA $D418 ; Select Filter Mode and Volume
JSR ClearPlanetTextureCharsets
JSR I n i t i a l i z e A c t i v e S h i p A r r a y ; Added to fix the bug.
JMP PrepareToLaunchIridisAlpha
; ------------------------------------------------------------------
; InitializeActiveShipArray
; ------------------------------------------------------------------
InitializeActiveShipArray
LDX \# $00
InitializeActiveShipLoop
LDA !` p l a n e t 1 L e v e l 1 D a t a
STA a c t i v e S h i p s W a v e D a t a L o P t r A r r a y ,X
LDA ?` p l a n e t 1 L e v e l 1 D a t a
STA a c t i v e S h i p s W a v e D a t a H i P t r A r r a y ,X
INX
CPX \# $10
BNE I n i t i a l i z e A c t i v e S h i p L o o p
RTS
239
CHAPTER 14. IRIDIS OOPS!
After a minute or two in the title screen, the game enters 'Attract Mode' and plays a
random level on autopilot for a few seconds. If you press F1 during this play you enter
the 'Made in France' pause-mode mini game. If you press F1 again you can now start
playing the level 'Attract Mode' selected at random.
This is because the `CheckKeyboardInGame` routine doesn't try to prevent you from
entering 'Pause Mode' while Attract Mode is running:
CheckKeyboardInGame
LDA lastKey Pressed
CMP \# $40 ; $40 means no key was pressed
BNE KeyWasPressed
LDA \# $00
STA f1WasPressed
ReturnEarlyFromKeyboardCheck
RTS
KeyWasPressed
LDY f1WasPressed
BNE R e t u r n E a r l y F r o m K e y b o a r d C h e c k
LDY a t t r a c t M o d e C o u n t d o w n
BEQ b787C
; If a key is pressed during attract mode , accelerate the
; countdown so that it exits it nearly immediately.
LDY \# $02
STY a t t r a c t M o d e C o u n t d o w n
240
Appendices
241
Jeff Minter's Development diary
for Iridis Alpha
Jeff Minter wrote a diary-of-the-game for Zzap! magazine describing his experience
of getting Iridis Alpha from a basic idea to a \=nished product. It was from published
issue 13 (May 1986) to issue 17 (September 1986).
29 January 1986
I suppose if I have to pick a day and say, 'I started the game', I guess this is it. I'm still
fresh offa the ST, and some idle tinkering with the new assembler I got for the 128 has
resulted in a rather neat star\=eld routine that I'm gonna have in the game. It has 34
stars and they're all generated using just Sprite 0, which leaves plenty of sprites free
and room for some scrolling stuff underneath. Uploaded a demo of it onto Cnet.
30 January 1986
Fixed a bug that was making the interrupts desync if you tried to run the stars backward
under stick control and overlayed a scrolling grid just to see what it looked like. OK
but grid too regular for high speeds. Don't care, only a demo anyway. Uploaded it to
CNET. Think this phase is gonna turn out like Sheep in Space a bit, but faster, much.
Opposing planet surfaces in centre of screen, warp between? Main character on planet
surface will probably be this lovely goat animation that Mo Warden did. It's ace, it even
butts. For planet surface you are the goat --- accelerate, and you metamorphose into
a spaceship for aerial combat. Accelerate again and you can go really fast over the
planet, perhaps auto shields flick on at high speeds? Dunno, I'll see. Never approach a
242
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
game with too much preconceived ideas, I reckon --- let it flow, change, metamorphose.
Oh yeah, I'm probably gonna call this game Iridis Alpha and it'll blast like crazy.
(Tied up with proceedings to the launch of Colourspace on the ST, which took place
on the 10th Feb at the Laserium, so I didn't hack any serious Commodore code during
this time. I needed to practise).
15 February 1986
The design is taking more concrete shape in my head. I am altering the scroll routine
to \=t in with my new plans and splitting the screen in the middle. Got the contraflow
routines going over garbage data just to see if they work. It seems that they do although
there seems to be a slight glitch at high forward accelerations that I'll look into later.
16 February 1986
Spent the day designing some planet data and graphics, and stuffed it in to see what
the routine looks like with some real data. Looked OK but a bit coarse going at 2 pix-
els/frametime minimum increment, so I rewrote the stars and planet scroll so that the
minimum increment is 1 pixel per frame time. It looks a lot better like that. The planet
graphics I did are too coarse, though... I don't really like the way they look, so I may
243
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
well do a new set tomorrow. The current set is based around a block of data 2 chars x
2 chars, and doesn't look that great.
Did write this really neat routine, though. A complete set of planet graphics takes up
512 bytes of character set data, so I just store the de\=nitions for the top planet and
let the computer generate the inverted/reflected set for the bottom planet. Works \=ne
after a little hassle --- reflecting multicolour data is a little awkward --- but saves storing
all those inverted de\=nitions.
17 February 1986
Redid the graphics completely, came up with some really nice looking metallic planet
structures that I'll probably stick with. Started to write the GenPlan routine that'll gen-
erate random planets at will. Good to have a C64 that can generate planets in its spare
time. Wrote pulsation routines for the colours; looks well good with some of the planet
structures. The metallic look seems to be 'in' at the moment so this \=rst planet will go
down well. There will be \=ve planet surface types in all, I reckon, probably do one with
grass and sea a bit like 'Sheep in Space', cos I did like that one. It'll be nice to have
completely different planet surfaces in top and bottom of the screen. The neat thing
is that all the surfaces have the same basic structures, all I do is \=t different graphics
around each one. Got to sort out the scroll limits tomorrow... at the moment you can
shoot right off the end of the planet into garbage data which ain't too cool. Down the
clocktower in the evening, cheap beer, 50p a pint, Courage Best promotion. Well good.
18 February 1986
Fixed scroll limits and did a little more work on the planet generator routine. Scroll
looks really neat, especially at high speed. Very pleasing. Must think about doing the
ship controls now.
19 February 1986
Wrote the code to put Our Hero (tentatively called B-D) (after the little Indian cigarettes
I like) on the planet surface, in the right place, and the right colour, and the right size.
Wrote the animation routines that'll be used to make him move. Hooked him up to the
scroller so that now he walks left and right under joystick command. Started work on
Dark Side set for Colourspace.
244
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
20 February 1986
Put in gravity routines for the robot --- he can now run and jump, too. The grav is nice
and low, graceful leaps. Robot will have to jump over features on the planet surface.
21 February 1986
Ship main control mode now complete, with the addition of the 'spaceship' mode: stop
the little robot, jump him up and push the stick left and right to make him transform
into a spaceship which can really belt over the planet surface. Control feels good, and
I'm pleased, cos that's important.
Weekend in Cardiff with some mates and Colourspace. Dark Side set \=nished and
demonstrated. Was in a car crash. Left my scarf in Cardiff. Freaked people out on
train on way home.
24 February 1986
Sprite plex routines written today to reproduce sprites 1-7 on both planets. Works OK.
Put in the other 'upside-down' ship controls, work \=ne but need upside-down sprites
de\=ning! At the moment it isn't inverted, uses the same Images as the top one.
245
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
25 February 1986
De\=ned the necessary inverted sprites and banged 'em in. They look \=ne, the mirrored
screen and planets scrolling different directions are really bad for the eyes! Tidied up
the joystick control to make it less \=nicky.
26 February 1986
27 February 1986
Put in planet surface \=ring for the top ship. The ship lobs out large round bullets while
it is on the planet surface --- I intend to have certain nasties that can only be properly
killed with ground-based \=re. The routine works but I am losing every other frame due
to interrupt overrun, so I reduce the number of stars on screen to get back about an
inch of interrupt time. This does the trick, all cool. Some faf\=ng around with interrupt
positioning needed too.
28 February 1986
Finished off \=ring routines of upper ship, added the faster, horizontal \=re that the ship
produces while flying above the planet surfaces. The whole thing feels nice, good
\=ring response, just the right spacing between the bullets, and a nice transition from
ground/airborne \=ring modes.
Bone idle.
246
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
3 March 1986
Wrote the extra bullet-handlers to add \=re to the lower ship as well as the upper. The
lower ship has its own, independent bullets, they can't just be reflections of the upper
ship's bullets. The \=ring is ace. Love it. Especially the gravity on the planet-bound \=ring,
but then I always did go for gravity.
Much messing around with graphics for the other planet surfaces, got four de\=ned
so far, Metallic, Brick, Country and Mushroom (although I have only half \=nished the
graphics on Mushroom). The afternoon went learning how to set up the new telly I've
just bought for doing Colourspace on.
6 March 1986
Also in London, getting very knackered doing the Atari show. Continuous lightshows,
on the hour, every hour, for three days. Went to Laserium Sat nite, crashed on mate's
247
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
10 March 1986
Drove to Ludlow for the second stage of the ZZAP! Challenge. Played games all day,
boozer in the evening, crashed the night on La Penn's floor. Ceremoniously burnt the
review of Mama Llama with Penn's own lighter. (Next morning Gary Liddon woke to
\=nd himself covered in the ashes ---Ed)
11 March 1986
Got up, read The Beano went to ZZAP! of\=ces to hassle them for a cup of tea but they
ran out of tea bags so had to go to restaurant down the road. Drove back from Ludlow.
Lots of sheep near Ludlow, you know. Pretty Welsh ones. Set up 8 foot Colourspace
screen in Llab. Had mega session on it.
12 March 1986
Getting stuff ready for taking to France. Will take 128. Probably won't have much
chance to work on Iridis Alpha until I get there, now. Another session on the big Space
rig tonight with some Clocktower regulars.
Settled in here now. Found an ace black run today, you can go for a whole side of Gene-
sis down it without stopping, but on the second time down I hit a tree stump protruding
through the snow, and did an un intentional flying-Yak bit, and landed on a part of my
anatomy I'd rather not have landed on. Fired up the 128, \=nished the Mushroom planet
and designed one more, then did a nice fade between planets routine.
Cloudy weather --- makes skiing hard coz you can't see the bumps. Wrote sonix driver
and started to phase in some of the FX for walking, jumping etc.
248
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Still cloudy. Some snow. Had to buy crappy, French headphones to replace my ex-
cellent Sony pair that I knackered when I got them tangled up in a chairlift, dammit!
Extended the sonix driver and did a few more FX. Sonix take ages, lots of messing
around to do before you get it just right.
Piste all day, back for more SFX, had to rearrange the interrupt sequence to get it all to
\=t in the frametime.
Excellent day --- bright sun, good snow, didn't start work till late coz I went on skiing
so long. Wrote module to link 8 sprites reserved for 'enemy ships' to planetary motion,
and also give them each independent velocities. Did a little work on the Pause mode
when I got back from the bar.
Really bad weather, horrible snow that's nearer rain and so sticky you need to be stand-
ing on a near-vertical incline before you even start moving. Hit the bar early, then back
for mega Guardian session, then a little more work on the pause mode.
REALLY crappy weather. Got soaked skiing, thawed out in the bar. Retired to room
to think about the alien control system while listening to 'The Wall'. Planned it out on
paper ready to code later. Got a neat idea for Phase II of the game, thinking along the
lines of Batalyx Subgame 1 crossed with a sort of overhead view vertically-scrolling
Marble Madness track. Finished off the Pause mode after evening bar. (This'll be the
only Pause mode that's been written TOTALLY under the influence of very expensive
Guinness).
249
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
New snow, much better skiing all round. Linked completed Pause mode to rest of
game. Started on alien control system. Went down bar and got absolutely smashed
and had amazing discussion on Life, the Universe and Everything. Listened to 'Wish
You Were Here' at half-3, in the morning... ace!
Skiing OK, came back after full day's pistebashing to do some AC System hacking. Hit
a terrible awful bug, ran through the code a million times but not got it yet, so down bar
to drown sorrows in copious amounts of Guinness.
Skiing all day then back for the last day's coding in France, I go home tomorrow. Wres-
tled with the same bug for three hours, was despairing, then noticed a single missing
comma in a massive data table that the assembler had neglected, in its in\=nite wis-
dom, to flag as an error during assembly, choosing instead to trash the whole data
table. Inserted comma; end of bug. Guinness.
250
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Trains, trains, trains and Frenchmen, ferry, more train, London, underground, train, bus,
Tadley, tea, crash.
Lazy. Didn't do anything, couldn't because me 128 is in France and I need to buy another
one, and it's Easter holidays.
Went into Reading to get a 128D, got it, intended to return and dutifully do some work,
but instead met some of the Incentive mob, went to pub (fatal mistake for program-
mers), all ended back in Tadley for mega-Colourspace session, so fat chance of getting
any work done there...
Set up new 128D, machine is \=ne but has a noisy fan and sounds like a small but en-
thusiastic Hoover. Did a little more work on the ACM, not much mind you.
Went up to London to see Ariola mob and copped some Amiga stuff off them --- EA
stuff but not Marble Madness yet --- they seem quite keen on Iridis Alpha, especially
my ideas for phase 2.
251
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Decided for a break to do a little work on Phase 2 and give Phase 1 a rest. Started at
11 am, \=nished at 7 am next morning, with a LOT of work done.
Lots more work done today, I now have a tidy little demo of Phase 2, including com-
plete control system and scrolling background in four different colourschemes, and all
inertia routines working. Not bad for a couple days' hacking --- got to bed early tonite,
6am!
Started to get a little sidetracked now, coz I have to get my newsletter done before I go
to Lanzarote on Thursday. HAD A MEGA COLOURSPACE SESSION that \=nished about
half-3 then up writing newsletter till 6. One day maybe I get some sleep.
252
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Did a little more tweaking to phase 2, removing the odd bug I'd found. Then \=nished
newsletter overnight.
All day working on lightshow for performance at Clocktower this evening. Went good.
Got big cheer for 'Stairway to Heaven', and free beer all night.
Preparing to go away tomorrow. It's a hard life having to keep trekking around to the
snow and the beaches, you never seem to get a decent stretch of work done... (hehehe)
Prepared demos to send off to ZZAP! Couldn't get much serious done because I have to
cart all my gear up to London tomorrow for CES Show at Olympia, goes on till Thursday!
Then, thank goodness, I get a clear run till the Commodore show, I will at last be able
to settle down to some decent coding. Holidays and shows are \=ne but tend to disrupt
you something chronic!!!
Too Much
I've decided to drop the individual daily notes for this particular section. I looked at
it and there was just too much stuff that was the same on consecutive days, y'know,
stuff like May 3: Worked on ACONT. May 4: More work on ACONT. May 5: did stuff for
ACONT, etc... etc... What I'll do is try and tell you exactly what's been developed within
the game and why it's there.
253
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
ACONT
This is the bit that I knew would take me ages to write and get glitch free, and the bit
that is absolutely necessary to the functioning of the game. The module ACONT is
essentially an interpreter for my own 'wave language', allowing me to describe, exactly,
an attack wave in about 50 bytes of data. The waves for the \=rst part of IRIDIS are in
good rollicking shoot-'em-up style, and there have to be plenty of them. There are \=ve
planets and each planet is to have twenty levels associated with it. It's impractical to
write separate bits of code for each wave; even with 64K you can run outta memory
pretty fast that way, and it's not really necessary coz a lot of stuff would be duplicated.
Hence ACONT.
You pass the interpreter data, that describes exactly stuff like: what each alien looks
like, how many frames of animation it uses, speed of that animation, colour, velocities
in X--- and Y--- directions, accelerations in X and Y, whether the alien should 'home in' on
a target, and if so, what to home in on; whether an alien is subject to gravity, and if so,
how strong is the gravity; what the alien should do if it hits top of screen, the ground,
one of your bullets, or you; whether the alien can \=re bullets, and if so, how frequently,
and what types; how many points you get if you shoot it, and how much damage it
does if it hits you; and a whole bunch more stuff like that. As you can imagine it was
a fairly heavy routine to write and get debugged, but that's done now; took me about
three weeks in all I'd say.
GENESYS
With ACONT running I had to implement the GENESYS routine, which actually oversees
passing data to ACONT, \=nding out what aliens to unleash depending on what wave
we're on and what planet, arranging for shot aliens to be cleaned up and new ones
sent out to replace them. I had ACONT running with a limited, one-wave only version
of GENESYS at the Commodore show, where a demo of IRIDIS was running non-stop
on our stand. I stayed up till three, the morning of the show, preparing a neato title
screen with one of my sprite star\=elds, the game's title and an animated demo, but
hardly anyone saw the demo anyway coz they were all playing the game.
I was surprised at the response, after all the thing was only a demo, the scoring was
erratic, there was only one wave and you couldn't get killed, but still it was heavily played
at the show. People seemed to get into it, enjoying the raw blasting of the thing. One
lad even begged to buy my development demo off me, he was just getting off on the
blasting and wanted to carry on at home!
254
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Figure A.6: An early version of the Mushroom planet and the DNA demo.
CBM
The Commodore show was fun, as ever: I met a lot of good people there, and did some
serious partying... I don't think Mat or Psy or Wulf are going to forget THAT night for a
while. Everything they say about programmers is TRUE. Make of that what you will.
255
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Fatigue
After CBM was over, I spruced up GENESYS and got it to the point where I could actu-
ally start doing the attack waves. That's more or less what I've been doing up till now:
designing sprite sequences, flight paths, puzzles in some levels, testing 'em to make
sure they are not too dif\=cult for mere mortals. After doing about 40 waves and realis-
ing that there's still another 60 to go, 'Attack Wave Fatigue' starts to show up, but you
just gotta plug on and get 'em done. At the time of writing this I've done 66 of them. I
also did a lot of tweaking to the flight mechanics, and designed the display panel and
got its various gauges and meters running.
The Core
IRIDIS is unusual in offering two scores, one for each ship. Each ship also has an
individual energy bank. As you collide with stuff, you lose energy, naturally. If you lose
it all, you DIE. So you shoot some stuff, and as you kill, so energy gets added to your
ship's bank. You gotta watch it, though, coz if you collect up too MUCH energy, guess
what happens? Yup --- you DIE.
Filling up the CORE entirely will grant you a bonus and allow access to Phase II of
IRIDIS, that vertically-scrolling thing I mentioned in the last set of notes. You'll have to
run the gauntlet of the scrolling course and dump your energy at the end for a mega
bonus, then return to main IRIDIS and continue climbing the levels.
Once I \=nish the attack waves, I gotta tie up all of Phase One before going in to \=nish
Phase II. A rather mean thing is going to be the scoring system --- the faster you fly, the
more points you get for each killing. Standing still and blasting will earn you no points
at all. Flying about at mach III like an F-111 pilot over Libya will net the most points.
Distractions
256
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
Ronnie James Dio in concert (twice) Colourspace II starting to get written on the ST
THRUST Time Bandit, Star Raider, Spy Hunter, Joust on the ST The Incredible Bioxwich
Trip (Too Weird for Words) Invisible Touch Blade Runner DNA (GOTO YAK and DOWN-
LOAD!) My assembler politely informing me that every single branch in the whole bit
of code was out of range, then trashing my disk Compunet and all the heroes thereon
I'm a Hero...
As I write this, IRIDIS is nearly completed. I just gave the \=rst pre-production prototype
to one of the Hewson mob, ready to be duplicated and dished out to the press at the
press launch on Thursday. Getting it ready for the press launch has meant a couple of
all-nighters over the last weekend, but it's worth it --- I got it done, so I'm a hero...
Phase II
Basically, since last time I wrote, I've been doing Phase II most of the time. I \=nished
off the tricky ACONT routine, and de\=ned the data for all 100 attack waves, then I got
down to doing Phase II which was interesting, 'coz it's a vertically scrolling game, and
I don't usually do vert-scrollers.
Although I described it before as a loose cross between Phase I of BATALYX and MAR-
BLE MADNESS, it is actually closer to a cross between Phase I of BATALYX and pinball.
When you're playing it, you get the odd feeling of actually being the pinball as poor Gilby
ricochets off everything in sight at high Delta V. I once saw a pinball game being sold
in America which claimed that 'you are the pinball', but when I played it, it turned out to
be just a scrolling pin-table, and you were the flippers, not the ball. In Phase II of IRIDIS
you are de\=nitely the ball. No doubt about it. And you get hotly pursued by four flying
eyeballs.
257
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
In Phase II there are 256 possible courses, each one different - I worked this trick by
generating each level randomly out of 20 or 30 basic components. But, to ensure that
each level would be consistent from game to game, I seeded the random number gen-
erator with the level number each time the course gets generated. You get distinct
courses for each level, but Level 1 will always look like Level 1, for example, and won't
be random every time you go in, so, you can make maps and learn the courses as
you play. It's neat, 'coz it looks as if I carefully designed and stored all those differ-
ent courses, and all I really did was call the ol' RAN$ routine a couple of times. I love
cheating.
Well 'ard
I've included a neat high score table, and a new system of graphically displaying the
player's progress through the game, as well as progressive opening of the Warp Gate
as the player's skill increases. The game now starts up with only one planet, so that
new players have a chance without it all being too complicated. Once the third wave
(Licker Ships --- well 'ard) is passed, the second planet becomes available. As the player
goes through the game, more planets become available, and he can sustain his game
by earning extra lives on Phase II.
I had a bit of room knocking about under the Kernal so I \=tted in my DNA demo; it's
available from inside MIF (the little pause mode sub-game I wrote in France).
258
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
There's also a title page under there, and a twenty-name Hi Score table (full of default
entries like YAK, PSY and MAT, RATT, and various other Compunetters)...
All that's really left for me to do now is \=nal debug, tidying up of rough edges, and add
a couple of surprises... maybe. I have a week or so to do that, then it's the end-of-July
deadline and if I don't make it I get parts of my anatomy chopped off. I'll do it. I'm a
hero, like I said, without even playing BIGGLES.
Figure A.8: The cute 'n' cuddly Gilbies walk along the surfaces of the duo planets
A good 'un
One thing I like about IRIDIS is that it's got very playable, more so than just about any
other of my games.
I realised this when I passed the point that comes whenever you write a game: there's
always a day when the game stops being just a collection of scroll routines and stuff
that you have to run and debug, and starts to become a real game. You know it's
happened because you \=nd yourself testing the game even when it doesn't need any
testing, and suddenly all your mates know the SYS number to get it started, and use
it frequently. IRIDIS passed that point a while back, and it's now well into the 'lights
out, heavy rock music on, colour on monitor nice 'n' high, let's go give 'em HELL!' stage.
It's great when you've done the high score table and you can rack up a good 'un, too.
Remember way back when I started and had nothing much beyond a star scroll, and I
said that IRIDIS was gonna blast like crazy? I was right... hehe.
259
APPENDIX A. JEFF MINTER'S DEVELOPMENT DIARY FOR IRIDIS ALPHA
I'm off
After I've \=nished, I'm off to Corfu for a couple of weeks well-earned rest doing nothing
but parascending, lying on the beach, and getting paralytic at Mrs Platypus's bar.
And playing SATAN OF SATURN, the local video game. And listening to 'Brothers in
Arms'.
Finally, then, I will leave you, having chronicled the progress of IRIDIS from conception
to birth. If you love a blaster then I think you'll like IRIDIS. It's been heavy work, but
ultimately worth it, I think.
Distractions
IRIDIS ALPHA being brought to you by YAK the hairy, with the support of the Coca Cola
Company, Atari UK, Pink Floyd and Genesis, Heavy Metal, Wadworths 6X, Ratt, Ben,
Mat, Psy, Wulf, etc, Compunet, Dried leaves diffused in boiling water, MIND WALKER,
MARBLE MADNESS, STAR GATE, Taun-Tauns, Camels, Llamas, Sheep and Goats...
MARBLE MADNESS...
Assembled on a C128 using a partially-\=nished JCL assembler and the horrible, slow
Commodore disk drives. Next time I'm gonna use a 6502 X-ASM running in 2.5
Megabytes of RAM on me trusty ST...
260
Sprite Atlas
261
APPENDIX B. SPRITE ATLAS
262
APPENDIX B. SPRITE ATLAS
263
APPENDIX B. SPRITE ATLAS
FLYING COCK FLYING COCK FLYING COCK RIGHT FLYING COCK RIGHT
264
APPENDIX B. SPRITE ATLAS
265
APPENDIX B. SPRITE ATLAS
GILBY TAKING OFF GILBY TAKING OFF GILBY TAKING OFF GILBY TAKING OFF
GILBY TAKING OFF GILBY AIRBORNE LEFT GILBY AIRBORNE TURNINGGILBY AIRBORNE RIGHT
266
APPENDIX B. SPRITE ATLAS
LAND GILBY LOWERPLANETLAND GILBY LOWERPLANETLAND GILBY LOWERPLANETGILBY TAKING OFF LOWERPLANET
267
APPENDIX B. SPRITE ATLAS
268
Enemy Data
Jeffrey Says
After CBM was over, I spruced up GENESYS and got it to the point where I could
actually start doing the attack waves. That's more or less what I've been doing
up till now: designing sprite sequences, flight paths, puzzles in some levels,
testing 'em to make sure they are not too dif\=cult for mere mortals. After doing
about 40 waves and realising that there's still another 60 to go, 'Attack Wave Fatigue' starts
to show up, but you just gotta plug on and get 'em done. At the time of writing this I've done
66 of them. I also did a lot of tweaking to the flight mechanics, and designed the display
panel and got its various gauges and meters running.
This section provides the level data for each wave of enemies in each planet. Figures
are provided to indicate the appearance and movement pattern of the enemy.
269
APPENDIX C. ENEMY DATA
270
APPENDIX C. ENEMY DATA
271
APPENDIX C. ENEMY DATA
272
APPENDIX C. ENEMY DATA
273
APPENDIX C. ENEMY DATA
274
APPENDIX C. ENEMY DATA
275
APPENDIX C. ENEMY DATA
276
APPENDIX C. ENEMY DATA
277
APPENDIX C. ENEMY DATA
278
APPENDIX C. ENEMY DATA
279
APPENDIX C. ENEMY DATA
280
APPENDIX C. ENEMY DATA
281
APPENDIX C. ENEMY DATA
282
APPENDIX C. ENEMY DATA
283
APPENDIX C. ENEMY DATA
284
APPENDIX C. ENEMY DATA
285
APPENDIX C. ENEMY DATA
286
APPENDIX C. ENEMY DATA
287
APPENDIX C. ENEMY DATA
288
APPENDIX C. ENEMY DATA
289
APPENDIX C. ENEMY DATA
290
APPENDIX C. ENEMY DATA
291
APPENDIX C. ENEMY DATA
292
APPENDIX C. ENEMY DATA
293
APPENDIX C. ENEMY DATA
294
APPENDIX C. ENEMY DATA
295
APPENDIX C. ENEMY DATA
296
APPENDIX C. ENEMY DATA
297
APPENDIX C. ENEMY DATA
298
APPENDIX C. ENEMY DATA
299
APPENDIX C. ENEMY DATA
300
APPENDIX C. ENEMY DATA
301
APPENDIX C. ENEMY DATA
302
APPENDIX C. ENEMY DATA
303
APPENDIX C. ENEMY DATA
304
APPENDIX C. ENEMY DATA
305
APPENDIX C. ENEMY DATA
306
APPENDIX C. ENEMY DATA
307
APPENDIX C. ENEMY DATA
308
APPENDIX C. ENEMY DATA
309
APPENDIX C. ENEMY DATA
310
APPENDIX C. ENEMY DATA
311
APPENDIX C. ENEMY DATA
312
APPENDIX C. ENEMY DATA
313
APPENDIX C. ENEMY DATA
314
APPENDIX C. ENEMY DATA
315
APPENDIX C. ENEMY DATA
316
APPENDIX C. ENEMY DATA
317
APPENDIX C. ENEMY DATA
318
APPENDIX C. ENEMY DATA
319
APPENDIX C. ENEMY DATA
320
APPENDIX C. ENEMY DATA
321
APPENDIX C. ENEMY DATA
322
APPENDIX C. ENEMY DATA
323
APPENDIX C. ENEMY DATA
324
APPENDIX C. ENEMY DATA
325
APPENDIX C. ENEMY DATA
326
APPENDIX C. ENEMY DATA
327
APPENDIX C. ENEMY DATA
328
APPENDIX C. ENEMY DATA
329
APPENDIX C. ENEMY DATA
330
APPENDIX C. ENEMY DATA
331
APPENDIX C. ENEMY DATA
332
APPENDIX C. ENEMY DATA
333
APPENDIX C. ENEMY DATA
334
APPENDIX C. ENEMY DATA
335
APPENDIX C. ENEMY DATA
336
APPENDIX C. ENEMY DATA
337
APPENDIX C. ENEMY DATA
338
APPENDIX C. ENEMY DATA
339
APPENDIX C. ENEMY DATA
340
APPENDIX C. ENEMY DATA
341
APPENDIX C. ENEMY DATA
342
APPENDIX C. ENEMY DATA
343
APPENDIX C. ENEMY DATA
344
APPENDIX C. ENEMY DATA
345
APPENDIX C. ENEMY DATA
346
APPENDIX C. ENEMY DATA
347
APPENDIX C. ENEMY DATA
348
APPENDIX C. ENEMY DATA
349
APPENDIX C. ENEMY DATA
Om Planet - Level 1 .
350
APPENDIX C. ENEMY DATA
Om Planet - Level 2 .
351
APPENDIX C. ENEMY DATA
Om Planet - Level 3 .
352
APPENDIX C. ENEMY DATA
Om Planet - Level 4 .
353
APPENDIX C. ENEMY DATA
Om Planet - Level 5 .
354
APPENDIX C. ENEMY DATA
Om Planet - Level 6 .
355
APPENDIX C. ENEMY DATA
Om Planet - Level 7 .
356
APPENDIX C. ENEMY DATA
Om Planet - Level 8 .
357
APPENDIX C. ENEMY DATA
Om Planet - Level 9 .
358
APPENDIX C. ENEMY DATA
Om Planet - Level 10 .
359
APPENDIX C. ENEMY DATA
Om Planet - Level 11 .
360
APPENDIX C. ENEMY DATA
Om Planet - Level 12 .
361
APPENDIX C. ENEMY DATA
Om Planet - Level 13 .
362
APPENDIX C. ENEMY DATA
Om Planet - Level 14 .
363
APPENDIX C. ENEMY DATA
Om Planet - Level 15 .
364
APPENDIX C. ENEMY DATA
Om Planet - Level 17 .
365
APPENDIX C. ENEMY DATA
Om Planet - Level 18 .
366
APPENDIX C. ENEMY DATA
Om Planet - Level 20 .
367
Planet Data
368
APPENDIX D. PLANET DATA
369
APPENDIX D. PLANET DATA
370
APPENDIX D. PLANET DATA
371
APPENDIX D. PLANET DATA
372
APPENDIX D. PLANET DATA
373
APPENDIX D. PLANET DATA
374
APPENDIX D. PLANET DATA
375
APPENDIX D. PLANET DATA
376
APPENDIX D. PLANET DATA
377
APPENDIX D. PLANET DATA
378
18/100,000,000,000,000 Theme
Tunes
379
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
380
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
381
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
382
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
383
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
384
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
385
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
386
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
387
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
388
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
389
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
390
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
391
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
392
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
393
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
394
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
395
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
396
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
397
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
398
APPENDIX E. 18/100,000,000,000,000 THEME TUNES
399
Bonus Phase - Map Segments
400
APPENDIX F. BONUS PHASE - MAP SEGMENTS
$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 $00,$15,$16,$17,$00,$00,$15,$16,$17,$00
$00,$14,$14,$14,$14,$14,$14,$14,$14,$00 $11,$11,$11,$11,$11,$11,$11,$11,$11,$11
$13,$13,$13,$13,$13,$13,$13,$13,$13,$13 $12,$12,$12,$12,$12,$12,$12,$12,$12,$00
$14,$14,$00,$15,$16,$17,$00,$00,$14,$14 $15,$16,$16,$16,$16,$16,$16,$16,$16,$17
401
APPENDIX F. BONUS PHASE - MAP SEGMENTS
$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 $00,$00,$0F,$0F,$0F,$0F,$0F,$0F,$00,$00
$01,$01,$01,$01,$00,$00,$01,$01,$01,$01
$00,$00,$0B,$0B,$0B,$0C,$0C,$0C,$00,$00 $00,$02,$03,$04,$05,$06,$07,$08,$09,$0A
$02,$03,$04,$05,$05,$05,$05,$0B,$0B,$0B $00,$00,$01,$01,$00,$00,$01,$01,$00,$00
402
APPENDIX F. BONUS PHASE - MAP SEGMENTS
$00,$00,$0E,$0D,$00,$00,$0E,$0D,$00,$00 $00,$02,$03,$04,$05,$08,$09,$0A,$0B,$00
$00,$00,$00,$1A,$1A,$1A,$18,$18,$18,$18 $00,$00,$00,$1A,$1A,$1A,$19,$19,$19,$19
$00,$00,$18,$18,$00,$00,$00,$00,$19,$19 $00,$00,$1B,$1B,$00,$00,$15,$16,$17,$00
$15,$16,$17,$1D,$1D,$15,$16,$17,$1D,$1D $14,$14,$1E,$1E,$00,$00,$15,$16,$17,$00
403
APPENDIX F. BONUS PHASE - MAP SEGMENTS
$00,$0B,$0B,$0B,$15,$16,$17,$15,$16,$17 $00,$00,$1D,$1D,$1D,$1D,$1E,$1E,$1E,$1E
$00,$00,$20,$1F,$20,$1F,$00,$00,$11,$11 $00,$00,$20,$1F,$20,$1F,$20,$1F,$20,$1F
$00,$1E,$1E,$1E,$20,$1F,$1D,$1D,$1D,$00 $00,$0C,$0C,$0C,$15,$16,$17,$00,$00,$00
$00,$02,$03,$04,$05,$08,$09,$0A,$0B,$00 $00,$00,$06,$06,$06,$11,$11,$11,$00,$00
404
Bonus Phase - Map Rows
405
APPENDIX G. BONUS PHASE - MAP ROWS
Index Image
10
11
12
13
14
15
406
APPENDIX G. BONUS PHASE - MAP ROWS
Index Image
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
407
408
APPENDIX H. BONUS PHASE - TILESHEET
Cover
410
APPENDIX I. BUMPH
Manual
411
APPENDIX I. BUMPH
412
APPENDIX I. BUMPH
413
APPENDIX I. BUMPH
414
Index
415
INDEX
416
INDEX
417
INDEX
418
INDEX
419
INDEX
420
INDEX
421
INDEX
422
INDEX
Sprites, 45, 60, 61, 63, 67, 71, 108, 139, timeToNextUpdateCounterForWave2,
140, 180, 258--264 220, 222
sprites, 45--47, 55, 57, 59, 61, 63, titleMusicHiBytes, 184, 185, 192
65--68, 71, 72, 107, 108, 136, titleMusicLowBytes, 184, 185, 192
138, 158, 160, 215, 216, 222, titleMusicNoteArray, 182, 183,
223, 232, 238, 241, 242, 245 187--189, 192, 193, 196, 197,
srcOfProceduralBytes, 198 199, 203, 205
star\=eld, 46, 62, 65, 68, 105, 107, 238 titleMusicSeedArray, 192, 196, 197, 199
starFieldInitialStateArray, 152, 160 TitleScreenAnimation, 56--59, 66
StarFieldSkipMSB, 60, 62, 63 titleScreenColorsArray, 64, 68
STARTED, 54 titleScreenGilbiesMSBXPosArray, 67
Stickiness, 135, 149 titleScreenGilbiesMSBXPosOffset, 67
stickiness, 150 titleScreenGilbiesXPosArray, 67, 71
StorePointersAndReturnIfZero, 124 titleScreenGilbiesYPosARray, 67
StorePositionAndReturn, 117 TitleScreenInterruptHandler, 44--46,
StoreRandomPositionInPlanetInPlanetPtr, 56
83, 87--89 TitleScreenMutateStar\=eldAnimationData,
structure, 76, 83--87, 121, 125, 129, 131, 58
132, 172, 182, 186, 191--194, titleScreenStarFieldAnimationCounter,
196, 201, 205 58, 66
structureRoutineHiPtr, 87 titleScreenStarFieldColorsArrayLookUp,
structureRoutineLoPtr, 87 64
structures, 74, 83--85, 87, 111, 130, 240 titleScreenStar\=eldMSBXPosArray, 60,
structureSubRoutineArrayHiPtr, 86, 87 62
structureSubRoutineArrayLoPtr, 86, 87 titleScreenStarFieldXPosArray, 60
subroutine, 87 titleScreenStarFieldYPosArray, 58--60,
surface, 74--81, 83, 87--89, 91, 93, 108, 64
111, 112, 116, 117, 238, titleScreenTextLine1, 53, 54
240--242 titleScreenTextLine2, 53, 54
surfaceDataInactiveLowerPlanet, 89, titleScreenTextLine3, 53, 54
90 titleScreenTextLine4, 53, 54
SwitchToAlternatingWaveData, 156 titleScreenTextLine5, 53, 54
SwitchToNextLayerInPlanet, 85, 86 titleTextLine1, 214
titleTextLine2, 214
TALKING, 214 titleTextLine3, 214
tempHiPtr1, 113, 115, 116 titleTextLine4, 214
temporaryStorageForXRegister, 152 titleTextLine5, 214
tempVarStorage, 108, 140 titleTextLine6, 214
textForInactiveLowerPlanet, 89, 90 titleTextLine7, 214
textures, 84, 93 titleTextMSBXPosArray, 71
timesToNextUpdateForFrequency, 220 titleTextSpriteArray, 71
timeToNextUpdateCounterForWave1, titleTextXPosArray, 71
220, 222 TrySequenceByteValueOf3, 131
423
INDEX
424
INDEX
425
Notes \& References
[3] A History of Llamasoft, Jeff Minter's memoir of the early years of Llamasoft.
[4] C64 Programmer's Reference Guide, The bible for C64 programmers.
[5] Programming the 6502, Rodnay Zaks The book used by Jeff Minter to learn as-
sembly. ""Learning Machine Code ... I used PROGRAMMING THE 6502 by Rodney
Zaks as a good 6502 book, and used the VIC and 64 Programmers Reference
Guides for each machine. The VIC REVEALED is also good for VIC 20 owners.""
[6] Iridis Alpha Source Code, Source code for Iridis Alpha, reverse-engineered by
the author of this book.
[10] The Nature of the Beast, Jeff Minter's regular newsletter to Llamasoft sub-
scribers.
426