Update (november, 2015): I further developed Racer into a 'rebooted', improved version, and thus I re-made/re-wrote the graphics, sound, and code. Even though this tutorial covers only the 'classic' version of Racer, the source code for all versions (classic, easter and rebooted) is available at Racer's homebrew page.

This article is part development notes, part tutorial, related to the creation of Racer - a simple racing game inspired by Speedway! on the Magnavox Odyssey II console (released 1978). This console is the second iteration of the original Magnavox Odyssey created by the legendary Ralph H. Baer (1922-2014) - one of the founding fathers of the video game industry. Speedway! is part of a multigame cart, and before you scream "ugly graphics!" at Racer's simple sprites, you should watch how the original plays.

For comparison I present a screenshot of the original Speedway! on the Magnavox Odyssey II besides a screenshot of Racer.

Speedway!Racer - classicRacer - rebooted

Source code and assets for each of the tutorial's steps are accessible directly from the page you are reading now. Additionally, you can visit Racer's discussion thread to participate in the discussion about the game, ask questions, add comments, etc. After writing this tutorial, I made a few minor adjustments to the code. You can get the latest version of Racer, including a special Easter edition, from Racer's homebrew page.

Introduction

Reading another programmer's source code can be really helpful, but it can also be quite overwhelming. To help facilitate the reader's learning, I break the game development process into 5 separate steps. And as you can imagine, the source for the first step (a scrolling road) has relatively little code in it. Code is added as we progress through the steps. The only missing elements are scoring, death-handling (including super simple collision detection) and the title screen. Upon request I will happily make more detailed explanations if required. I can also add the scoring and/or collision detection as separate steps. I really did the five steps in order as I developed Racer. I added the title screen and music in the end, and also the way scoring and dying is handled, because these elements are different from the original game (it has no title screen, and it just crashes when your two minute timer is up).

This wiki page will not discuss everything that goes on in the code. I discuss some of the central stuff going on in each step, and I highlight some related routines. The source code is almost fully line-by-line commented, so just browsing it should give you a pretty good idea about what goes on.

My motivation for doing this? I wish I have had something like this to proceed to after completing Maxim's Hello World tutorial. Writing tutorials even though you are not an ancient SMS coder, in fact I'm getting started myself, seems to be in the spirit of content creation for SMSPower's Getting Started section[1]. This unfortunately also means that I don't have the skills and knowledge to provide you with super optimized code. As I review my code over and over, I think of new ways to improve it. You should too. For example, the way I update the sprite table buffer (see later) is not particularly great, but it gets the job done before vblank finishes.

Disclaimer: It is important that you attune your expectations to what this document really is (and thus also what it is not). It IS a rather detailed personal account of how I made my first complete SMS game. And as such, it is definitely NOT a template for building optimized, generalized, clean and easily extendable assembly programs. This is NOT a tutorial that will teach you general SMS-programming techniques and good practices. But hopefully you will find it helpful if you are getting started with SMS-programming - just like I was when I wrote Racer.

Ideally you should have completed at least Maxim's Hello World tutorial, and preferably have done a little general z80 assembler fiddling and SMS document reading in addition, before taking on this tutorial. I wont go into super much detail about all the stuff going on in the source code. What I will do is provide you with some fundamental building blocks to stack on top of each other to re-create Racer. These building blocks are five steps describing how 1) the assets were created, 2) the scrolling road was made 3) player control was added, 4) enemy Ash and 5) enemy Mae was added.

I start with some general technical considerations. Then I present the core of this tutorial: A five step approach to recreate Racer. At each step you can download the source code and relevant assets. I have line-by-line commented almost the entire source code. I highlight a few important points under each step in this tutorial. If you need more help, I'm happy to provide further explanations to the best of my knowledge. Put a question or comment in Racer's thread on the forums.

You can also check out GameGearGuy's Racer-based games, including Mr. Ultra, Bananas are good and Mr. Banana. Thanks to GameGearGuy for really taking the Racer codebase in new directions... from a vertically scrolling race game to a sidescrolling jumper, and a fly-swatting reflex game, etc...

This is an exiciting time in the community! It seems like we are on the verge of a breakthrough for C-based homebrewing. I look forward to these advancements, and feel certain that both assembler- and C-based coding will thrive alongside each other for many years to come. These notes describe assembler-based game development.

