ZalaXa — Table of Contents

  1. The Idea
  2. Setting Up the Dev Environment (code)
  3. A Simple Start Menu (code)
  4. First Glimpse of Multicolour (code)
  5. Wide Tiles (code)
  6. Preshifted Sprites (code)
  7. Moving the Ship (code)
  8. Clearing the NIRVANA+ Screen (code)
  9. Proportional Fonts With FZX (code)
  10. A Naïve Scoring Routine (code)
  11. Flashy 1UPmanship (code)
  12. Towards a Scrolling Starfield (code)
  13. You Gotta Roll With It (code)

ZalaXa 1 — The Idea

Talking to my Spec-Chum Andy Dansby the other day, I came up with the idea of doing a blog tutorial series on writing a multicolour ZX Spectrum game.

Maybe not for the complete beginner, as there are any number of excellent tutorials for n00bs out there already. But something where you follow along and let it carry you through into a specialised subject area.

I will talk about the history of multicolour on the Spectrum in more detail later, but for now, let’s say that one of the strengths, and quirks, of the Spectrum is its display format. Unlike other home micros of the era, the Spectrum restricted you to one foreground and one background for every block of 8 by 8 pixels—the primary motivation of the designers being economy: a relatively small amount of precious RAM was taken up by graphics data, leaving more for other program features. Being as popular as it became, this was taken as a creative challenge by 1980s programmers, and many excellent games and other programs were written, making maximum use of both the available memory and these particular visual constraints.

It’s not the only way of doing things, though. Instead of the dedicated Spectrum ULA handling the graphics output, earlier homebrew computers often used the microprocessor to draw the display. This took up much of the available processor time, and involved precise timing—the program would “chase” the TV’s raster beam as it scanned across and down the screen, outputting pixels at the precisely-synchronised time.

At some point during the Spectrum’s heyday, some talented people figured out raster chasing could be used on the Spectrum too—you could let the ULA chip draw the pixels, but you could change the colours on the fly, as you chased the raster beam. In this way you could have one foreground colour in every 8×4, 8×2 or even 8×1 pixel block. Fast forward to 2015, and the smart folks had systemised this to the extent of enabling Einar Saukas to publish the NIRVANA bicolor graphics engine, using 8×2 attributes.

It’s this engine, or rather its fullscreen variant NIRVANA+, that I’d like to base this tutorial series around.

As a taster, here’s Einar’s NIRVANA+ demo program. Download the .TAP here.

ZalaXa 2 — Setting up the dev environment

I’m going to dive right in and talk about setting up my dev environment. All the code and techniques we use with Z80 and ZX Spectrum development can be made to work with any toolchains, and every established developer has their favourites.

I’m no exception, and I’m an enthustiastic user of Simon Brattel‘s Zeus. Zeus has a lineage going back to 1977 on Simon’s homebrew computers, and on the Spectrum from the beginning. Many now-legendary games developers used Zeus back in the day.

Simon was also an early pioneer of cross-development. Many of his classic games, like Halls of the Things and Dark Star, were written on his Z80 homebrew computer, Basil, with a Parasys debugger link between Basil and the Spectrum.

The Windows cross-development IDE has had a continuous pedigree since, as the main IDE for Simon’s electronics and processor design business. And, for the last decade, as a continuously-developed Spectrum-oriented assembler, IDE and emulator with, incidentally, inbuilt support for NIRVANA+.

My personal affiliation to Zeus is based on how easy it makes my development process. I suspect most of this is an affinity between the way Simon and I think—some of this being that our thinking is similar, and some being the way I’m challenged to think differently.

Whatever the philosophical underpinnings, the end result is that I’ll be doing this tutorial series on Zeus. Feel free to follow along with Zeus if you’re a relative beginner, or adapt my examples for your own favourite IDE, assembler and emulator.

Installing Zeus is easy. Download the latest version of zeus.exe here, stick it in a directory and make a shortcut to it. If you’re on Windows 7/8/10, you’ll have to do the usual unblocking the first time you run the program. Zeus works well on Linux and MacOS under Wine, although I only use Windows myself.

I’ve made a GitHub repository for this series of tutorials. Familiarize yourself with the git version control system, and the process of cloning a repository, if you don’t already know this. I use TortoiseGit on Windows, and find it very simple and intuitive.

I will establish a convention that the source for the latest version of ZalaXa will be inside the /src directory, and the source for a particular tutorial post will be inside the /tutorials directory—this posts code is found at /tutorials/part2, for example.

Once you have cloned the repository and updated it to the latest version, choose File >> Open in the Zeus menu, and open \tutorials\part2\main.asm. You will see this code—the bare minimum template to assemble a Z80 program and run it in Zeus’s emulator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; main.asm
 
zeusemulate             "48K"                           ; Tell the Zeus emulator to be a 48K Spectrum
zoLogicOperatorsHighPri = false                         ; Zeus assembler options
zoSupportStringEscapes  = false                         ;   (see Config tab
zoAllowFloatingLabels   = false                         ;   for details)
Zeus_PC                 = Start                         ; Tell the Zeus emulator where to start running code at
org                     $8000                           ; Tell the Zeus assembler where to place the code
 
 
 
Start                   proc                            ; A named PROCedure (also our start point)
                        jp Start                        ; Go into an endless loop!
pend

Click the Assemble Then Emulate button:

You should be rewarded with an assembler status message saying this:

nErrors = 0 nRedef = 0 nUndef = 0

and a completely blank white spectrum screen in the Zeus emulator:

And on that bombshell, we will continue in the next post!

ZalaXa 3 — A Simple Start Menu

I bet everyone is eager to dive right in and start with some multicolour goodness. I definitely am. We’re not quite there yet, though. Lets’s set up a simple start menu first. There’s a good reason for this, which will be apparent later 🙂

Let’s have a look at the code for part 3 in GitHub. The first section, which deals with configuring Zeus and setting org, is the same as part 2.

