- The Idea
- Setting Up the Dev Environment (code)
- A Simple Start Menu (code)
- First Glimpse of Multicolour (code)
- Wide Tiles (code)
- Preshifted Sprites (code)
- Moving the Ship (code)
- Clearing the NIRVANA+ Screen (code)
- Proportional Fonts With FZX (code)
- A Naïve Scoring Routine (code)
- Flashy 1UPmanship (code)
- Towards a Scrolling Starfield (code)
- You Gotta Roll With It (code)
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.
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!
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 🙂
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
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 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,
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
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
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!
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
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.
/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).
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
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!
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_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
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.
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...)
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
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…)
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!
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]:
- Pixel clear: This uses a faster, stack-based way of block copying bytes than we used in the
ldir-based ClsAttr routine (part 3).
- 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
deltaslookup 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.
- 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
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 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 🙂
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.
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
The Namco font is imported with a standard
16 17 18 19 20
;database.asm Font proc Namco: import_bin "..\fonts\Namco.fzx" pend
I’ve done something very similar in
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
&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
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.