Description of the Game

I strongly suggest that you download and try out the game before you proceed!

Gameplay

You find yourself on the track - ready to go! Race full speed up the track, and avoid enemy cars as you overtake them. At first there is only one enemy, but that will change soon enough. Keep calm and stay alive for as long as you can. The game starts with a humble hiscore of 200. Break that hiscore. Die a couple of times. Feel the almost flappy bird-like gameplay frustration. Invite your friend/wife/husband/kids, and let them have a go at beating your hiscore. Put Racer on next time you throw a party. The unforgiving, fast and super simple gameplay will hopefully both motivate you... and piss you off!

Controls

Press button one (start) to progress from the title screen, and press again to start the engine of the car. Left and right on the d-pad are for horizontal control of your car. After you have crashed, you are back to the starting point, and a shining new car is waiting for you. Now you can press button 1 to fire up the engine and try again, or you can pass the controller on to your eager friend next to you. If you want to go back to the title screen, just wait a moment, and you are taken there automatically.

Development Goals

Development Principles and Constraints

For a larger game, I would probably have used wla-dx's .sections to organize the code. I have chosen to keep the raw hex numbers for i.e. the vdp status port ($bf) and data port ($be). These values cannot be changed by the programmer, so you might as well learn them. If you prefer, you can of course create a constant definition, i.e. .equ VDP_Control_Port $bf, and use this instead. To me, long and fancy definitions are sometimes harder to remember than the handful of raw hex numbers referring to hardware ports, etc.

I have created constants for some of my values, i.e. horizontal speed (hspeed), vertical speed (vspeed), etc. Creating a constant will allow me to change all references to this value by changing just the value in the definition. As I developed the game, I tweaked these constants in order to create the best flow of the cars' movement.

General Technical Notes

This section describes the software I have used, as well as the various technical documents I have read and used for reference. I also include some tables and considerations supporting my work with the vertical blanking and vram updating, and general principles for organizing a generic main game loop.

Tools for Writing and Debugging Code

My development toolchain is mainly based on the setup described in Maxim's tutorial:

Tools for Creating Graphics and Sound

Documents

The Display's 6 Phases

This is explained in Charles' vdp documentation, and also discussed on the forum, but I have included it here for reference. I use this table together with the line counting features of Meka and Emulicious to make sure that I never update the vram in the active display phase. While writing Racer I had some colorful, but magically appearing pixels on the title screen near the bottom. Later I found out that these pixels were caused by me writing to the color ram during the bottom border phase. As you can see from the table, this is not good. I added a delay that caused my color ram updating to happen in the bottom blanking phase instead, and then the mystic pixels disappeared as expected.

The following table does only apply to NTSC.


 Lines       Description 
---------------------------------------------------------------------------
 000-191     Active display (192 lines) 
             Update VRAM very carefully! Don't touch graphics currently 
             being drawn by/on the raster line. Work with line interrupts 
             and/or the counter, i.e. in A,($7E). 

 192-215     Bottom border (24 lines) 
             Start of VBlank interrupt (also called frame interrupt). 
             Write sprites, tiles and tilemaps. 

 216-218     Bottom blanking (3 lines) 
             Write everything, including colors (CRAM). 

 219-221     Vertical sync (3 lines) 
             Write everything. Beam moves back. 

 222-234     Top blanking (13 lines) 
             Write everything. 

 235-261     Top border (27 lines) 
             Write sprites, tiles and tilemaps. 
---------------------------------------------------------------------------

A Generic Game Loop

I don't think such a thing as a generic game loop exists. Anyway, I defined the following overall code-structuring principles in the beginning of the project. These principles are primarily used on the main game loop.

1. Wait for vblank (frame interrupt). The first thing in the loop is a wait for the screen to blank.

2. Write to vram from buffers. In order to update vram while the screen is blanked, next thing I do is to load the sprite attribute table buffer and the vertical scroll register variable into vram. Thise step finishes around line 218, som I'm well within the time frame for vram updating (please refer to the table above).

3. Resolve issues rising from current state of game objects. This means collision detection and test/responding to counters.

4. Update game objects. Set new states/positions (x,y), etc. Get, and respond to, user input. Calculate new positions for enemies. Note: Nothing happens with regards to sprites in this step.

5. Update vram buffers. Change the sprites' vertical and horizontal positions. Below is a map of the sprite attribute table buffer.