The next section expands on the starting code that gets run as soon as we jump into our program:

12
13
14
15
16
Start                   proc                            ; A named PROCedure (also our start point)
                        ld sp, Start                    ; Put our stack right below the program
                        Border(Black)                   ; Set the border to black using a helper macro
                        call ClsAttr                    ; Call another named procedure to do a fast CLS (like GOSUB)
                        Print(MenuText, MenuText.Length); Print text on the screen using ROM routines

Immediately, on line 13 we set our stack to just below the program. This is somewhat of a personal preference, but stacks are generally either kept above or below our programs, and there’s a possibility this might end up being a 128K-only game. If that’s the case, the top 16K of RAM becomes quite valuable as it can be switched in and out with other 16K banks. Having the stack in this area tends to put a crimp on this.

Line 14 calls a macro to set the border to black. Hopefully this is self-explanatory. I like to treat macros as opportunities to improve code readability—although the opposite can be true too! The macro, further down at line 45, is the standard way of doing this in Z80.

45
46
47
48
Border                  macro(Colour)                   ; Macro (makes the code more readable) to set border
                        ld a, Colour                    ; Set a to the colour desired
                        out (ULAPort), a                ;   and output it to the ULA Port (defined in constants)
mend                                                    ; No RET is needed - this code is inserted inline

ULAPort is a constant I use instead of $FE (decimal 254), purely to make the code self-descriptive. The other readability win here is that the macro is parameterized—whatever you pass in as the value of Colour gets substituted. As I noted in the comments, there is no difference between writing Border(0), and writing:

                        ld a, 0                         ; Set a to black
                        out ($FE), a                    ;   and output it to the ULA Port

Incidentally, Zeus’s macro expansion is pretty clever. I could use my macro, unchanged, with Border(b) (without any quotes around the b). As b is a valid Z80 register, and ld a, b is a valid opcode, Zeus will assemble exactly that! It’s even smart enough to know that Border(hl) would result in ld a, hl—and grumble mightily that this is an invalid opcode.

But where were we? Oh yes. call ClsAttr on line 15 is a function. The code in this function is big enough that we don’t want to repeat it unnecessarily by inlining it every time we clear the screen. Nor does it take any parameters, so let’s assemble it once, invoke it with call, and let it return to the next line with ret, exactly like GOSUB/RETURN in BASIC. The function looks like this:

33
34
35
36
37
38
39
40
41
ClsAttr                 proc                            ; Do an attribute CLS using LDIR block copy
                        xor a                           ; Set a to 0 (blank ink, black paper)
                        ld hl, AttributeAddress         ; Address to start copying from (start of attributes)
                        ld de, AttributeAddress+1       ; Address to start copying to (next byte)
                        ld bc, AttributeLength-1        ; Number of bytes to copy (767, all the attirbutes)
                        ld (hl), a                      ; Set first byte to attribute value
                        ldir                            ; Block copy bytes
                        ret                             ; Return from the procedure (like RETURN)
pend

This is a pretty easy method to copy the same value into a range of bytes. Again, AttributeAddress and AttributeLength are defined as constants for readability.

On line 16, Print(MenuText, MenuText.Length) is another macro. This one is highly parameterized, and as such really improves readability.

52
53
54
55
56
57
58
59
Print                   macro(TextAddress, TextLength)  ; Macro to print text on the screen using ROM routines
                        ld a, ChannelUpper              ; Channel 2 (defined in constants) is the upper screen
                        call CHAN_OPEN                  ; Open this channel (ROM routine)
PrintLoop:              ld de, TextAddress              ; Address of string to print
                        ld bc, TextLength               ; Length of string to print
                        call PR_STRING                  ; Print string (ROM routine)
 
mend

This makes use of two ZX Spectrum ROM routines, to print a string of text on the upper screen. I won’t say too much about them, as they’re often used in other programs.

You’ll notice I’m using dot notation to refer to MenuText.Length in the macro invocation. MenuText is another procedure which I’ve used to encapsulate some data that will be assembled into bytes in RAM, and also some constants. All the constant definitions inside a proc are local to that proc, so there’s an opportunity to namespace your labels for greater semantic clarity. I’m also making use of colour and attribute constants, so that the code resembles a BASIC PRINT statement as much as possible.

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
MenuText                proc                            ; Named procedure to keep our print data tidy
                        db At, 7, 13                    ; These codes are the same as you would use
                        db Paper, Black, Bright, 1      ;   with Sinclair BASIC's PRINT command
                        db Ink, Red, "Z"
                        db Ink, Yellow, "A"
                        db Ink, Cyan, "L"
                        db Ink, Magenta, "A"
                        db Ink, White, "X"
                        db Ink, Green, "A"
                        db At, 21, 6
                        db Ink, Yellow, "PRESS "
                        db Ink, White, "SPACE"
                        db Ink, Yellow, " TO START"
Length                  equ $-MenuText                 ; Let Zeus do the work of calculating the length
pend                                                   ; ($ means the current address Zeus is assembling to)

After printing the menu, the next section goes into an endless loop until the space key is pressed.

17
18
19
20
21
22
23
WaitForSpace:                                           ; All labels inside procedures are local to that procedure
                        halt                            ; Wait for the next 1/50th second interrupt (like PAUSE 1)
                        ld bc, zeuskeyaddr(" ")         ; Get the IO address to input
                        in a, (c)                       ; Read those 5 keys
                        and zeuskeymask(" ")            ; AND with the bit for SPACE
                        jr z SetupGame                  ; If it's zero the key is pressed
                        jp WaitForSpace                 ; Otherwise check keys again

Again, this is standard Spectrum Z80 stuff for reading keys. Zeus has nice zeuskeyaddr and zeuskeymask functions to make it slightly easier to code and read.

The final section does a clear screen and goes into another endless loop after the space key is pressed. This will probably get replaced in the next tutorial, but I wanted it to be clear that pressing space actually does something.

