Over the next few months I will be adding to this series of code snippets to help people make games on the spectrum next, so make sure you check back every now and then, or you could leave a comment and get notified when I post again!
Snippet 1
This little piece of code will let you check to see which scanline the beam is on as it draws the screen, so rather than using interrupts we can wait for any vertical position on the screen.
In your main loop, after you have done all your game code, you can wait for the beam to be offscreen and then go around the main loop again!
WasteLoop: call ReadRaster // check the current scan line ld a,192 // check for 192 cp l // are we at 192 yet jp nz,WasteLoop // nope so waste cycles jp MainLoop // do the whole thing black and blue again and again
So to the snippet
This code will read the scan line and return the value in HL, its all commented so there is not much more to say.
//-------------------------------------------------------------------------- // // Read the current Raster into HL // Out: hl = raster line on the screen // // Dirty a,bc // //-------------------------------------------------------------------------- ReadRaster: ld a,$1e // select the Active video line (MSB) ld bc,$243b // set bc to $243b select the NEXT register to read the video line out (c),a // tell the hardware we are going to read the video line ld bc,$253b // set bc to $253b so we can access the read value in a,(c) // so now read the port $253b NextREG data/value and 1 // mask off the Active line MSB ld h,a // store the high part in h for returning ld a,$1f // now read LSB of the Active raster line LSB (0-255) ld bc,$243b // as before set bc to $243b select the NEXT register out (c),a // tell the hardware we now want to read the register $1f ld bc,$253b // set bc to $253b so we can access the read value for the last time in a,(c) // read the register ld l,a // and store in the low part of hl ret
Snippet 2
The next snippet will generate pseudo random numbers, that can be used in many places in your game, such as determining baddie behaviour or just making things look more organic with a little random variance. In my shoot em’ up I am using a random number to rotate and flip my explosion sprites, so they don’t all look the same.
call getRandom // get the random number ld a,l // I only need the low part and %00001110 // mask off the rotations and mirrors ld (ix+object.mirrorRotateBits),a // bit 3 = X mirror, bit 2 = Y mirror, bit 1 = Rotate
So to the snippet
This code will generate a pseudo random number return the value in HL, its all commented so again there is not much more to say.
//-------------------------------------------------------------------------- // 16bit pseudo random number using the xor shift method // // out hl pseudorandom number // // dirty none //-------------------------------------------------------------------------- randomSeed: dw %0101101001100101 // seed (number) to start with getRandom: push af ld hl,(randomSeed) // get the last seed ld a,h // add high byte register rra // rotate right accumulator with carry ld a,r // adding in the Memory Refresh Register add a,l // now do the same with the low seed byte rra // rotate right accumulator with carry again xor h // exclusive or high seed byte with the accumulator ld h,a // put the accumulator into the high seed ld a,l // and put the low seed into the accumulator rra // rotate right accumulator with carry again ld a,h // do the same with the high using the carry from the low rra // rotate right accumulator with carry again xor l // exclusive or low seed byte with the accumulator ld l,a // yes store the accumulator in the low seed byte xor h // exclusive or high seed byte with the accumulator ld h,a // now store the accumulator in the high seed byte ld (randomSeed),hl // and store as the next seed pop af ret
In the past I have used a pseudo random number generator like this to make random levels that always come out exactly the same. Simply by setting the seed before I generate the level, I was able generate an identical sequence of random numbers which saved space as the whole level was just a 16 bit seed. Likewise on other games I have called the random number generator in the main loop which produced a more random set of random numbers.
Snippet 3
Now lets talk about the DMA, which is a fast processor to move large amounts of data pretty quickly, the data destination could be a ZX spectrum next system register, or another address in memory.
The DMA functions are quite self explanatory as I have used binary notation and I have added // 76543210 comments above so you can see which bits are being set, enabling you to learn more about the DMA in the docs.
The first function I use to upload sprites to the FPGA.
note on the DMA docs page
Since core 3.1.2 the zxnDMA is mapped to Datagear DMA Port ($xx6B / 107), and Zilog-DMA mode is mapped to MB02 DMA Port ($xx0B / 11).
//---------------------------------------------------------------------- // Function: Upload a set of sprites // In: a = dest sprite shape to start at // de = length of data // hl = shape data //---------------------------------------------------------------------- define PORT_DMA_DATA $6b // labled zxnDMA in the docs dmaSprites: ld bc, $303b // tell the hardware the next value is a sprite slot out (c),a // write the the slot ld (dmaSpriteSource),hl // set the source address ld (dmaSpriteLength),de // now set the length of the data ld hl,dmaSpriteCode // now start the transfer by copying the dma ld b,dmaSpriteCodeLen // code to the DMA processor which will ld c,PORT_DMA_DATA // start as soon as all the data has gone otir // through the port ret //---------------------------------------------------------------------- // actual sprite DMA code //---------------------------------------------------------------------- dmaSpriteCode: db $83 // DMA Disable // 76543210 db %01111101 // WR0-Transfer mode, A -> B, write adress + block length dmaSpriteSource: dw 0 // WR0-Port A, Start address (source address) dmaSpriteLength: dw 0 // WR0-Block length (length in bytes) // 76543210 db %01010100 // WR1-read A time byte, increment, to memory, bitmask db %00000010 // WR1-Cycle length port A // 76543210 db %01101000 // WR2-write v, Port B address is fixed, Port B is IO db %00000010 // WR2-Cycle length port B // 76543210 db %10101101 // WR4-Continuous mode (use this for block tansfer), write dest adress dw PORT_SPRITE // WR4-Dest address (destination address) // 76543210 db %10000010 // WR5-Restart on end of block, RDY active LOW (STOP) // 76543210 db %11001111 // WR6-Load db %10000111 // WR6-Enable DMA endSpriteDmaCode: dmaSpriteCodeLen equ endSpriteDmaCode-dmaSpriteCode // calculate the length of the dma code
The next one is a modification of the above function to transfer blocks of memory from one place to another.
//---------------------------------------------------------------------- // // Function: DMA block of memory from one location to another // In: hl = source address // de = address of destination // bc = length of data // //---------------------------------------------------------------------- dmaBlock: ld (dmaBlockSource),hl // same process as above setting the source ld (dmaBlockLength),bc // and length of the data, ld (dmaBlockDest),de // but this time its not a port so we have a dest address ld hl,dmaBlockCode // now set start the transfer by copying the dma ld b,dmaBlockCodeLen // code to the DMA processor which will ld c,PORT_DMA_DATA // start as soon as all the data has gone otir // through the port ret //---------------------------------------------------------------------- // actual block DMA code //---------------------------------------------------------------------- dmaBlockCode: db $83 // DMA Disable // 76543210 db %01111101 // WR0-Transfer mode, A -> B, write adress + block length dmaBlockSource: dw 0 // WR0-Port A, Start address (source address) dmaBlockLength: dw 0 // WR0-Block length (length in bytes) // 76543210 db %01010100 // WR1-read A time byte, increment, to memory, bitmask db %00000010 // WR1-Cycle length port A // 76543210 db %01010000 // WR2-write v, memory address is increments, Port B is IO db %00000010 // WR2-Cycle length port B // 76543210 db %10101101 // WR4-Continuous mode (use this for block tansfer), write dest adress dmaBlockDest: dw 0 // WR4-Dest address (destination address) // 76543210 db %10000010 // WR5-Restart on end of block, RDY active LOW (STOP) // 76543210 db %11001111 // WR6-Load db %10000111 // WR6-Enable DMA endBlockDmaCode: dmaBlockCodeLen equ endBlockDmaCode-dmaBlockCode // calculate the length of the dma code
It should be said that I got the original DMA code from a demo in cSpect, and used it to learn, then modified to suit my needs.
One interesting thing you can do with the DMA is clear a block of memory like this.
// use the DMA to clear a block of memory ld hl,characterScreen // destination address ld (hl),0 // write zero to the destination ld de,hl // copy the source address to the destination inc de // increment the destination ld bc,$a00-1 // how many bytes to clear call dmaBlock // now use the dma to clear
Snippet 4
Time for jump tables I think, in c++ or most other higher level languages we could use a switch case statement, were each case is handled based on the type of animation, like this.
// animation types #define ANIM_TYPE_NONE 0 #define ANIM_TYPE_LOOP 1 #define ANIM_TYPE_SINGLE 2 #define ANIM_TYPE_PING 3 #define ANIM_TYPE_PONG 4 #define ANIM_TYPE_STOP 5 void animate() { switch(object.animationType) { case ANIM_TYPE_LOOP: .... some code break; case ANIM_TYPE_SINGLE: .... some code break; case ANIM_TYPE_PING: .... some code break; ....... } }
In z80 we could do many compares and branches like this.
ld a,(ix+object.animationType) cp ANIM_TYPE_NONE ret z cp ANIM_TYPE_LOOP jp z,animateLoop cp ANIM_TYPE_SINGLE jp z,animateSingle cp ANIM_TYPE_PING jp z,animatePing .... more compares and jumps
but this works out at about 17 t-states per compare and jump, so we could not have many options before it eats most of the t-states.
So is there a faster way to do this in z80?
Yes we can use what we call a jump table which is a method of transferring program control (branching) to another part of a program based on an index.
// animation types ANIM_TYPE_NONE: equ 0 ANIM_TYPE_LOOP: equ 1 ANIM_TYPE_SINGLE: equ 2 ANIM_TYPE_PING: equ 3 ANIM_TYPE_PONG: equ 4 ANIM_TYPE_SHIP: equ 5 ANIM_TYPE_CASE: equ 6 ANIM_TYPE_DELETE: equ 7 ANIM_TYPE_STOP: equ 8 animJumpTable: dw noAnimate //ANIM_TYPE_NONE dw animateLoop //ANIM_TYPE_LOOP dw animateSingle //ANIM_TYPE_SINGLE dw animatePing //ANIM_TYPE_PING dw animatePong //ANIM_TYPE_PONG dw animateShip //ANIM_TYPE_SHIP dw animateCase //ANIM_TYPE_CASE dw animateDelete //ANIM_TYPE_DELETE dw notEnded Animate: ld a,(ix+object.animationType) // get the type of animation ld hl,animJumpTable // address of the jump table add hl,a // add the offset to the base of the table add hl,a // times 2 for words on the jump table ld de,(hl) // get the address ld hl,de // move to hl so we can jump jp (hl) // jump to the animation function //----------------------------------- // looping animation //----------------------------------- animateLoop: .... some animation code noAnimate: ret //----------------------------------- // ping of the ping pong animation //----------------------------------- animatePing: .... some animation code ret
So this works out at about 30 t-states no matter which type of animation function is needed, I am using this method for other things like baddie control (ix+object.BaddieType) and option selections.
Anyhow jump tables are your friend so enjoy!
Snippet 5
If branching on z80 appears a little confusing then how about some macros to make things a little clearer? Especially if you are coming from a 6502 background.
Obviously you can call these what you want but these names make sense to me.
// jump if less than macro jle address jp c,address endm // jump if greater than macro jgt address jp nc,address endm // jump if equal macro jeq address jp z,address endm // jump if not equal macro jne address jp nz,address endm // jump if signed macro js address jp m,address endm // jump if not signed macro jns address jp p,address endm
Usage
ld a,0 cp 0 jeq someBranchAddress ld a,100 cp 50 jle anotherBranchAddress
Snippet 6
Have you heard of self-modifying code? Self-modifying code is code which achieves its goal by rewriting itself as it goes. It’s a common trick in games programming, particularly in low level languages like assembler.
One use is modifying a call
In z80 we have some indirect jumps using jp (hl), jp (ix) and jp (iy) which are great if you don’t want to return to the next line, but what do we do if we want to just call a function from data and then return to the next line?
This is where self modifying code can help like this, the address from the object gets written into the address of the call, then the call calls the function and returns to the next line.
The hex in memory for a a call to address $FF00 would be CD 00 FF, so by modifying the call+1 writes the address into memory leaving the call (CD) opcode un touched.
initFunction: ... // more code ret callInit: ld hl, (ix + object.initFunction) // get the address of the init // function ld (.callLine + 1), hl // self modifying code .callLine: call $FF00 // the $FF00 gets modified with the // address of the init function ... // more code
Workaround using indirect jumps
When the processor does a call it first puts the return address (next line of code) onto the stack and then does the call, so we can use that to our advantage by putting our own return address on the stack then doing a jump, so its our return address that gets pulled on the ret from the jump, making it act like a call.
initFunction: ... // more code ret // this will return to nextLineAddress callInit: ld hl,nextLineAddress push hl ld ix,initFunction jp (ix) nextLineAddress: ... // more code
Self-modifying Add
Here, I can’t use (iy+brainS.xOffset) because IY will be the new ammo object and I would need create the ammo and then add the offset to the ammo, as you see IX and IY are in use, so using self-modifying code I can store the value I need in an immediate add.
ld hl,(iy+brainS.xOffset // get the sprite offset ld (.xStart+2),hl // modify the code below call baddieFireAmmo // this corrupts IY pop ix ld hl,(ix+object.xPos) // get the X position of the baddie //---------------------------------------------------- // the next line the zero becomes the // previous (iy+brainS.xOffset) //---------------------------------------------------- .xStart: add hl,0 // add the offset on the new ammo ld (iy+object.xPos),hl // store X in the new ammo
Self-modifying Compare
This one I use to load the data from one memory bank and place it in a compare for another memory bank (both banks mapped to $c000).
ld a, (ix+object.bezierIndex) // get the index of the object ld (.indexCmp+1),a // store for action compare MEM PATHS_BANK // get paths bank using macro ld b,4 // check 4 actions .checkAll: ld a,(hl) // get the count for actions .indexCmp: cp 0 // check against count jp z,.foundAction // they are the same inc hl // next action djn .checkAll // do them all
Obviously, these are just a cut down versions of my code for demonstration purposes.
More to come soon so please come back.
I want to be notified when you update this as I love your snippits 🙂
nice code 🙂
Great site.
I also want to be notified!
I’ve been enjoying reading through your code snippets. I’ve backed KS2 so can’t wait to get my Next. In the meantime trying to understand what I can in terms of coding for it. So looking forward to reading more on this great website.
Excellent explanation on how things work. Nice one.
In snippet 4, `ld hl,de` – is this your compiler expanding this out to `ld h, d : ld l, e` or should this just be `ex de, hl`?
Great stuff too ?
Thanks for that, Yes I have the compiler set to fake instructions so it takes care of most of that stuff for me, I am new to z80 so i don’t know all the tricks yet and I tend to spend time writing the game and getting speed increases through how I am doing it rather than optimizing every line.
I am impressed with your work too
I just couldn’t depart your website prior to suggesting that I extremely loved the standard info an individual provide for your visitors? Is gonna be back steadily in order to investigate cross-check new posts.
Hi, I just noticed this and had to say something.
In snippet 2, the lines
ld a, h
rra
do nothing more than create an 8 t-state delay as the following 2 lines
ld a, r
add a, l
destroy both the accumulator and carry flag rendering the previous 2 lines redundant. Maybe replace the add instructions with adc to make use of the carry generated by the rra instruction.
Yes this was not my original code, its a modified version of code from a cSpect demo that I used when I was just learning z80!