When all buffers are updated, we sit back and wait for the next vblank...

Sprite Attribute Table Buffer

This is the layout of the sprite attribute table (sat) buffer I use in Racer. The player is introduced in step 2, and the enemies (Ash and Mae) are introduced in steps 3 and 4 (see below).

This layout corresponds to the sprite attribute table mapping I do in the beginning of the source (note the memory locations):

; Map of the sprite attribute table (sat) buffer.
; Contains sprites' vertical position (vpos), horizontal posi-
; tion (hpos) and character codes (cc).

.equ highvp $c000          ; first hiscore vpos.
.equ highhp $c080          ; first highscore hpos.
.equ highcc $c081          ; first hiscore cc.

.equ scorvp $c004          ; first score vpos.
.equ scorhp $c088          ; first score hpos.
.equ scorcc $c089          ; first score cc.

.equ plrvp $c008           ; first player vpos.
.equ plrhp $c090           ; first player hpos.
.equ plrcc $c091           ; first player cc.

.equ maevp $c018           ; first Mae vpos.
.equ maehp $c0b0           ; first Mae hpos.
.equ maecc $c0b1           ; first Mae cc.

.equ ashvp $c028           ; first Ash vpos.
.equ ashhp $c0d0           ; first Ash hpos.
.equ ashcc $c0d1           ; first Ash cc.

Overview of Racer's Program Flow

1. Game initialization

2. Prepare title screen.

3. Title screen loop.

4. Prepare main loop.

5. 'Ready loop'.

6. Main loop.

7. Player dies (prepare death loop).

8. Death loop

Step 0 - The Assets


4 x 4 tiles

You can download the original .bmp files, the processed .inc files and also the title screen smash hit in Deflemask module format, so you can tweak and tinker as you see fit. I used Paint Shop Pro 7 (PSP7) for tile editing, because it has super tight palette control. When I work in PSP7, I usually apply a 8x8 grid, so I can always see how my drawings will be separated into tiles. You can see how it looks on the picture to the right. When I define colors in the palette, I refer to Maxim's color table from the Hello World tutorial. This way I can make sure I get some SMS-compatible colors into the palette.

As you can see, the blocky nature of the sprites are not due to them being zoomed (like in my other game Block Quest aka. Knights of the Square). So if you are fuelled by disappointment/rage regarding the visual output from Racer, I encourage you to do something about it! You can make almost Genesis-quality cars for the player and the enemies. 4 x 4 tiles per car means 32 x 32 pixels at your disposal for the player and Mae+Ash sprites respectively. Flicker-free sprites, that is! Kapow!! You can easily load some extra colors also. So knock yourself out. Just like when you start to improve on an old house (believe me, I live in one), you often realize that when you update one wall, you have to update the others. And then the floor. And the windows, and... phew! You cannot have a 7-color super detailed player car racing down a dull purple road, crashing into blocky white enemies, can you...? :) Soon you will spend a lot of time really remaking the game graphically - and if you do, I'll love to see the result. Thanks to Orlan Rod for hacking around with the graphical output from Racer.

I'm not perfectly happy with the digits for scrore and highscore. I took the easy way out, though. I re-used a set I had already grabbed from Shinobi. This means that the digit tiles are just 8x8 pixels (one tile per digit). In the original Speedway!, the digits are about twice this size. Although the larger digits sure look good, I choose to stick to my standard digit tiles for two reasons: 1) The score keeping and updating routine (pasted in from another game, and originating from Jonathan Cauldwell's tutorial) assumes 8x8 pixel digits. 2) I work exclusively with the hardware sprites in the sprite attribute table, and bigger score digits would be causing sprite overdraw as the enemy cars race by (+8 sprites on a scanline). Beacuse the enemy cars are 4 tiles wide, I can have 4 digits of scoring. I actually wanted five digits in the beginning, but chosed to live with 4 digits to obey the sprite limitations (and I have never even been close to getting 9999 points yet).

Having bigger digits, without the graphics suffering from sprite overdraw, would require me to treat the digits as part of the background (and thus update them through the name table, and not the sprite attribute table). This is not particularly hard, but I think it is nice, also from a tutorial keep-it-simple perspective, that I actually manage to make this game run solely on hardware sprites. OK, except from the scrolling background, but you know what I mean. There is no name table updating in the loops. Clean. Simple.