24
25
26
27
28
29
SetupGame:
                        call ClsAttr                    ; Clear the screen to prove we pressed space
EndlessLoop:
                        halt
                        jp EndlessLoop                  ; Go into an endless loop (for now...)
pend

The code at the end is worth mentioning briefly—Zeus can create TAP, TZX, SNA and Z80 files directly from code. What I’m doing here is similar to what Pasmo does with it’s --tapbas mode. Where Zeus comes into its own, though, is generating tape files for 128K Spectrums. We will see later, perhaps 🙂

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
BinPath                 equ "..\bin"                    ; Relative to main.asm
TapFile                 equ BinPath+"\ZalaXa.tap"       ; Filename of tap file
 
 
 
; Make tape file
End                     equ $                           ; Calculate the last byte of our program
Size                    equ End-Start                   ; Count the bytes to save to tape
output_tap              TapFile, "ZalaXa", "seven-fff.com/zalaxa", Start, Size, 2, Start
                                                        ; Make a .TAP file. Parameters:
                                                        ;   1) the file name
                                                        ;   2) the name of the BASIC loader program
                                                        ;   3) a comment that goes in the TAP header
                                                        ;   4) Start of machine code program
                                                        ;   5) Length of machine code program
                                                        ;   6) Zeus mode 2 files use the standard ROM loader
                                                        ;   7) Tell the BASIC loader what to run with RANDOMIZE
                                                        ;       USR (like Zeus_PC tells the Zeus emulator)

Well, that was a very long, very explain-y post. Next time I will talk a little more about NIRVANA+ and introduce some multicolour code!

ZalaXa 4 — First Glimpse of Multicolour

Well here we are! If you pull the latest commits from GitHub. you should see the code for part 4 of the tutorial.

If you Assemble And Run \tutorials\part4\src\main.asm in Zeus, you’ll see the familiar menu we wrote in part 3. This time, if you press space, hopefully you’ll see some multicolour animation!

Let’s try and have a look at what I added. First of all, you will notice there are now eight source files, in which I’ve reorganised the previous code, and added some new code:

Many assemblers allow this, and it does help to organise your code. In Zeus, it’s done with the include directive:

25
26
27
28
29
30
31
32
33
; main.asm
 
include                 "menu.asm"
include                 "sprites.asm"
include                 "utilities.asm"
include                 "database.asm"
include                 "constants.asm"
include                 "macros.asm"
include                 "nirvana+.asm"

The last file (which is last deliberately, and I’ll explain why later) is Einar’s source code for NIRVANA+, taken directly from his Dropbox drive. We won’t look at this in detail for now—suffice it to say it has an NIRVANA+ API, and we can call routines to do magic multicolour things.

The /tiles/nirvana+.btile file also comes from Einar’s Dropbox. This file contains 16×16 pixel multicolour tiles, in a special 8×2 attribute format tailored for NIRVANA.

ZX-Paintbrush is a graphical editing tool we will find indispensible for working with btiles. Download it from Klaus Jahn’s website here (by clicking on the image).

If you open the file in ZX-Paintbrush, you can see it contains the 17 sprites from part 1 (by Dave “R-Tape” Hughes):

A slightly closer examination reveals the important detail—each 8×2 pixel block can have exactly one background and foreground colour (and its own brightness and flash value), enabling the multicolour magic!

All I’m doing here is cycling between the first two btiles in this set, to do some simple but effective animation. First we clear the standard 8×8 attributes (for a fast clean CLS), then we set the index and coordinates of NIRVANA+ sprite A (there are eight of them) to the first btile in the top left hand corner, then we enable NIRVANA+:

17
18
19
20
21
22
23
24
25
26
27
; menu.asm
 
SetupGame               proc
                        call ClsAttr                    ; Clear the 8x2 attributes for a fast CLS before setup
                        ld a, BTile.NirvanaDemo         ; BTile.NirvanaDemo is 0 - the index of the first tile in the set
                        ld (Sprites.AIndex), a          ; Set NIRVANA+ sprite A to this sprite index
                        ld hl, $1000                    ; LSB is $00 (the column), MSB is $10 (the line, decimal 16)
                        ld (Sprites.AColumn), hl        ; Set NIRVANA+ sprite A coords to 0, 16
                        call NIRVANA_start              ; Enable NIRVANA+
                        ret
pend

NIRVANA+ horizontal coordinates range from 0 to 31—standard Spectrum character columns. Vertical coordinates range from 0 to 215. NIRVANA+ never draws anything multicolour on the first character row. It also allows you to draw a tile completely off the top or bottom of the screen, so the visible range is actually between 16 and 199. Don’t worry too much about this for now, just take note that pixel line 16 is the top of the visible NIRVANA+ screen.

Our main loop now has a call to AnimateDemo, 50 times a second:

17
18
19
20
21
22
23
; main.asm
 
Loop:
                        halt                            ; Wait until the next 50th second frame
                        call AnimateDemo                ; Animate our monster
                        jp Loop                         ; Go into an endless loop (for now...)
pend

The AnimateDemo routine reads the lowest byte of the Spectrum ROM’s frame counter, which is increment every 50th of a second by NIRVANA’s interrupt handler—when it goes above 255 it wraps back round to zero. It turns that into a number between 0 and 7 with a fancy bitwise modulus calculation using the and opcode. There are 32 lots of 8 in the 256 possible values of a byte, so this is a clever fast way of counting in eights (well, between 0 and 7).

So, for 7 out of every 8 frames, it returns without doing anything.

3
4
5
6
7
8
9
10
11
12
13
14
15
; sprites.asm
 