On the title screen the hiscore is made of what seems like 16x16 pixel tiles. Okay, I cheated a little and used zoomed sprites (setting the vdp register). So it is just the same tiles as in-game, but now they are zoomed. Because I have only four digits for the hiscore, the zoomed sprites should look alright on both SMS I and SMS II. The SMS I suffers from a hardware bug that makes it zoom only the first four sprites.

Anyway, when I'm done making graphics, I use BMP2Tile for making include files out of the bmp files. This is a super nice and easy workflow.

I used Deflemask to compose the title music, and I used the vgm2psg tool included with PSGLib to create psg out of vgm. As mentioned above, I have included the title screen tune in native Deflemask format. Refer to the "Sources" folder in the zip file. Please note: Because I have mapped ram to slot 2 (and not slot 3 where it usually goes), I needed to adapt the psglib a little. Refer to GameGearGuy's post for more details. Greetings goes out to GameGearGuy - thanks for your interest in Racer (look for the Racer-based games Yum! and Mr. Ultra on the forums)!

For the constant sound of engines roaring (??), and for the crash sound, I write directly to the noise channel on the programmable sound generator (psg). The following code excerpt shows how the engine sound is turned on when the race begins:

       ld a,%11110110      ; turn noise volume up to 12 db.
       out ($7f),a         ; write to psg.
       ld a,%11100110      ; make coarse noise - go car engine!
       out ($7f),a         ; write to psg.

This could have been done using the PSGLib and Deflemask. The reason why I write directly to the psg (not using the routines in PSGLib) is twofold: 1) I simply wanted to try and do it! It was my first time dealing directly with the psg. 2) I thought that I could do without title screen and music far into the development proces, because none of these things are in the original. Towards the end, when I had already written the motor and crash sound code, I decided to include PSGLib to spice up my title screen.

I used Photoshop to make the cover. It was a messy trial and error process, where I would arrange and resize stuff, print it, cut it out and try to cram it into an old World Soccer box. I would hereby like to extend my gratitude to the printer at my workplace, which made this possible. Even during regular work hours! :)

Aside from the technical considerations, I have pondered a little over ways to actually design cover art for homebrew games. You could possibly set it up to look like an original game, complete with the Sega logo, barcodes, and all that stuff. When I designed the cover art for Racer, I went more in the direction of the unlicensed games (i.e. games from the Gam* Boy, etc.). There is no need to pretend that Sega oficially endorses your game. Be proud of what it is: Unlicensed homebrew, and design your box art to reflect this fact.

I went to Deviantart to look for inspiration, and found morgenty's super nice drawing, which even matches the purple color theme of racer! I got her permission to use it. Thanks morgenty!

Step 1 - A Vertical Scrolling Road

In this first real coding step, we utilize the built in 'wrap-around' feature of the hardware vertical scroll register. If we just decrement the scroll register's value, and use a properly formatted background image, the road will scroll on and on for eternity.

Get the fully commented source code for this step, including relevant assets, in a handy zip file: Step1.zip. Everything is ready to be assembled, so you can have your own little scrolling road to tweak and cherish.

Apart from initialization and memory-mapping, this is really what goes on in order to scroll the road:

; This is the main loop.
; Of course it could be done without a buffer, but now we
; are building the scroll element the way it will look in the
; finished game....

mloop  halt                ; start main loop with vblank.

; Update vdp right when vblank begins!

       ld a,(scroll)       ; 1-byte scroll reg. buffer in ram.
       ld b,9              ; target VDP register 9 (v-scroll).
       call setreg         ; now vdp register = buffer, and the
                           ; screen scrolls accordingly.

; Blah blah ... in the game, lots of stuff goes on here....
; and then, towards the end...

; Scroll background - update the vertical scroll buffer.

       ld a,(scroll)       ; get scroll buffer value.
       sub vspeed          ; subtract vertical speed.
       ld (scroll),a       ; update scroll buffer.

       jp mloop            ; jump back for another round.

; --------------------------------------------------------------
; SET VDP REGISTER.
; Write to target register.
; A = byte to be loaded into vdp register.
; B = target register 0-10.

setreg out ($bf),a         ; output command word 1/2.
       ld a,$80
       or b
       out ($bf),a         ; output command word 2/2.
       ret
 