AnimateDemo             proc
 
                        ld a, (FRAMES)                  ; Read the LSB of the ROM frame counter (0.255)
                        and %00000111                   ; Take the lowest 3 bits (effectively FRAMES modulus 8),
                        ret nz                          ;   and return 7 out of every 8 frames.
 
                        ld a, (Sprites.AIndex)          ; For every 8th frame, read Sprite A's tile index,
                        xor %00000001                   ;   alternate between (0 => 1 => 0 => 1=> etc),
                        ld (Sprites.AIndex), a          ;   then save it back.
                        ret
pend

But for one frame in every eight, it flips the btile index of sprite A between 0 and 1 using a bitwise xor operation. In other words we do a two-frame animation, alternating every 0.16 of a second!

We won’t keep any of this code in the final game, but it neatly demonstrates two things:

  • we don’t need to do anything too fancy to set up NIRVANA+, and
  • a little animation really helps bring graphics to life.

More next time!

ZalaXa 5 — Wide Tiles

In this tutorial I’d like to take a little step sideways, and look at something that might come in useful later, and which will pave the way to some fun stuff in part 6.

The btiles we looked at in part 4 were 16×16 pixel size. NIRVANA+ also supports 24×16 wide tiles, in a .wtile file format. Take a look at \tutorials\part5\tiles\monster.wtile in the ZX-Paintbrush graphical editor.

These are the same two monster tiles we animated in part 4, in wtile format instead of btile format, repeated four times each. We will come back to the reason for the repetitions in part 6!

In order to use these wide tiles, I’ve made a few small changes to the part 4 code. Defining the ENABLE_WIDE_SPRITE and ENABLE_WIDE_DRAW constants enables wide tiles in NIRVANA+.

40
41
42
43
; constants.asm
 
ENABLE_WIDE_SPRITE      equ true
ENABLE_WIDE_DRAW        equ true

The database has a new WTile table importing the new tiles. (I suppose it doesn’t look much like a table, but I tend to think of any data in a regular repeating format as a table—the curse of having been a database programmer for many years. In this particular case, each btile is always 48 bytes long, and a wtile is 72 bytes long, so files containing tiles are totally made up of tabular data!)

14
15
16
17
18
19
20
21
22
23
; database.asm
 
WTile proc
::WIDE_IMAGES:
;                                                           BTile         Best
;          FileName                                       Indices    Viewed As     Notes
Monster equ ($-WTile)/Sprites.BTileLen
import_bin "..\tiles\monster.wtile"                     ; 000-007        4 x 2     Preshifted monster
 
pend

You may notice that they’re in a separate table, and the WTile indices start again at 0, even though we’ve left the BTiles in the program. This is because NIRVANA+ treats them separately—in fact you can have 255 of each type of tile, and can even mix and match them.

Finally, I’ve tweaked the AnimateDemo routine to alternate the tile indices between 0 and 4. Technically-speaking, the xor opcodes do a boolean exlusive-or operation on numbers. xor %00000100 (or xor 4 in decimal) flips bit 2 of the a register, leaving the other bits unchanged. In this case, it comes to the same thing as repeatedly adding or subtracting 4 from a. Because of the cunning way our wtiles are laid out (in sets of powers of two), the effect is to select the first monster in each animation frame.

3
4
5
6
7
8
9
10
11
12
13
14
15
; sprites.asm
 
AnimateDemo             proc
 
                        ld a, (FRAMES)                  ; Read the LSB of the ROM frame counter (0.255)
                        and %00000111                   ; Take the lowest 3 bits (effectively FRAMES modulus 8),
                        ret nz                          ;   and return 7 out of every 8 frames.
 
                        ld a, (Sprites.AIndex)          ; For every 8th frame, read Sprite A's tile index,
                        xor %00000100                   ;   alternate between (0 => 4 => 0 => 4 => etc),
                        ld (Sprites.AIndex), a          ;   then save it back.
                        ret
pend

The end result looks just the same as part 4, but we have a new technique up our sleeves now!

In the next part, I’m going to make our sprite move around the screen—animation in space, as well as the animation in time we already have.

ZalaXa 6 — Preshifted Sprites

At the end of part 5, I promised to make our sprite move around the screen this time.

To that end, in the code for part 6, I’ve added a call to new MovePlayer routine to the main loop.

17
18
19
20
21
22
23
; main.asm
 
Loop:                                                   ;   of a game and the start of the next one
                        halt                            ; Wait until the next 50th second frame
                        call AnimateDemo                ; Animate our monster
                        call MovePlayer                 ; Move up/down/left/right
                        jp Loop                         ; Go into an endless loop (for now...)

The MovePlayer routine is fairly long. Let’s split it into two parts:

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
; sprites.asm
 
MovePlayer              proc
                        ld de, 0                        ; d (vertical) and e (horizontal) will hold -2/0/+2 movement offsets
                        ld bc, zeuskeyaddr("OP")        ; Get the I/O port address for O (left) and P (right)
                        in a, (c)                       ; Read keys
                        ld b, a                         ; Save value for Right check
                        and zeuskeymask("O")            ; Mask out everything but O
                        jp nz, Right                    ; If result is non-zero O was not pressed, so check Left key
                        ld e, -2                        ; otherwise set horizontal offset to -2
                        jp Up                           ; and skip Left key check
Right:
                        ld a, b                         ; Retrieve Left/Right keypress reading
                        and zeuskeymask("P")            ; Mask out everything but P
                        jp nz, Up                       ; If result is non-zero P was not pressed, so check Up key
                        ld e, +2                        ; otherwise set horizontal offset to +2
Up:
                        ld bc, zeuskeyaddr("Q")         ; Get the I/O port address for Q (up)
                        in a, (c)                       ; Read keys
                        and zeuskeymask("Q")            ; Mask out everything but Q
                        jp nz, Down                     ; If result is non-zero Q was not pressed, so check Down key,
                        ld d, -2                        ; otherwise set vetical offset to -2.