Even though it is totally overkill to use a buffer (just a one-byte variable in this case) to perform the scrolling at this point, it is firmly in line with the general principle to update vram via buffers, in order to make sure every vram update happens within the vertical blanking period. As the program grows bigger, I write sprite and scroll info to buffers as needed during the flow of the program. Every loop then starts with a "halt" opcode (meaning wait for interrupt - in this case the frame interrupt = the beginning of the vertical blanking phase), and right after this wait, I load stuff from my various buffers into vram. Edit: Note that I made a mistake here! The halt opcode will also resume program flow when returning from the pause button interrupt handler, not only from the frame interrupt. For a long time, this resulted in a bug, where pressing the pause button instantly killed the player. See the discussion i Racers thread, and read more at the end of this document.

For more info on the different phases of the display (including what you can/should write to vram in each phase), refer to the table presented earlier.

Step 2 - A Player Controlled Car

Let us get some sprites onto the stage! Download Step2.zip to get the fully commented source code for this step. In step 1 we made a scrolling road. Here in step 2, we will add a player controlled car to this road. The major point to note is that from this step and onwards, we will work with a sprite attribute table (sat) buffer in ram, and blast the buffer contents to the real sat in vram, during the vertical blanking phase in each loop.

The player has x and y coordinates, and can move his car horizontally between the left and the right border of the road. The player car is made up of 16 hardware sprites (4x4), and these sprites' vertical and horizontal positions must be adjusted with regards to the player's coordinates at every loop.

This is how we get input from the controller, and update the player's coordinates accordingly:

; Test if player wants to move right.

       call getkey         ; read controller port.
       ld a,(input)        ; read input from ram mirror.
       bit 3,a             ; is right key pressed?
       jp nz,mpl           ; no - test for left key.

       ld a,(plx)          ; get player's hpos (x-coordinate).
       cp rightb           ; is player over thr right border?
       jp nc,mpl           ; yes - skip to left test.

; Move player right.

       ld a,(plx)          ; get player x-coordinate.
       add a,hspeed        ; add constant hspeed
       ld (plx),a          ; update player x-coordinate.
       jp endchk           ; exit key check part.

; Test if player wants to move left.

mpl    ld a,(input)        ; read input from ram mirror.
       bit 2,a             ; is left key pressed?
       jp nz,endchk        ; no - end key check.

       ld a,(plx)          ; get player's hpos (x-coordinate).
       cp leftb            ; is player over the left border?
       jp c,endchk         ; yes - then don't move left.

; Move player left.

       ld a,(plx)          ; get player x-coordinate.
       ld b, hspeed        ; load horizontal speed (constant).
       sub b               ; subtract hspeed from player x.
       ld (plx),a          ; update player x-coordinate.
endchk                     ; end key check

; Update player sprites in the buffer.

       call upbuf          ; update sat buffer.

And this is the subroutine for reading the controller port into a variable. This sub is called once every frame (once every pass in the main loop). The variable "input" is defined in the beginning of the code, along with the other variables.

; --------------------------------------------------------------
; GET KEYS.
; Read player 1 keys (port $dc) into ram mirror (input).

getkey in a,$dc            ; read player 1 input port $dc.
       ld (input),a        ; let variable mirror port status.
       ret

Below is an excerpt of the code that translates the players' coordinates to new sprite positions in the buffer. The Update Buffer (upbuf) subroutine is called near the end of the main loop. At first (in this step), it does only handle the player's car, but later it will also handle the two enemies. This is partly reflected in the comments, i.e. "each of the cars". Sorry.

; --------------------------------------------------------------
; UPDATE SAT BUFFER
; Generate vpos, hpos and cc data for the sprites that make up
; each of the cars.

; Generate sat buffer data from player's x,y coordinates.

upbuf  ld a,(ply)          ; load player's current y-coordinate.
       ld hl,plrvp         ; point to sat buffer.
       call cary           ; refresh buffer according to y.

       ld a,(plx)          ; load player's current x-coordinate.
       ld hl,plrhp         ; point to sat buffer.
       call carx           ; refresh buffer according to x.


       ret

; --------------------------------------------------------------
; CAR Y TO SPRITES' VERTICAL POSITIONS (VPOS) IN BUFFER.
; Generate vpos sat buffer data from a car's y position.
; A = car's y (i.e. ply), HL = buffer address of car vpos.

cary   ld b,4              ; a car is 4 tiles wide.
-      push af             ; a row of 4 tiles share the same y,
       push af             ; so here the y's are saved on stack.
       push af
       push af
       add a,8             ; next row is 8 pixels below.
       djnz -              ; make 4 consecutive rows.

       ld de,15            ; load buffer offset into DE.
       add hl,de           ; add buffer offset to HL.
       ld b,16             ; we need to update 16 bytes.
-      pop af              ; get saved y from stack.
       ld (hl),a           ; write it to the buffer.
       dec hl              ; point to previous byte.
       djnz -              ; backwards from vpos+15 to vpos+0.
       ret

; --------------------------------------------------------------
; CAR X TO SPRITES' HORIZONTAL POSITIONS (HPOS) IN BUFFER.
; Generates hpos sat buffer data from a car's x position.
; A = car's x (i.e. plx), HL = buffer address of car hpos.

carx   ld c,a              ; save hpos in C
       .rept 4             ; wladx: Repeat code four times.
       ld a,c              ; load hpos into A
       ld b,4              ; loop: Repeat four times.
-      ld (hl),a           ; write value to buffer at address.
       inc hl              ; skip over the char code byte.
       inc hl              ; point to next hpos byte in buffer.
       add a,8             ; add 8 (a tile's width in pixels).
       djnz -              ; jump back
       .endr               ; end of wladx repeat directive.
       ret
 

Please refer to the SAT buffer map discussed earlier. After the upbuf routine has done its thing, the buffer is fully updated to reflect new positions for the player (and later enemies). Next time we have a vertical blanking phase, this buffer is loaded into vram (content is copied into the real sprite attribute table). This is the code responsible for the updating. It is not the fastest, most optimized stuff, but it gets the job done.

; --------------------------------------------------------------
; LOAD SPRITE ATTRIBUTE TABLE
; Load data into sprite attribute table (SAT) from the buffer.

ldsat  ld hl,$3f00         ; point to start of SAT in vram.
       call vrampr         ; prepare vram to recieve data.
       ld b,255            ; amount of bytes to output.
       ld c,$be            ; destination is vdp data port.
       ld hl,satbuf        ; source is start of sat buffer.
       otir                ; output buffer to vdp.
       ret

Step 3 - The First Enemy Car

In this step we add one enemy car. For convenience I name the enemies Ash and Mae, with Ash as the first enemy. Even though the gameplay is made up of what seems to be multiple racing cars that the player takes over as he/she races forward, it is really just the same two cars wrapping around forever.

Download the fully commented source code and assets that make up step 3.

Enemy Ash uses the same functions as the player to make sprites in the buffer from y,x positions. The update buffer subroutine is expanded to include updates to Ash's sprites:

; --------------------------------------------------------------
; UPDATE SAT BUFFER
; Generate vpos, hpos and cc data for the sprites that make up
; each of the cars (player and Ash).

; Generate sat buffer data from player's x,y coordinates.

upbuf  ld a,(ply)          ; load player's current y-coordinate.
       ld hl,plrvp         ; point to sat buffer.
       call cary           ; refresh buffer according to y.

       ld a,(plx)          ; load player's current x-coordinate.
       ld hl,plrhp         ; point to sat buffer.
       call carx           ; refresh buffer according to x.

; Generate sat buffer data from Ash's x,y coordinates.

       ld a,(ashy)         ; load Ash's current y-coordinate.
       ld hl,ashvp         ; point to sat buffer.
       call cary           ; refresh buffer according to y.

       ld a,(ashx)         ; load Ash's current x-coordinate.
       ld hl,ashhp         ; point to sat buffer.
       call carx           ; refresh buffer according to x.

       ret

Now it is time to introduce a kind of enemy artificial intelligence (AI). Maybe that is too strong a concept to cover what is going on here. Anyway, I have a subroutine named "enemy" that takes care of updating the three variables of an enemy car: 1) direction (left or right), 2) y-coordinate and 3) x-coordinate.