Down:
                        ld bc, zeuskeyaddr("A")         ; Get the I/O port address for A (down)
                        in a, (c)                       ; Read keys
                        and zeuskeymask("A")            ; Mask out everything but A
                        jp nz, Move                     ; If result is non-zero Q was not pressed, so go ahead
                        ld d, +2                        ; otherwise set vetical offset to +2
Move:

Hopefully the code is commented well enough to understand it. All you really need to get is that the QAOP keys are read, then some vertical and horizontal directional offsets are calculated, having values -2, 0 or +2 depending on which keys were pressed.

This means we will be moving around the screen in steps of two pixels. Why not steps of one pixel, you ask? Well, the nature of NIRVANA+ with its 8×2 attributes makes it harder to move vertically in one pixel steps—we could do it, but the colours of the sprite tile would probably get messed up in the odd-numbered pixel lines. This is essentially attribute clash, but on a two pixel grid instead of the classic eight pixel grid.

We can certainly move horizontally in one pixel steps, but it would create an imbalance in most games to move at half the speed horizontally than you did vertically. So, as a design decision, I’ve made both axes move in steps of two pixels.

The second half of the routine is concerned with calculating the coordinates for the player sprite (now sprite B). It also introduces a second blank sprite (sprite A).

It’s important to know that NIRVANA sprites are drawn every frame, in their last position. But they’re never undrawn—you have to do that part yourself. It’s a pretty flexible way of doing things, and opens up a few techniques for doubling or tripling the effective number of sprites you have available. In our case, though, we want to blank out the trails our sprite leaves as it moves around. In this demonstation, the simplest way to do that is with a second blanking sprite.

Remember earlier I said we have eight available “for free” as part of standard NIRVANA+)? In fact that was for square btiles; only five wtiles can be drawn “for free”. For now this doesn’t matter, as we’re still exploring concepts and options. We will look at some ways to break past this limit later. Here’s the code1:

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
; sprites.asm
 
Move:
Y equ $+1:              ld a, SMC                       ; <SMC Read the previous player vertical position
                        ld (Sprites.ALine), a           ;      and set the blank sprite vertical position to this.
                        add d                           ; Then add the vertical offset (-2/0/+2)
                        cp -2                           ; Check if we went negative
                        jp nz, MaxY                     ; If not, carry on,
                        xor a                           ;   otherwise set vertical position to 0 (the minimum).
MaxY:
                        cp 200                          ; Check if we went below the bottom of the screen
                        jp c, SetY                      ; If not, carry on,
                        ld a, 200                       ;   otherwise set vertical position to 200 (the maximum).
SetY:
                        ld (Sprites.BLine), a           ; Set the player sprite to the new verified vertical position
                        ld (Y), a                       ;   and also save it for next time (for the blank sprite
                                                        ;   to be drawn underneath the player sprite)
X equ $+1:              ld a, SMC                       ; <SMC Read the previous player horizontal position
                        call CalculatePlayerX           ; Call the routine to calculate the column and tile offset
                        ex af, af'                      ; The column is returned in a'
                        ld (Sprites.AColumn), a         ; Set column for blank sprite (based on previous X position)
 
                        ld a, b                         ; CalculatePlayerX saved the previous X position in b, restore it
                        add a, e                        ;   then add the horizontal offset (-2/0/+2)
                        cp -2                           ; Check if we went negative
                        jp nz, MaxX                     ; If not, carry on,
                        xor a                           ;   otherwise set vertical position to 0 (the minimum).
MaxX:
                        cp 238                          ; Check if we went beyond the right of the screen
                        jp c, SetX                      ; If not, carry on,
                        ld a, 238                       ;   otherwise set vertical position to 238 (the maximum).
SetX:
                        ld (X), a                       ; Save it for next time (for the blank sprite underneath)
                        call CalculatePlayerX           ; Call the routine again for the new player X position
AnimOffset equ $+1:     add a, SMC                      ; The X offset is returned in a, add the animation offset from AnimateDemo
                        ld (Sprites.BIndex), a          ; Set tile index for player sprite (current position)
                        ex af, af'                      ; The column is returned in a'
                        ld (Sprites.BColumn), a         ; Set column for player sprite (current position)
                        ret
pend

This second half of the routine does bounds checks to stop the sprite going off the screen. If we didn’t, we’d crash the program quite badly! The reason for this is that NIRVANA+’s multicolour attribute data is interspersed tightly with the code that draws the attributes. If we start going off the map, we will overwrite important code pretty soon.

This routine calls another routine called CalculatePlayerX, twice—once for the blanking sprite and once for the player sprite. I’ve done this to try and reduce duplicated code, and the calculations for both are more or less the same (except the blanking sprite always uses the same tile index). This is the routine:

84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
; sprites.asm
 
CalculatePlayerX        proc                            ; X coordinate is passed in a (0.255)
                        ld b, a                         ; Save the X coordinate for later
                        and %11111000                   ; Round it to the nearest eight
                        rra                             ;   then
                        rra                             ;   divide
                        rra                             ;   by eight to get the column (0..31).
                        ex af, af'                      ; Column is returned in a'
                        ld a, b                         ; Get the X coordinate back
                        and %00000110                   ; Get the remainder after rounding it to the nearest eight
                        rra                             ;   then divide that by two to get the tile index (0/2/4/6)/
                        add a, WTile.Monster            ; Tile index is returned in a
                        ret
pend

This does something interesting. Remember that we made eight monster tiles—four for each animation cycle in part 5?

Notice that we’re storing our X coordinates in the range from 0 to 255. But NIRVANA+ actually takes a column number in the range 0 to 31 for all its sprite and tile operations. One way to look at this is like a fraction—the column is the whole number part, and the pixel position within the column (between 0 and 7) is the fractional part. Actually, this is a fraction, albeit a binary2 one!

It turns out that the bottom-most three bits of the X coordinate hold the fractional part—the maximum value three bits can hold is %111, or decimal 7, which is our maximum pixel position within the column. This means that the top-most five bits contain the column number.