I use this to update Ash - and in a moment also Mae. When register IX is pointing to ashdir (Ash's direction), we know that ix+1 is Ash's vertical position and ix+2 is Ash's horizontal position, as this is the order in which the variables are declared in the enumeration section in the beginning of the code.

; Update enemy x,y positions.

       ld ix,ashdir        ; point to enemy Ash data.
       call enemy          ; move Ash down and left/right.

; ..... code continues...

; --------------------------------------------------------------
; UPDATE ENEMY.
; Calculate new x,y positions for an enemy car.
; IX = start of enemy data block.

enemy  ld a,(ix+0)         ; test direction.
       cp 0                ; moving left (0=left, 1=right)?
       jp nz,enem0         ; no - then enemy is moving right.

; Direction: Left - test left border.

       ld a,(ix+2)         ; load enemy's x-coordinate.
       cp leftb            ; compare it to left border constant.
       jp nc,+             ; branch if accumulator (x) > leftb.
                           ; else - enemy is on the left border
       ld a,1              ; shift direction to 'right'.
       ld (ix+0),a         ; load it into direction byte.
       jp enem1            ; skip forward to vertical movement.

; Direction: Left - subtract from enemy x coordinate.

+      ld b,hspeed         ; load horizontal speed into B.
       ld a,(ix+2)         ; load enemy x into A.
       sub b               ; subtract hspeed from x (move left).
       ld (ix+2),a         ; update enemy x coordinate.
       jp enem1            ; skip forward to vertical movement.

; Direction: Right - test right border.

enem0  ld a,(ix+2)         ; load enemy x.
       cp rightb           ; compare it to right border.
       jp c,+              ; skip if rightb > accumulator (x).

       xor a               ; else - shift direction to 0 = left.
       ld (ix+0),a         ; load new value into direction var.
       jp enem1            ; forward to vertical movement.

; Direction: Right - add to enemy x coordinate.

+      ld b,hspeed         ; load hspeed constant into B.
       ld a,(ix+2)         ; load enemy x into A.
       add a,b             ; add hspeed to enemy x.
       ld (ix+2),a         ; update enemy x coordinate.

; Vertical movement for enemy (move enemy car down).

enem1  ld a,(ix+1)         ; load enemy y into A.
       add a,espeed        ; add constant enemy vertical speed.
       ld (ix+1),a         ; update enemy y.
       ret
 

This is super simple, to the point that it is actually almost bad practice on a more general level. Ash races on and on, and wraps around the screen. As he leaves at the bottom on the screen, he reappears at the top. What is wrong here, is that some of Ash's sprites' vertical positions will, from time to time, be 208, which is $d0 in hex. A vertical position of $d0 is the sprite terminator character, and when the vdp sees this character as it renders sprites, it simple stops the sprite rendering. So the remaining sprites are not rendered. This is not a problem when we have got only Ash, but when I added Mae and the score and hiscore digits, I experienced issues with the graphics, stemming from my unawareness of this problem [3].

Step 4 - The Second Enemy Car

May I present Mae. She is the second enemy, and she appears after a little while. As always I recommend that you download and explore the source code for this step.

You will see that Mae and Ash share a lot of the routines. So adding Mae, when you already have Ash, is pretty straight forward. The main point to note in this step is regarding Mae's delayed entrance on the playfield. Actually Mae is always on the road, but she starts out invisible, and made up by transparent tiles. A counter for Mae's delay (maedel) is decremented at every frame until it reaches zero. Then Mae's transparent tiles are swapped for the same kind of tiles that make up Ash's car. This is highlights from the related code:

; Handle Mae visibility.

       ld a,(maedel)       ; check Mae delay.
       cp $ff              ; is Mae already visible ($ff)?
       jp z,endmae         ; yes - then skip rest of Mae routine.
       cp 0                ; else - is delay counter depleted?
       jp nz,+             ; no - then skip forward, else...

; Make Mae visible by loading opaque tiles into buffer.

       ld hl,encar         ; point to enemy car char codes.
       ld de,maecc         ; point to buffer.
       call carcc          ; load new char codes to buffer.

+      ld hl,maedel        ; point to Mae delay counter.
       dec (hl)            ; decrement it.
endmae

; blah blah blah ... more code...

; Charcodes for player, enemy and invisible car.

plrcar .db 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
encar .db 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
invcar .db 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

You can see how the character codes for the player and enemy cars are strings of bytes that correspond to tile indexes in the tile bank. The invisible car is made up of 16 x tile 0, which is a totally empty (transparent) tile. When the game (re-)starts, Mae's character codes are defined from the invcar (invisible car) string. When it is time for her to appear, the character codes are redfined from the encar (enemy car) string.

For these steps I have not included the collision detection. It is super simple, and you can easily see how it works in the full game's source code. Anyway, I'll just mention it here.

I read the vdp status byte into a variable at every frame interrupt.

; Read the vdp status flag at every frame interrupt.
; Sprite collision is read from bit 5 of the status flag.

.orga $0038                ; frame interrupt address.
       ex af,af'           ; save accumulator in its shadow reg.
       in a,$bf            ; get vdp status / satisfy interrupt.
       ld (status),a       ; save vdp status in ram.
       ex af,af'
          ; restore accumulator.
       ei                  ; enable interrupts.
       ret                 ; return from interrupt.

In my main loop, I test bit 5 of the status byte. This bit is set if two or more sprites overlap (that is, opaque parts of two or more sprites). When Mae is made of transparent tiles, she does not trigger this bit. Mae and Ash do never collide with each other. So if two sprites overlap, it must be the player's car colliding with either Ash or (visible) Mae.

; Test for collision between sprites.

       ld a,(status)       ; load vdp status (set by interrupt).
       bit 5,a             ; is the sprite collision bit set?
       jp nz,plrdie        ; yes - jump out of main loop.

If sprites collide, then jump to the place that handles stuff when the player dies. Super simple, and I can have a very accurate collision detection scheme without doing a lot of heavy work with hitboxes etc.

Conclusion

I have learned a lot from undertaking a complete project from first mockup to these final lines of documentation. It is a very simple and modest game, and that was exactly what I needed at this point in my SMS education. It was nice to work on a game that for sure would be possible to create on the SMS, without too many workarounds and tricks.

Having the original Speedway! to aim for, I could concentrate on writing code and making assets, and not worrying too much about game play mechanics and overall design. These choices were already made for me in the original game. For example: If I have not had Speedway! as a guide, I would surely have been trapped for weeks thinking about an overly complicated, pseudo random, and surely worse, way to make the enemies appear and move on the road. Speedway! shows how it is done, with just two cars bouncing from side to side. Almost too simple, but it works in practice.

At this point I'm puzzled by an unresolved bug: On real hardware, pressing pause instantly kills the player. I can't explain why...? It might have something to do with memory handling - it often has when emulators and real hardware works differently.

Speaking of memory handling, I had a ghost in the shell moment one evening, when I tried enemy Mae on real hardware. You might remember how she is initially made up of transparent tiles. In an earlier version of the game, I added a fixed offset to all Mae's tiles initially, so her character codes would all be pointing to empty, unused tiles in the sprite bank. This worked great on emulators, but on real hardware I saw this unsettling stuff where Mae was:

It seems like the vram was remembering some kind of charset from an earlier gaming session. I don't readily recognize the characters, but for sure I did not load them as part of Racer. I even thought about going straight to the forum to ask if we are 100% certain that the vram is not preloaded with some characters. We are sure, aren't we...? Anyway, once I got my vram initialization routine going (mfill), those spooky characters disappeared. This is just to say: Remember to initialize ram and vram! Crazy warped stuff lurk in those innocent looking chips.

Update 1: With the help of Calindro, I finally managed to eliminate the lethal pause button bug. I was not aware that pressing pause also counts as an interrupt, as far as the halt opcode is concerned. As I think about it now, it seems pretty obvious though. Thanks Calindro! You can get some details on Racer's thread.

Update 2: Calindro also helped me realize that the mysterious charset mentioned above is not so ghostly at all. It is the remains of the Everdrive's menu assets. I tend to forget that I'm running stuff through an Everdrive.

If you have any comments, questions, etc., then I encourage you to post them in Racer's forum thread.

If you are still with me, I want to thank you for taking the time to read through my humble SMS development musings. Making games for the SMS was something I always dreamt of as a child. As an adult, it feels massively satisfying to be taking the first steps down the developer's path...

THE CHALLENGE WILL ALWAYS BE THERE!

/Anders. March 2015.

Footnotes


  1. ^ As described by Maxim in this thread
  2. ^ According to TmEE Z80 is beautiful, and Maxim states that Z80 code is very readable and almost poetic. Click here to go to the thread
  3. ^ Sprite clipping and vertically oriented games are discussed in this thread


Return to top
0.254s