In order to separate out these two numbers from the X coordinate byte, all we need are a few bitwise operations. The column number can be obtained by dividing by eight. Dividing by powers of two is easy in Z80, using one of the shift-right opcodes. We can also do it, faster, with the rotate-right opcodes—provided none of the bits being shifted out contain 1s (otherwise they’ll be shifted back in at the left hand side, giving a number larger than 31!) I’m ensuring this doesn’t happen with and %11111000, which clears the bottom-most three bits before rotating them.

Obtaining the fractional part is easier—we just have to mask the X coordinate with and %00000110, to clear all the bits apart from the bottom three3. If we had seven tiles, each shifted a single pixel, this would be perfect. But we’re moving in two pixel steps, so we need to divide this by two with rra. Notice that there isn’t any remainder4 when dividing by a power of two in Z80—we’re effectively rounding down after dividing, which is exactly what we need here!

This preshifting technique is quite common in 8-bit graphics programming. Generally, it makes a compromise between speed (all those shifted versions could be calculated on the fly, but it would be slow) and size (four or eight times the number of tiles!). R-Tape has written a more comprehensive tutorial about preshifting and sprite animation. You may find this interesting—it doesn’t relate specifically to NIRVANA, but the techniques are the same.

With the column number (0..31) and tile offset (0/2/4/6) we have everything we need to calculate the correct tile to make it look as if we’ve moving in two pixel steps. The only thing remains is to add in the animation cycle offset (0 or 4). I’ve modified the AnimateDemo routine (which gets called before MovePlayer every frame) to calculate this offset and write it directy into the MovePlayer routine:

3
4
5
6
7
8
9
10
11
12
13
14
15
; sprites.asm
 
AnimateDemo             proc
 
                        ld a, (FRAMES)                  ; Read the LSB of the ROM frame counter (0.255)
                        and %00000111                   ; Take the lowest 3 bits (effectively FRAMES modulus 8),
                        ret nz                          ;   and return 7 out of every 8 frames.
 
                        ld a, (MovePlayer.AnimOffset)   ; For every 8th frame, read player's tile offset,
                        xor %00000100                   ;       alternate between (0 => 4 => 0 => 4 => etc),
                        ld (MovePlayer.AnimOffset), a   ; SMC>  then save it back.
                        ret
pend

You should be able to see that the place it writes it into isn’t strictly speaking a piece of data; it’s the operand of a ld a, N opcode:

75
76
77
; sprites.asm
 
AnimOffset equ $+1:     add a, SMC                      ; The X offset is returned in a, add the animation offset from AnimateDemo

For this reason, the technique is usually called self-modifying code. I try and maintain the convention that code doing the modifying is marked with SMC> in the comments, and the code getting modified is marked with <SMC. The technique doesn’t work everywhere (in ROM code, for example), and it can make debugging much harder. But it’s very useful when well-controlled. I’ve defined a SMC equ 0 constant, which lets me semantically mark the code too. If I’ve written things correctly, the value always gets modified to it’s correct starting value during setup, or else having it start at zero is harmless.

The only other change I’ve made is to add the blank tile:

21
22
23
24
; database.asm
 
Blank equ ($-WTile)/Sprites.BTileLen
import_bin "..\tiles\blank.wtile"                       ; 008-008        1 x 1     Blank

This is, as you’d expect, a tile with no pixels, having black ink and paper colours. How does this so neatly erase the previous position of the player sprite? Well, NIRVANA+ sprites have an order of precedence, or z-order. Sprite A appears underneath the other sprites, and Sprite E (or H when we’re using btiles) appears on top of everything else. Sprites can partially or fully overlap, so this takes care of the messiness. It’s not a particularly good use for a scarce resource, but we’re still just exploring techniques here. When we find we’ve used all the sprites and still need more, let’s revisit this.

If you Assemble Then Emulate the part 6 code, you should be able to move our monster around the screen with the QAOP keys:

I’ve set the vertical limits (0 and 200) to let the sprite to go completely off the top and bottom of the screen. NIRVANA+ allows this because it allocates a small buffer of bytes either side of the attribute data. You can’t do the same thing with the horizontal axis, however NIRVANA (without the plus) has an 8 pixel buffer on the left and right sides. It does this at the expense of only having 30 columns of multicolour data instead of 32. This works really well for some game designs, so the choice is yours which to use. In the main, NIRVANA works pretty much the same as NIRVANA+.

If you want to keep the sprite onscreen at all times, you can adjust the vertical limits by 16:

46
47
48
49
50
51
52
53
54
55
56
57
58
; sprites.asm
 
Y equ $+1:              ld a, SMC                       ; <SMC Read the previous player vertical position
                        ld (Sprites.ALine), a           ;      and set the blank sprite vertical position to this.
                        add d                           ; Then add the vertical offset (-2/0/+2)
                        cp 14                           ; Check if we went negative
                        jp nz, MaxY                     ; If not, carry on,
                        ld a, 16                        ;   otherwise set vertical position to 0 (the minimum).
MaxY:
                        cp 184                          ; Check if we went below the bottom of the screen
                        jp c, SetY                      ; If not, carry on,
                        ld a, 184                       ;   otherwise set vertical position to 200 (the maximum).
SetY:

That’s it for part 6! Next time I will be giving some thought to the type of game we will make (the clue just might be in the name…)

ZalaXa 7 — Moving the Ship

Just a short post today. As I’m sure you realised, I wanted to try and make a Galaga-like game for this multicolour tutorial series.

I designed a wide ship sprite, with two variants to give a flickering animation effect. My first attempt used square btiles, but I didn’t much like how it looked:

The biggest problem with this is the graphic can only be 10 pixels wide when preshifting left and right within a 16 pixel wide tile—any more and you need wide tiles to capture the full range of shifts. This is even worse when you’re preshifting in one pixel steps—then, the graphic can only be 9 pixels wide. The ship doesn’t look too bad in ZX-Paintbrush, but it looked way too small and unimposing on the Spectrum screen.

The original arcade Galaga operated in portrait mode, so perhaps this has a bearing on how they designed the sprites.

My wide tile ship looks better, I think. With 24 pixel wide tiles, the graphic can be a maximum of 18 pixels wide when preshifting in two pixel steps1. It reminds me of the God Phoenix from Battle of the Planets/Gatchaman.

This will cause us difficulties later, and we’ll probably have to squeeze every last T-state out of the machine to put aliens and bullets on the screen. Oh well, it’ll be a challenge!

I modified the code from part 6 by removing the lines of MovePlayer that related to moving up and down. I also speeded up the animation to cycle every four frames, to give more of a dynamic flickering effect:

15
16
17
18
19
; sprites.asm
 
                        ld a, (FRAMES)                  ; Read the LSB of the ROM frame counter (0.255)
                        and %00000011                   ; Take the lowest 2 bits (effectively FRAMES modulus 4),
                        ret nz                          ;   and return 3 out of every 4 frames.

Then I added some code to check the M key and flash the border red in every frame it is pressed. This won’t happen in the real game, but it feels like a small amount of progress to make something visual happen early on.

5
6
7
8
9
10
11
12
13
14
15
; sprites.asm
 
                        ld bc, zeuskeyaddr("M")
                        in a, (c)
                        and zeuskeymask("M")
                        ld b, Black
                        jp nz, SetBorder
                        ld b, Red
SetBorder:
                        ld a, b
                        out (ULAPort), a

That’s pretty much it. I think the result looks quite nice.

More next time. Happy new year!

ZalaXa 8 — Clearing the NIRVANA+ Screen

If you take a screenshot of the part 7 code and look at it in a screen editor such as ZX-Paintbrush, you’ll see we’re setting attributes to black-on-black1 but not clearing pixels. This is going to cause us problems later when we start writing to the screen, so let’s address that now.

We’ll still clear the 8×8 attributes, because that happens very fast and gives a nice snappy transition. But let’s add a ClsNirvana routine that we call afterwards:

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
; utilities.asm
 
ClsNirvana              proc
                        di                              ; 1) Clear all pixels
                        ld (RestoreStack), sp           ; SMC> Save the stack
                        ld sp, AttributeAddress         ; Set stack to end of screen
                        ld de, $0000                    ; All pixels unset
                        ld b, 0                         ; Loop 256 times: 12 words * 256 = 6144 bytes
LoopPixels:
                        loop 12
                          push de
                        lend
                        djnz LoopPixels
RestoreStack equ $+1:   ld sp, SMC                      ; <SMC Restore the stack
 
                        ld hl, ClsNirvanaGame           ; 2) Clear 8x2 attributes
                        ld ix, race_raster
                        ld de, NAttrVOffset
                        xor a
                        ld (Col+1), a
LoopAttr:               ld b, (hl)
                        xor a
                        cp b
                        jp z, TopRow
                        inc hl
                        ld c, (hl)
                        inc hl
Times:                  ld a, (Col+1)
                        cp 32
                        jp nz, Skip
                        xor a
                        add ix, de
Skip:                   inc a
                        ld (Col+1), a
Col:                    ld a, (deltas-1)                ; NIRVANA_org+8957 (65280, $FF00)
                        ld (Delta+2), a
Delta:                  ld (ix+0), c
                        djnz Times
                        jp LoopAttr
TopRow:
                        ClsAttrLine(0, BrightRedBlackP) ; 3) Clear top row of 8x8 attributes
                        ret
pend

This is split into three sections[I believe Einar Saukas, the author of NIRVANA+, gave me the original version of this routine, although I’ve hacked it about several times since. Thanks, Einar![/note]:

  1. Pixel clear: This uses a faster, stack-based way of block copying bytes than we used in the ldir-based ClsAttr routine (part 3).
  2. Multicolour attribute clear: This is the fiddly one. there are 2944 ($B80) multicolour attributes, and they aren’t laid out contiguously in memory. In fact the attributes for column 7 aren’t even near the attributes for column 8, for example. Every line of attributes is laid out 82 bytes after the previous line, moving down the screen. To find anything else, we need to use the deltas lookup table that NIRVANA+ handily provides. The upshot is that we need a slightly more complicated routine to set these attributes. This section uses a colour table for more flexibility. I’ll talk about this in more detail in a minute.
  3. Top row attribute clear: In NIRVANA+, the top row of attributes are always rendered with standard 8×8 attributes. The third section does this. Here, I’m setting them to red, because the arcade Galaga displays red status text on the top row. You can configure NIRVANA+ to render some of the lower rows with 8×8 attributes too. The main advantage of doing this is to to reclaim some of the processor time used for drawing the attributes for your user code, if your program design can get away with not having multicolour over the full screen. For this reason, I’m being slightly flexible and writing this section with a macro, which allows me to specify the row I want to clear2:
21
22
23
24
25
26
27
28
29
30
31
32
33
34
; macros.asm
 
ClsAttrLine             macro(Line, Colour)
                        if Colour = DimBlackBlackP
                         xor a
                        else
                          ld a, Colour
                        endif
                        ld hl, AttributeAddress+(Line*32)
                        ld (hl), a
                        ld de, AttributeAddress+(Line*32)+1
                        ld bc, 32
                        ldir
mend

I usually set any pixels and 8×8 attributes at the end of setup routines, as they can sometimes display earlier than the multicolour parts, particularly if you have NIRVANA+ turned off while you do the setup. I don’t think it matters much here, but it’s helpful to keep this as the last section of ClsNirvana.

The colour table used by the second section looks like this. It consists of pairs of bytes, with a zero byte terminator. The first byte of each pair is a count, and the second byte is a colour. The counts should add up to 2944. By all means make them add up to less, but if they add up to more, code will get overwritten and weird things will happen. Be warned!

Here, I’m setting the second character row to white on black (32 columns x 4 attribute lines = 128), as we’ll display scores here. The rest of the screen will get cleared to yellow on black. Our bullets will probably be yellow, and this might make the drawing code slightly easier if the screen is already the right colour.

35
36
37
38
39
40
41
42
43
44
; database.asm
 
ClsNirvanaGame          proc
                        db 128, BrightWhiteBlackP
                        loop 11
                          db 255, BrightYellowBlackP
                        lend
                        db 11, BrightYellowBlackP
                        db 0
pend

The sprites (only the ship, so far) have their own background and foreground colours, and will always overlay attribute values written this way, so we don’t need to worry about them here.

I defined a whole bunch of colour constants uing equ. This lets us refer to named foreground and background colours when we hardcode them. Check them out on line 81 of constants.asm, if you’re interested.

If you assemble and run the part 8 code, you will see… exactly the same results as part 7! But we have set ourselves up nicely for the next part, so it’s all good 🙂

If you want to see at least some visible change, play around with the ClsNirvanaGame colour table. It can be as long or complicated as you like! See if you can make a blocky pattern out of individual attributes. I used this technique to animate clouds on the Jet Power Jack main menu—notice how both the blue and white clouds go behind most of the graphics, but go in front of a few parts – the letters p, e and k in the title, for example:

Yes, this really is the 406th version of Jet Power Jack, and counting3. One day it will get finished and released…

That’s all for today! Let’s get fancy with proportional fonts next time 🙂

ZalaXa 9 — Proportional Fonts with FZX

FZX is an excellent proportional font driver by Andrew Owen and Einar Saukas. The latest source code can be downloaded here, although the version I have added to the code for part 9 has been slightly modified.123

FZX is an indispensible font editor by Klaus Jahn. Download it from here by clicking on the image.

The SetupMenu routine has a new section, as I’ve rewritten it to use a proportional font instead of the standard Spectrum font.

7
8
9
10
11
12
;menu.asm
 
                        ld hl, Font.Namco               ; Set FZX font
                        ld (FZX_FONT), hl
                        ld hl, MenuText.FZX             ; Start of menu ASCII data
                        PrintTextHL()                   ; Macro to print FZX proportional text with FZX

We set the font file to use 4, load hl with the address of the new menu text, and invoke the PrintTextHL macro. The macro looks like this:

23
24
25
26
27
28
29
30
31
32
33
34
35
;macros.asm
 
PrintTextHL             macro()
PrintMenu:              ld a, (hl)                      ; for each character of this string...
                        cp 255
                        jp z, Next                      ; check string terminator
                        push hl                         ; preserve HL
                        call FZX_START                  ; print character
                        pop hl                          ; recover HL
                        inc hl
                        jp PrintMenu
Next:                                                   ; This will be whatever code follows the macro
mend

We don’t really need to know much about the internals of FZX_START, except that it prints a single ASCII character in the range 32 to 255. It uses a pixel coordinate system, so the numbers are eight times larger than the BASIC PRINT routine. It recognises the At constant, which is followed by a pair of coordinate bytes, Y first.

Here is the revised MenuText table. You will see I have cheated slightly—because FZX doesn’t set attribute colours, I have retained the previous ROM printing code, which sets the same attributes as before, but only prints spaces. I wouldn’t normally do it like this, but I intend to change the menu again later, so let’s keep it quick and dirty for now.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
;macros.asm
 
MenuText                proc                            ; Named procedure to keep our print data tidy
                        db At, 7, 13                    ; These codes are the same as you would use
                        db Paper, Black, PrBright, 1    ;   with Sinclair BASIC's PRINT command
                        db Ink, Red, " "                ; Set the attributes here, with spaces,
                        db Ink, Yellow, " "             ;   because FZX only prints pixels
                        db Ink, Cyan, " "
                        db Ink, Magenta, " "
                        db Ink, White, " "
                        db Ink, Green, " "
                        db At, 21, 6
                        db Ink, Yellow, "      "
                        db Ink, White, "      "
                        db Ink, Yellow, "        "
Length                  equ $-MenuText                  ; Let Zeus do the work of calculating the length
                                                        ; ($ means the current address Zeus is assembling to)
FZX:                    db At, 56, 104                  ; FXX coordinates are (Y, X) in pixels
                        db "ZA%AXA"
                        db At, 168, 55
                        db "PRESS SPACE TO START"
                        db 255                          ; Terminator byte
pend

You may also notice the % character in ZA%AXA. I did this because L in the Namco font was a bit narrower, and stopped the text aligning to character boundaries. I defined an unused character as a slightly wider L.

The Namco font is imported with a standard import_bin directive:

16
17
18
19
20
;database.asm
 
Font                    proc
  Namco:                import_bin "..\fonts\Namco.fzx"
pend

I’ve done something very similar in SetupGame:

25
26
27
28
; menu.asm
 
                        ld hl, GameText                 ; Start of menu ASCII data
                        PrintTextHL()                   ; Macro to print FZX proportional text with FZX
45
46
47
48
49
50
51
52
53
; database.asm
 
GameText                proc                            ; FXX coordinates are (Y, X) in pixels
                        db At, 0, 0, "&UP"
                        db At, 0, 94, "HIGH SCORE"
                        db At, 9, 0, "0"
                        db At, 9, 112, "20000"
                        db 255                          ; Terminator byte
pend

The &UP text is actually 1UP. Proportional fonts conventionally have all their digits to same width, so that columns of numbers always line up. Doing it this way makes the 1 of 1UP start on the second pixel of the line, so I made a shifted version of it in another space character.

If you recall, in part 8 we set the first character line of the game screen to bright red, and the second line to bright white, using the ClsNirvana routine. Well, if we got everything right, our new score text will appear in the right place, with these colours.

It does! I think this looks reasonably like the font Namco used in the original arcade version of Galaga.