Jump to content
  • Advertisement
  • entries
    104
  • comments
    103
  • views
    262289

About this blog

Musings of a hobbyist

Entries in this blog

 

A c64 game in several steps (lots of 'em)

Welcome!
Today's development is heaps and bounds beyond imagination from 20 years ago. I've always had a soft spot for the C64 after all this years. So I sat down and tried to start assembly programming on a C64.

Today I'll start with a sort of tutorial on how to write a C64 game. I have prepared 36 steps for now, planned are probably a few more.

I'll start out very small but there will be bigger steps later on. The code is supposed to be heavily commented but is probably not clear for everyone. I'll be happy to answer questions regarding the code. The code is written for the ACME cross compiler, which allows to compile the code on any bigger OS.


Step #1 is a simple base for a game. It provides a Basic start (10 SYS 2064), sets up the VIC relocation and shows a simple synchronized game loop.

To show the loop running the border color is flashed and the top left char is rotating throughout all characters.

The not too eye-popping result looks like this:


Find here the source code and binary for use in an emulator of your choice (I recommend WinVICE):
step1.zip

Next Step 1b

Endurion

Endurion

 

First step explained in detail

As threatened the first step detailed. I reckon that the first step is overwhelming if it's your first voyage into C64 game programming. There's quite a few assumptions made about the viewer's knowledge and the later steps won't get exactly easier.

A note about the !zone macro. ACME allows for global and local labels. A local label is starting with a . and is only visible inside a zone. This allows for easier reuse of common names like loop or retry.

This snippet tells the ACME cross compiler to assemble the result into the file "jmain.prg" with the cbm (Commodore Business Machines) type. This basically boils down to the well known .prg format which contains a word with the loading address at the start followed by the assembly. ;compile to this filename !to "jmain.prg",cbm   The next snipped just defines a constant. I try to use them throughout so you can understand where I'm putting bytes at. The value 52224 is the address of the screen buffer, where 25 lines a 40 characters are stored continously. This is not the default memory location for the screen, a part of this base code relocates the screen. ;define constants here ;address of the screen buffer SCREEN_CHAR = 52224   Now a very interesting piece which took me longer to work out than it should have. A C64 has two types of files, Basic files and machine code files. A Basic file can be started by RUN, a machine code file just contains the code and usually must be jumped at with the SYS command. Any half decent game will provide a proper Basic kick start that jumps directly at the machine code.

To allow for this we set the file start address to $801 (2049), the default Basic start. The file content starts out with the tokenized bytes of a simple Basic line calling SYS for us. The line is built by a word containing the address of the next Basic line. Following is a word with the line number (10 in our sample). After that the token for the SYS command ($9e) followed by a space ($20) and the ASCII representation of the target address (2064 in our sample). After that there is one zero byte marking the end of the line. The next zero word represents the end of the Basic file. I've got some extra zero bytes which are actually wrong but also don't really hurt. ;this creates a basic start *=$801 ;SYS 2064 !byte $0C,$8,$0A,$00,$9E,$20,$32,$30,$36,$34,$00,$00,$00,$00,$00   The next snippet disables any visible sprites, relocates the VICs memory bank (resulting in a relocated screen buffer and charset address). ;init sprite registers ;no visible sprites lda #0 sta VIC_SPRITE_ENABLE ;set charset lda #$3c sta VIC_MEMORY_CONTROL ;VIC bank lda CIA_PRA and #$fc sta CIA_PRA   This piece is the main game loop. It's rather easy, we increase the border color (resulting in flashing), increase the top left character on the screen, wait for the vertical blank (not exactly but to the effect) and rerun the loop. ;the main game loop GameLoop ;border flashing inc VIC_BORDER_COLOR ;top left char inc SCREEN_CHAR jsr WaitFrame jmp GameLoop   This snippet is quite interesting. The C64 allows you to read the current raster line on the screen that is currently being redrawn. The code checks for a certain raster position at the bottom of the screen to sync the game to the computer's display speed.

In detail we're waiting for the raster line to NOT be the position we want to wait for. Once we are on any line but the wanted we now really wait for our raster line to appear. This avoids the problem when the routine is called too fast in succession and we end up on the same raster line. !zone WaitFrame ;wait for the raster to reach line $f8 ;this is keeping our timing stable ;are we on line $F8 already? if so, wait for the next full screen ;prevents mistimings if called too fast WaitFrame lda $d012 cmp #$F8 beq WaitFrame ;wait for the raster to reach line $f8 (should be closer to the start of this line this way) .WaitStep2 lda $d012 cmp #$F8 bne .WaitStep2 rts
step1.zip

Previous Step 1 Next Step 2

Endurion

Endurion

 

A c64 game - Step 2

And onwards we stumble!

In the first part we prepared everything for the VIC, now we set up our modified charset. Note that I used a selfmade tool (similar to CharPad), which is included (Windows binary). The file J.CHR contains the charset and is included into the source as binary. The memory layout of the game expects the modified charset at $f000. Since the C64 can't load files to locations after $C000 we have to copy the charset to the target memory at $f000. To be able to properly write at those addresses we need to switch off the ROM overlay.

The current step should display "HELLO". The rest of the screen depends on the current memory setup of your emulator/C64


First code piece we add is the copy routine. Interrupts are blocked because we turn off the kernal ROM. If we didn't the IRQ code would jump in the middle of uninitialised RAM, likely resulting in a crash. The RAM/ROM layout is influenced by memory address $1. ;---------------------- ;copy charset to target ;---------------------- ;block interrupts ;since we turn ROMs off this would result in crashes if we did not sei ;save old configuration lda $1 sta PARAM1 ;only RAM ;to copy under the IO rom lda #%00110000 sta $1 ;take source address from CHARSET LDA #<CHARSET STA ZEROPAGE_POINTER_1 LDA #>CHARSET STA ZEROPAGE_POINTER_1 + 1 ;now copy jsr CopyCharSet ;restore ROMs lda PARAM1 sta $1 cli   The actual copy routine. Note that we only copy 254 characters. The last two characters are omitted to not overwrite the default IRQ vectors residing at $fffb. Since we deal with a 8 bit machine there is an extra loop taking care of the high bytes of our addresses. At the end of the copy routine we include the binary charset data. !zone CopyCharSet CopyCharSet ;set target address ($F000) lda #$00 sta ZEROPAGE_POINTER_2 lda #$F0 sta ZEROPAGE_POINTER_2 + 1 ldx #$00 ldy #$00 lda #0 sta PARAM2 .NextLine lda (ZEROPAGE_POINTER_1),Y sta (ZEROPAGE_POINTER_2),Y inx iny cpx #$8 bne .NextLine cpy #$00 bne .PageBoundaryNotReached ;we reached the next 256 bytes, inc high byte inc ZEROPAGE_POINTER_1 + 1 inc ZEROPAGE_POINTER_2 + 1 .PageBoundaryNotReached ;only copy 254 chars to keep irq vectors intact inc PARAM2 lda PARAM2 cmp #254 beq .CopyCharsetDone ldx #$00 jmp .NextLine .CopyCharsetDone rts CHARSET !binary "j.chr"   To display HELLO on the screen we simple poke the character codes on the screen and also set the characters colors to white. ;test charset lda #'H' sta SCREEN_CHAR lda #'E' sta SCREEN_CHAR + 1 lda #'L' sta SCREEN_CHAR + 2 sta SCREEN_CHAR + 3 lda #'O' sta SCREEN_CHAR + 4 lda #1 sta SCREEN_COLOR sta SCREEN_COLOR + 1 sta SCREEN_COLOR + 2 sta SCREEN_COLOR + 3 sta SCREEN_COLOR + 4
Clarifications:

The charset of the C64 is using 8 bytes per character. This totals at 256 characters a 8 bytes = 2048 bytes. A custom character set can be positioned almost everywhere in RAM (at 2048 interval steps).

In hires text mode every bit corresponds to a pixel. In multicolor text mode pixels are doubling width, so two bits make up one pixel. In multicolor mode two colors are shared by all multi-color characters, one is the background color and one is the current char color.


The memory layout looks like this (nicked from www.c64-wiki.de): $FFFF = 65535 ????????????????????????????????? ?---------------?|||||||||||||||? ||| = read by PEEK ?---------------?|||||||||||||||? --- = written to by POKE ?---------------?|||||||||||||||? +++ = read and write ?---------------?||| KERNAL- |||? other = not reachable from BASIC ?---------------?||| ROM |||? ?---------------?|||||||||||||||? ?---------------?|||||||||||||||? $E000 = 57344 ????????????????????????????????????????????????? ? ? ?+++++++++++++++? ? ? CHAR ROM ?+++++ I/O +++++? ? ? ?+++++++++++++++? $D000 = 53248 ????????????????????????????????????????????????? ?+++++++++++++++? ?+++++++++++++++? ?+++++++++++++++? $C000 = 49152 ????????????????????????????????? ?---------------?|||||||||||||||? ?---------------?|||||||||||||||? ?---------------?||| BASIC- ||||? ?---------------?||| ROM ||||? ?---------------?|||||||||||||||? ?---------------?|||||||||||||||? ?---------------?|||||||||||||||? $A000 = 40960 ????????????????????????????????? ?+++++++++++++++? ?+++ BASIC- ++++? ?+++ RAM ++++? . . ?+++ BASIC- ++++? ?+++ RAM ++++? $800 = 2048 ?+++++++++++++++?-? $400 = 1024 ?+++++++++++++++?-?Default Screen Address $0000 ?????????????????-?Zeropage and Enhanced Zeropage
step2.zip

Previous Step 1b Next Step 3

Endurion

Endurion

 

A C64 game - Step 3

Quite similar to step 2 this time we set up sprites. Note that I used another selfmade tool (similar to SpritePad). The file J.SPR contains the sprites and is included into the source as binary. The sprites are located "under" the I/O ROM at $D000 onwards.
And another note: Since I'm lazy I built the sprite copy loop to always copy packages of 4 sprites (1 sprite comes as 63 + 1 byte, so 4 make nice 256 byte packages).

The current step should display the known charset garbage and "HELLO" as before, but also show an elegantly handicrafted sprite.


Right below the charset copy routine we add the copy call.

  ;take source address from SPRITES lda #<SPRITES sta ZEROPAGE_POINTER_1 lda #>SPRITES sta ZEROPAGE_POINTER_1 + 1 jsr CopySprites   The sprite copy routine is quite similar to the charset copy but a tad shorter due to the 256 byte packaging: ;------------------------------------------------------------ ;copies sprites from ZEROPAGE_POINTER_1 to ZEROPAGE_POINTER_2 ; sprites are copied in numbers of four ;------------------------------------------------------------ !zone CopySprites CopySprites ldy #$00 ldx #$00 lda #00 sta ZEROPAGE_POINTER_2 lda #$d0 sta ZEROPAGE_POINTER_2 + 1 ;4 sprites per loop .SpriteLoop lda (ZEROPAGE_POINTER_1),y sta (ZEROPAGE_POINTER_2),y iny bne .SpriteLoop inx inc ZEROPAGE_POINTER_1 + 1 inc ZEROPAGE_POINTER_2 + 1 cpx #NUMBER_OF_SPRITES_DIV_4 bne .SpriteLoop rts   In front of GameLoop we put the sprite display code. The sprite is positioned at coordinates 100,100, the sprite pointer set to the correct image and the sprite is enabled. ;set sprite 1 pos lda #100 sta VIC_SPRITE_X_POS sta VIC_SPRITE_Y_POS ;set sprite image lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE ;enable sprite 1 lda #1 sta VIC_SPRITE_ENABLE   step3.zip

Previous Step 2 Next Step 4

Endurion

Endurion

 

A C64 game - Step 4

Now we take a bigger step: Moving the sprite with the joystick. Since we want to make a real game we also allow to move the sprite over to the right side (in other words we'll take care of the extended x bit).

For clarification: The C64 has 8 hardware sprites. That's 8 objects of the size 24 x 21 pixels. They can be placed anywhere. The coordinates are stored in memory addresses. However since the X resolution is higher than 256 all the sprites 9th bit is stored in another memory location (which makes it highly annoying to work with).
Sprite coordinates are set in X, Y pairs via the memory locations 53248 (=X sprite 0), 53249 (=Y sprite 0), 53250 (=X sprite 1), etc. The extended sprite bits are stored in 53248 + 16.

Since I don't plan to allow sprites to go off screen in the game later there is no defined behaviour if you move the sprite off screen too far. It'll simply bounce back in once the coordinate wraps around.

The joystick ports can be checked via the memory locations 56320 (Port 2) or 56321 (Port 1). The lower 5 bits are cleared(!) if either up, down, left, right for fire is pressed.


This step shows:
-Joystick control
-Sprite extended x bit



Inside the GameLoop we add a call to the players control function: jsr PlayerControl   PlayerControl itself checks the joystick port (II) and calls the proper direction move routines. Note that the move routines themselves simply set the object index to 0 (for the player) and call a generic sprite move routine.
  ;------------------------------------------------------------ ;check joystick (player control) ;------------------------------------------------------------ !zone PlayerControl PlayerControl lda #$2 bit $dc00 bne .NotDownPressed jsr PlayerMoveDown .NotDownPressed lda #$1 bit $dc00 bne .NotUpPressed jsr PlayerMoveUp .NotUpPressed lda #$4 bit $dc00 bne .NotLeftPressed jsr PlayerMoveLeft .NotLeftPressed lda #$8 bit $dc00 bne .NotRightPressed jsr PlayerMoveRight .NotRightPressed rts PlayerMoveLeft ldx #0 jsr MoveSpriteLeft rts PlayerMoveRight ldx #0 jsr MoveSpriteRight rts PlayerMoveUp ldx #0 jsr MoveSpriteUp rts PlayerMoveDown ldx #0 jsr MoveSpriteDown rts   The sprite move routines are rather simple, update the position counter variables and set the actual sprite registers. A bit more complicated are the X move functions. If X reaches the wraparound, the extended x bit (the 9th bit) is looked up in a table and then added/removed.
  ;------------------------------------------------------------ ;Move Sprite Left ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteLeft MoveSpriteLeft dec SPRITE_POS_X,x bpl .NoChangeInExtendedFlag lda BIT_TABLE,x eor #$ff and SPRITE_POS_X_EXTEND sta SPRITE_POS_X_EXTEND sta VIC_SPRITE_X_EXTEND .NoChangeInExtendedFlag txa asl tay lda SPRITE_POS_X,x sta VIC_SPRITE_X_POS,y rts ;------------------------------------------------------------ ;Move Sprite Right ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteRight MoveSpriteRight inc SPRITE_POS_X,x lda SPRITE_POS_X,x bne .NoChangeInExtendedFlag lda BIT_TABLE,x ora SPRITE_POS_X_EXTEND sta SPRITE_POS_X_EXTEND sta VIC_SPRITE_X_EXTEND .NoChangeInExtendedFlag txa asl tay lda SPRITE_POS_X,x sta VIC_SPRITE_X_POS,y rts ;------------------------------------------------------------ ;Move Sprite Up ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteUp MoveSpriteUp dec SPRITE_POS_Y,x txa asl tay lda SPRITE_POS_Y,x sta 53249,y rts ;------------------------------------------------------------ ;Move Sprite Down ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteDown MoveSpriteDown inc SPRITE_POS_Y,x txa asl tay lda SPRITE_POS_Y,x sta 53249,y rts step4.zip

Previous Step 3 Next Step 5

Endurion

Endurion

 

A C64 game - Step 5

Our next big step:

Obviously we want some play screen, and more obviously, we are not going to store full screens (we've got only 64Kb Ram after all). Therefore there's a level build routine that allows to build a screen with various building elements. For now we'll start out with vertical and horizontal lines. Since we always have a level border at the screen edges we'll have the border as a second screen data block (LEVEL_BORDER_DATA).
To allow faster screen building we also have a table with the precalculated char offsets of a vertical line (SCREEN_LINE_OFFSET_TABLE_LO and SCREEN_LINE_OFFSET_TABLE_HI). Note that with the C64s mnemonics the best way to get stuff done is tables, tables, tables.
The BuildScreen sub routine clears the play screen area, builds the level data and then the border. The level data is a collection of primitives which are then worked through until LD_END is hit.

The first addition is the call to the buildroutine:
  ;setup level lda #0 sta LEVEL_NR jsr BuildScreen   The BuildScreen routine sets up a level completely. It starts out with clearing the current play area (not the full screen). Then LEVEL_NR is used to look up the location of the level data in table SCREEN_DATA_TABLE.
.BuildLevel is jumped to to actually work through the level primitives and put them on screen. Then we use LEVEL_BORDER_DATA as second level to display a border on the screens edges. .BuildLevel uses Y as index through the data. Depending on the first byte different routines are called (.LineH, .LineV, .LevelComplete). Since I think levels may be complex and use more than 256 bytes we add Y to the level data pointers so we can start out with Y=0 for the next primitive.
  ;------------------------------------------------------------ ;BuildScreen ;creates a screen from level data ;------------------------------------------------------------ !zone BuildScreen BuildScreen lda #0 ldy #6 jsr ClearPlayScreen ;get pointer to real level data from table ldx LEVEL_NR lda SCREEN_DATA_TABLE,x sta ZEROPAGE_POINTER_1 lda SCREEN_DATA_TABLE + 1,x sta ZEROPAGE_POINTER_1 + 1 jsr .BuildLevel ;get pointer to real level data from table lda #<LEVEL_BORDER_DATA sta ZEROPAGE_POINTER_1 lda #>LEVEL_BORDER_DATA sta ZEROPAGE_POINTER_1 + 1 jsr .BuildLevel rts .BuildLevel ;work through data ldy #255 .LevelDataLoop iny lda (ZEROPAGE_POINTER_1),y cmp #LD_END beq .LevelComplete cmp #LD_LINE_H beq .LineH cmp #LD_LINE_V beq .LineV .LevelComplete rts .NextLevelData pla ;adjust pointers so we are able to access more ;than 256 bytes of level data clc adc #1 adc ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_1 lda ZEROPAGE_POINTER_1 + 1 adc #0 sta ZEROPAGE_POINTER_1 + 1 ldy #255 jmp .LevelDataLoop   The primitive display routines .LineH and .LineV are rather straight forward. Read the parameters, and put the character and color values in place.
  .LineH ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;width iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;char iny lda (ZEROPAGE_POINTER_1),y sta PARAM4 ;color iny lda (ZEROPAGE_POINTER_1),y sta PARAM5 ;store target pointers to screen and color ram ldx PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 tya pha ldy PARAM1 .NextChar lda PARAM4 sta (ZEROPAGE_POINTER_2),y lda PARAM5 sta (ZEROPAGE_POINTER_3),y iny dec PARAM3 bne .NextChar jmp .NextLevelData .LineV ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;height iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;char iny lda (ZEROPAGE_POINTER_1),y sta PARAM4 ;color iny lda (ZEROPAGE_POINTER_1),y sta PARAM5 ;store target pointers to screen and color ram ldx PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 tya pha ldy PARAM1 .NextCharV lda PARAM4 sta (ZEROPAGE_POINTER_2),y lda PARAM5 sta (ZEROPAGE_POINTER_3),y ;adjust pointer lda ZEROPAGE_POINTER_2 clc adc #40 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda ZEROPAGE_POINTER_2 + 1 adc #0 sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 dec PARAM3 bne .NextCharV jmp .NextLevelData
step5.zip

Previous Step 4 Next Step 6

Endurion

Endurion

 

A C64 Game - Step 6

And onwards we go: Obviously we don't want the player to move through walls. In this step we check the chars in the players way to see if they are blocking.
To make this easier we store the character pos and the character delta pos (0 to 7) for x and y for every sprite (SPRITE_CHAR_POS_X, SPRITE_CHAR_POS_X_DELTA). If the sprite is not at a character brink the move is allowed, if it hits the brink, check the characters at the target.

For this step any character equal or above index 128 is considered blocking, any below is free to move.

The collision code assumes that the collision box of a sprite is one char wide and two chars high.

This step shows:
-calculating character positions while moving about
-checking the position for blocking chars
-calculating the required sprite position from character pos (for starting a sprite at a specific place)

Since the code is basically the same for all four directions I'll only go into details on one of them:
  ;------------------------------------------------------------ ;PlayerMoveLeft ;------------------------------------------------------------ !zone PlayerMoveLeft PlayerMoveLeft ldx #0 ;check if we are on the brink of a character lda SPRITE_CHAR_POS_X_DELTA beq .CheckCanMoveLeft ;no, we are not .CanMoveLeft dec SPRITE_CHAR_POS_X_DELTA jsr MoveSpriteLeft rts .CheckCanMoveLeft lda SPRITE_CHAR_POS_Y_DELTA beq .NoThirdCharCheckNeeded ;find the character in the screen buffer ldy SPRITE_CHAR_POS_Y lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 lda SPRITE_CHAR_POS_X clc adc #39 ;39 equals one line down (40 chars) and one to the left (-1) tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft .NoThirdCharCheckNeeded ldy SPRITE_CHAR_POS_Y dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X dey lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft tya clc adc #40 tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft lda #8 sta SPRITE_CHAR_POS_X_DELTA dec SPRITE_CHAR_POS_X jmp .CanMoveLeft .BlockedLeft rts   The subroutine IsCharBlocking is rather primitive, as described it only checks if the character is smaller than 128:
  ;------------------------------------------------------------ ;IsCharBlocking ;checks if a char is blocking ;A contains the character ;returns 1 for blocking, 0 for not blocking ;------------------------------------------------------------ !zone IsCharBlocking IsCharBlocking cmp #128 bpl .Blocking lda #0 rts .Blocking lda #1 rts
step6.zip

Previous Step 5 Next Step 7

Endurion

Endurion

 

A C64 game - Step 7

Now it's starting to resemble a game. Loosely.
In this step we add gravity and jumping. The player will fall if there is no blocking char below. On joystick up the player jumps in a curve.
Both fall speed and jump speed are non linear and based on tables.

This step shows:
-gravity (accelerating)
-jumping (following a delta y curve)

Most prominent addition are the jump and fall table. These hold the deltas we use to make the movement not look linear but somewhat naturalistic:


  PLAYER_JUMP_POS !byte 0 PLAYER_JUMP_TABLE !byte 8,7,5,3,2,1,1,1,0,0 PLAYER_FALL_POS !byte 0 FALL_SPEED_TABLE !byte 1,1,2,2,3,3,3,3,3,3


The jump is only possible if the player is not falling. Once the player jumped the PLAYER_JUMP_POS is increased on every frame and the player moved upwards for entry-of-jump-table pixel. If the player is blocked moving upwards the jump is aborted:


  .PlayerIsJumping inc PLAYER_JUMP_POS lda PLAYER_JUMP_POS cmp #JUMP_TABLE_SIZE bne .JumpOn lda #0 sta PLAYER_JUMP_POS jmp .JumpComplete .JumpOn ldx PLAYER_JUMP_POS lda PLAYER_JUMP_TABLE,x beq .JumpComplete sta PARAM5 .JumpContinue jsr PlayerMoveUp beq .JumpBlocked dec PARAM5 bne .JumpContinue jmp .JumpComplete .JumpBlocked lda #0 sta PLAYER_JUMP_POS jmp .JumpStopped

To check for falling an attempt is made to move the player down one pixel. If he is blocked he is standing on solid ground. If he can fall the fall counter is increased. The fall counter is increased in every frame up to the max number of entries in the fall table.

If the player is falling the player is moved down entry-of-fall-table pixel. .PlayerFell ldx PLAYER_FALL_POS lda FALL_SPEED_TABLE,x beq .FallComplete sta PARAM5 .FallLoop dec PARAM5 beq .FallComplete jsr PlayerMoveDown jmp .FallLoop .FallComplete lda PLAYER_FALL_POS cmp #( FALL_TABLE_SIZE - 1 ) beq .FallSpeedAtMax inc PLAYER_FALL_POS .FallSpeedAtMax


step7.zip

Previous Step 6 Next Step 8

Endurion

Endurion

 

A C64 Game - Final Step

And thus this create a game series ends...

We fix the last few bugs (score was not properly reset on replay, music was stuck on saving the scores). Now the game is complete and can be enjoyed as it was meant to be!



Oh joy, Smila (the graphician) actually touched up the game even more for a retail release. It has been out for a few weeks now, but if you didn't know, go here:
Psytronik (for digital download, tape or disk) or to RGCD (cartridge with nifty packaging and stickers).
Or simply marvel in all the nice games that are still being made for older computers.

There you can actually buy a severely enhanced version (more two player game modes, better graphic, gameplay enhancements) as either digital download for emulators, or as a real tape, disk and finally as cartridge!

Thank you for your encouragements throughout, and keep on coding

step100.zip

Previous Step  

Endurion

Endurion

 

A C64 game - Step 13

And yet again a bigger step.


Of course we need lots of goodies from the killed enemies. We add items. Items are displayed as 2x2 block of characters. There's a new list of possible items with location added (ITEM_ACTIVE, etc.). The list also stores the original background behind the items.
To get an item spawned just walk beside one of the enemies (look in their direction) and keep fire pressed until it dies.

Note that the player cannot collect the items yet (that's up for the next step).




Inside the player subroutine FireShot we add another subroutine call after killing an enemy: .EnemyKilled jsr RemoveObject jsr SpawnItem   SpawnItem itself resembles the AddObject routine. First we loop over the active item table (ITEM_ACTIVE) to find a free slot.

Once found we randomly chose the item type (for now there are two types). The ugly part below that stores the original character and color at the item position and puts the items char and color in its place. Remember the item is sized 2x2 chars, so we need to store 8 bytes overall. However to keep the code comfortable, we actually use 8 tables. This allows use to only work with the item index instead of manually accessing a second index. There's only two index registers after all.
  ;------------------------------------------------------------ ;spawns an item at char position from object x ;X = object index ;------------------------------------------------------------ !zone SpawnItem SpawnItem ;find free item slot ldy #0 .CheckNextItemSlot lda ITEM_ACTIVE,y cmp #ITEM_NONE beq .FreeSlotFound iny cpy #ITEM_COUNT bne .CheckNextItemSlot rts .FreeSlotFound jsr GenerateRandomNumber and #$1 sta ITEM_ACTIVE,y lda SPRITE_CHAR_POS_X,x sta ITEM_POS_X,y lda SPRITE_CHAR_POS_Y,x sta ITEM_POS_Y,y sty PARAM1 ;find address in screen buffer... tay lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ;...and for the color buffer clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_2 + 1 ldy SPRITE_CHAR_POS_X,x ldx PARAM1 ;store old background and put item ;we do not take overlapping items in account yet! lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_UL,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_UL,x lda ITEM_CHAR_UL,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_UL,x sta (ZEROPAGE_POINTER_2),y iny lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_UR,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_UR,x lda ITEM_CHAR_UR,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_UR,x sta (ZEROPAGE_POINTER_2),y tya clc adc #39 tay lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_LL,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_LL,x lda ITEM_CHAR_LL,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_LL,x sta (ZEROPAGE_POINTER_2),y iny lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_LR,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_LR,x lda ITEM_CHAR_LR,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_LR,x sta (ZEROPAGE_POINTER_2),y rts
step13.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 8

Of course a game isn't a game without some challenge. Therefore we need enemies. Since we have some neat little level build code why not use it for enemies as well?
We add a new level primitive type LD_OBJECT which adds objects (= sprites). We use it for both player and enemies. A new table SPRITE_ACTIVE is added to see if a sprite is used (and which type).


One central function to this is FindEmptySpriteSlot. It iterates over the sprite active table and looks for a free slot to use. If there is a free slot we set the object active, apply the object startup values and use the previously created CalcSpritePosFromCharPos to place the sprite.


Note that we don't plan to use more than 8 objects so we can directly map object to sprites.


Find an empty slot: ;------------------------------------------------------------ ;Looks for an empty sprite slot, returns in X ;#1 in A when empty slot found, #0 when full ;------------------------------------------------------------ !zone FindEmptySpriteSlot FindEmptySpriteSlot ldx #0 .CheckSlot lda SPRITE_ACTIVE,x beq .FoundSlot inx cpx #8 bne .CheckSlot lda #0 rts .FoundSlot lda #1 rts

How we add an object during level buildup:
  .Object ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha ;add object to sprite array jsr FindEmptySpriteSlot beq .NoFreeSlot lda PARAM3 sta SPRITE_ACTIVE,x ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ;enable sprite lda BIT_TABLE,x ora VIC_SPRITE_ENABLE sta VIC_SPRITE_ENABLE lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE,x .NoFreeSlot jmp .NextLevelData
step8.zip


Previous Step 7 Next Step 9

Endurion

Endurion

 

A C64 game - Step 9

What are enemies if they just sit put and don't move at all? Therefore we now add the sub routine ObjectControl. ObjectControl loops through all objects (even the player) and jumps to the behaviour function depending on the object type. This incurs that the behaviour is tied to the object type. We provide a table with function pointers to every object's behaviour code (including the player).

ObjectControl takes the object type as index into the table and jumps to the target address. For now we have two enemy types, dumb moving up/down or left/right. For moving we reuse the previously created functions we already use for the player, namely ObjectMoveLeft/ObjectMoveRight etc.

Loop over all active objects and jump at their behaviour code. Note that we apply a nasty trick. Since jsr doesn't allow for indirect jumps we manually push the return address on the stack and then call indirect jmp. This allows for the behaviour code to return with rts.
  ;------------------------------------------------------------ ;Enemy Behaviour ;------------------------------------------------------------ !zone ObjectControl ObjectControl ldx #0 .ObjectLoop ldy SPRITE_ACTIVE,x beq .NextObject ;enemy is active dey lda ENEMY_BEHAVIOUR_TABLE_LO,y sta ZEROPAGE_POINTER_2 lda ENEMY_BEHAVIOUR_TABLE_HI,y sta ZEROPAGE_POINTER_2 + 1 ;set up return address for rts lda #>( .NextObject - 1 ) pha lda #<( .NextObject - 1 ) pha jmp (ZEROPAGE_POINTER_2) .NextObject inx cpx #8 bne .ObjectLoop rts   The main game loop is now changed; removed the call of PlayerControl and added the call to ObjectControl: ;------------------------------------------------------------ ;the main game loop ;------------------------------------------------------------ GameLoop jsr WaitFrame jsr ObjectControl jmp GameLoop   The behaviour table is built from the behaviour code addresses. Actually we use two tables for high and low byte, this way we don't have to mess with the index. The < and > operators return the low and high byte of a 16 bit value.
  ENEMY_BEHAVIOUR_TABLE_LO !byte <PlayerControl !byte <BehaviourDumbEnemyLR !byte <BehaviourDumbEnemyUD ENEMY_BEHAVIOUR_TABLE_HI !byte >PlayerControl !byte >BehaviourDumbEnemyLR !byte >BehaviourDumbEnemyUD
step9.zip



Previous Step 8 Next Step 10

Endurion

Endurion

 

A C64 game - Step 10

So you found out the enemies couldn't hurt you? Well, we're working towards that goal in this step. We add collision checks. Since I'm not completely sure about later changes we are NOT relying on the VICs collision checks but roll our own. Remember the object size contraints from step #6? We apply those to the object collision checks as well.

We add a new subroutine CheckCollisions which in turn uses IsEnemyCollidingWithPlayer. We do not check for collisions between enemies. The check is not completely exact (behaves more like 9 pixel * 16 pixel), but that's good enough. To test the function a collision is signalled by setting the border color to white.

  The routine CheckCollisions is simply added to the main game loop: GameLoop jsr WaitFrame jsr ObjectControl jsr CheckCollisions jmp GameLoop   The function CheckCollision just loops through the active object list and calls IsEnemyCollidingWithPlayer for every active entry:
  ;------------------------------------------------------------ ;check object collisions (enemy vs. player etc.) ;x ;------------------------------------------------------------ CheckCollisions ldx #1 .CollisionLoop lda SPRITE_ACTIVE,x bne .CheckObject .NextObject inx cpx #8 bne .CollisionLoop lda #0 sta VIC_BORDER_COLOR rts .CheckObject stx PARAM2 jsr IsEnemyCollidingWithPlayer bne .PlayerCollidedWithEnemy ldx PARAM2 jmp .NextObject .PlayerCollidedWithEnemy lda #1 sta VIC_BORDER_COLOR ;ldx #0 ;jsr RemoveObject rts   IsEnemyCollidingWithPlayer employs a few tricks to ease the calculation.
First we do the Y coordinate check to weed out. For the X coordinate: Since the actual X position is 9 bits we half the value (half the X coordinate and add 128 if the extended X bit is set). Now the comparation is easy.
The routine then returns 1 if a collision occurs and 0 if not.
  ;------------------------------------------------------------ ;check object collision with player (object 0) ;x = enemy index ;return a = 1 when colliding, a = 0 when not ;------------------------------------------------------------ !zone IsEnemyCollidingWithPlayer .CalculateSimpleXPos ;Returns a with simple x pos (x halved + 128 if > 256) ;modifies y lda BIT_TABLE,x and SPRITE_POS_X_EXTEND beq .NoXBit lda SPRITE_POS_X,x lsr clc adc #128 rts .NoXBit lda SPRITE_POS_X,x lsr rts IsEnemyCollidingWithPlayer ;modifies X ;check y pos lda SPRITE_POS_Y,x sec sbc #( OBJECT_HEIGHT ) ;offset to bottom cmp SPRITE_POS_Y bcs .NotTouching clc adc #( OBJECT_HEIGHT + OBJECT_HEIGHT - 1 ) cmp SPRITE_POS_Y bcc .NotTouching ;X = Index in enemy-table jsr .CalculateSimpleXPos sta PARAM1 ldx #0 jsr .CalculateSimpleXPos sec sbc #4 ;position X-Anfang Player - 12 Pixel cmp PARAM1 bcs .NotTouching adc #8 cmp PARAM1 bcc .NotTouching lda #1 rts .NotTouching lda #0 rts   step10.zip
Previous Step 9 Next Step 11

Endurion

Endurion

 

A C64 game - Step 11

From colliding to dying is a small step. Once the player collides with an enemy we kill him by removing the player object. A "Press Fire to Restart" message is displayed and a press on the button will revive the player object.
We add the function RemoveObject which simply removes the object from the SPRITE_ACTIVE table and disables its sprite. While we wait for the player to press the button all the rest of the game moves on.


First of all we add the getting killed part in our old routine "CheckCollisions". Nothing ground breaking, a call to the text display function follows by removing the object and resetting the button released flag.
  .PlayerCollidedWithEnemy ;display text lda #<TEXT_PRESS_FIRE sta ZEROPAGE_POINTER_1 lda #>TEXT_PRESS_FIRE sta ZEROPAGE_POINTER_1 + 1 lda #10 sta PARAM1 lda #23 sta PARAM2 jsr DisplayText ldx #0 stx BUTTON_PRESSED stx BUTTON_RELEASED jsr RemoveObject rts

A new call is added to the main game loop which controls behaviour when the player is dead: GameLoop jsr WaitFrame jsr DeadControl jsr ObjectControl jsr CheckCollisions jmp GameLoop   Surprisingly easy. We check if the player is really dead, if he isn't, bail out. Then we check for the joystick button being pressed, but only allow to go on, if the button has been released before. If all that happened, we simply force the player object back into life (for now with hard coded values).
  !zone DeadControl DeadControl lda SPRITE_ACTIVE beq .PlayerIsDead rts .PlayerIsDead lda #$10 bit $dc00 bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED bne .Restart rts .ButtonNotPressed lda #1 sta BUTTON_RELEASED rts .Restart lda #5 sta PARAM1 lda #4 sta PARAM2 ;type lda #TYPE_PLAYER sta PARAM3 ldx #0 lda PARAM3 sta SPRITE_ACTIVE,x ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ;enable sprite lda BIT_TABLE,x ora VIC_SPRITE_ENABLE sta VIC_SPRITE_ENABLE ;initialise enemy values lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE,x ;look right per default lda #0 sta SPRITE_DIRECTION,x rts
step11.zip

Previous Step 10Next Step

Endurion

Endurion

 

A C64 game - Step 17

Another rather small step, but visually pleasing. We're enhancing the player sprite with animation and better jump abilities.

All the hard work is added to PlayerControl. On every movement we update the sprite while checking the player states like jumping, recoil, falling, etc. Suddenly things look more interesting

It's basically updating and checking counters during different control parts. SPRITE_ANIM_DELAY is used for controlling animation speed while SPRITE_ANIM_POS is used for the animation frame.



Here are the new parts for walking left:
  ;animate player lda SPRITE_FALLING bne .NoAnimLNeeded lda PLAYER_JUMP_POS bne .NoAnimLNeeded inc SPRITE_ANIM_DELAY lda SPRITE_ANIM_DELAY cmp #8 bne .NoAnimLNeeded lda #0 sta SPRITE_ANIM_DELAY inc SPRITE_ANIM_POS lda SPRITE_ANIM_POS and #$3 sta SPRITE_ANIM_POS .NoAnimLNeeded   The same for right movement:
  ;animate player lda SPRITE_FALLING bne .NoAnimRNeeded lda PLAYER_JUMP_POS bne .NoAnimRNeeded inc SPRITE_ANIM_DELAY lda SPRITE_ANIM_DELAY cmp #8 bne .NoAnimRNeeded lda #0 sta SPRITE_ANIM_DELAY inc SPRITE_ANIM_POS lda SPRITE_ANIM_POS and #$3 sta SPRITE_ANIM_POS .NoAnimRNeeded   And all the missing animation for jumping, falling, recoil and combined states. Note that the sprites are arranged in right/left pairs, so that adding SPRITE_DIRECTION (0 = facing right, 1 = facing left) to the sprite frame results in the proper sprite.
  ;update player animation lda SPRITE_FALLING bne .AnimFalling lda PLAYER_JUMP_POS bne .AnimJumping ;is player shooting? lda PLAYER_SHOT_PAUSE beq .AnimNoRecoil ;recoil anim lda SPRITE_ANIM_POS asl clc adc SPRITE_DIRECTION adc #SPRITE_PLAYER_WALK_R_1 adc #8 sta SPRITE_POINTER_BASE rts .AnimNoRecoil lda SPRITE_ANIM_POS asl clc adc SPRITE_DIRECTION adc #SPRITE_PLAYER_WALK_R_1 sta SPRITE_POINTER_BASE rts .AnimFalling lda PLAYER_SHOT_PAUSE bne .AnimFallingNoRecoil lda #SPRITE_PLAYER_FALL_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts .AnimFallingNoRecoil lda #SPRITE_PLAYER_FALL_RECOIL_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts .AnimJumping lda PLAYER_SHOT_PAUSE bne .AnimJumpingNoRecoil lda #SPRITE_PLAYER_JUMP_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts .AnimJumpingNoRecoil lda #SPRITE_PLAYER_JUMP_RECOIL_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts   step17.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 12

One of the more complex steps. And also one I someday need to heavily optimize. The player can now shoot an enemy.

The central function for this is FireShot. We don't use a bullet but insta-shot. However walls should block the shot as well. This means, we need to take the current player direction and position, and work our way to the left/right until we hit an enemy or a wall.
Since there's no direct collision involved we take the character pos of the player, add or decrease the x pos and compare against all alive enemies pos. Rinse and repeat until done.
I've given the enemies 5 HP, and the player has a shot delay of 10 frames. Therefore it takes a while for the enemy to disappear (best tested with the box on top). If the player is purple the shot delay is active.
We start with adding a fire delay to PlayerControl:
  lda PLAYER_SHOT_PAUSE bne .FirePauseActive lda #1 sta VIC_SPRITE_COLOR lda #$10 bit $dc00 bne .NotFirePushed jsr FireShot jmp .FireDone .FirePauseActive dec PLAYER_SHOT_PAUSE .FireDone .NotFirePushed
This simply checks for PLAYER_SHOT_PAUSE. If it is higher than 0 the player is still pausing. If so the counter is decreased and the fire function skipped. If the counter is zero, we check the fire button and if pressed call the FireShot routine.

The FireShot routine is not that complicated, however it's taking its processing time. First set the fire pause to 10 frames. Mark the player as shooting by changing his color.

Now the hard part. There is no visible bullet. So we take the current player position, increase/decrease X and check for a blocking char or a hittable enemy. If the bullet is blocked, done. If an enemy is hit, decrease its health by one point. Once the health is down to zero the enemy is removed.
  !zone FireShotFireShot ;frame delay until next shot lda #10 sta PLAYER_SHOT_PAUSE ;mark player as shooting lda #4 sta VIC_SPRITE_COLOR ldy SPRITE_CHAR_POS_Y dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X .ShotContinue lda SPRITE_DIRECTION beq .ShootRight ;shooting left dey lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .ShotDone jmp .CheckHitEnemy .ShootRight iny lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .ShotDone .CheckHitEnemy ;hit an enemy? ldx #1 .CheckEnemy stx PARAM2 lda SPRITE_ACTIVE,x beq .CheckNextEnemy tax lda IS_TYPE_ENEMY,x beq .CheckNextEnemy ;sprite pos matches on x? ldx PARAM2 sty PARAM1 lda SPRITE_CHAR_POS_X,x cmp PARAM1 bne .CheckNextEnemy ;sprite pos matches on y? lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y beq .EnemyHit ;sprite pos matches on y + 1? clc adc #1 cmp SPRITE_CHAR_POS_Y beq .EnemyHit ;sprite pos matches on y - 1? sec sbc #2 cmp SPRITE_CHAR_POS_Y bne .CheckNextEnemy .EnemyHit ;enemy hit! dec SPRITE_HP,x lda SPRITE_HP,x beq .EnemyKilled jmp .ShotDone .EnemyKilled jsr RemoveObject jmp .ShotDone .CheckNextEnemy ldx PARAM2 inx cpx #8 bne .CheckEnemy jmp .ShotContinue .ShotDone rts   step12.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 14

And onwards we go. Picking up items showed a new problem. When an item is picked up we want the background behind restored (plus the item characters should not cut holes into the play field, allowing the player to fall through floors).

In the end I decided to have a second back buffer screen that contains the original play screen. Now every time an item is removed the character blocks are copied from the back buffer. Also, the back buffer is now used for collision detection. I could not avoid having to redraw the still existing item images in case the removed item was overlapping.

Effectively we double the work during level building. We start out with the new "buffers":
  ;address of the screen backbuffer SCREEN_BACK_CHAR = $C800 ;address of the screen backbuffer SCREEN_BACK_COLOR = $C400
After calling the BuildScreen subroutine we copy the screen and color RAM to the backup buffers. Note the check for 230 bytes. We only have a play field of 40x23 characters, so only 4 * 230 = 920 bytes are needed. ;copy level data to back buffer ldx #$00 .ClearLoop lda SCREEN_CHAR,x sta SCREEN_BACK_CHAR,x lda SCREEN_CHAR + 230,x sta SCREEN_BACK_CHAR + 230,x lda SCREEN_CHAR + 460,x sta SCREEN_BACK_CHAR + 460,x lda SCREEN_CHAR + 690,x sta SCREEN_BACK_CHAR + 690,x inx cpx #230 bne .ClearLoop ldx #$00 .ColorLoop lda SCREEN_COLOR,x sta SCREEN_BACK_COLOR,x lda SCREEN_COLOR + 230,x sta SCREEN_BACK_COLOR + 230,x lda SCREEN_COLOR + 460,x sta SCREEN_BACK_COLOR + 460,x lda SCREEN_COLOR + 690,x sta SCREEN_BACK_COLOR + 690,x inx cpx #230 bne .ColorLoop
The repaint item function is thusly modified to simply copy the character and color values from the backup buffer:
  ;------------------------------------------------------------ ;remove item image from screen ;Y = item index ;------------------------------------------------------------ !zone RemoveItemImage RemoveItemImage sty PARAM2 ;set up pointers lda ITEM_POS_Y,y tay lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_2 + 1 sec sbc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 sec sbc #( ( SCREEN_BACK_CHAR - SCREEN_BACK_COLOR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_4 + 1 ldx PARAM2 ldy ITEM_POS_X,x ;... and copying lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y iny lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y tya clc adc #39 tay lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y iny lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y ;repaint other items to avoid broken overlapped items ldx #0 .RepaintLoop lda ITEM_ACTIVE,x cmp #ITEM_NONE beq .RepaintNextItem txa pha jsr PutItemImage pla tax .RepaintNextItem inx cpx #ITEM_COUNT bne .RepaintLoop ldy PARAM2 rts
step14.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 18

This time we add some enemy animation and a path based movement enemy type. The movement path is stored in a table of delta X and delta Y values. Values with the highest bit set are treated as negative.

The animation of the bat is also stored in a table (it's a simple ping pong loop).

Every objects get an animation delay (SPRITE_ANIM_DELAY), animation pos (SPRITE_ANIM_POS) and movement pos counter (SPRITE_MOVE_POS).





Remember, adding a new type means just adding the new constant and entries to the startup value tables.
If you wonder about the flickering white border on the bottom half: It's an easy way to see how long the actual per frame code runs. You'll notice more complex code taking quite a bit more time.

Here's a detailed look at the path code. It's actually pretty straight forward. Read the next byte. Check if the high bit is set and use the result to either move left/right. Rinse and repeat for Y.
  ;------------------------------------------------------------ ;move in flat 8 ;------------------------------------------------------------ !zone BehaviourBat8 BehaviourBat8 ;do not update animation too fast lda DELAYED_GENERIC_COUNTER and #$3 bne .NoAnimUpdate inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$3 sta SPRITE_ANIM_POS,x tay lda BAT_ANIMATION,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x and #31 sta SPRITE_MOVE_POS,x ;process next path pos tay lda PATH_8_DX,y beq .NoXMoveNeeded sta PARAM1 and #$80 beq .MoveRight ;move left lda PARAM1 and #$7f sta PARAM1 .MoveLeft jsr MoveSpriteLeft dec PARAM1 bne .MoveLeft jmp .XMoveDone .MoveRight jsr MoveSpriteRight dec PARAM1 bne .MoveRight .NoXMoveNeeded .XMoveDone ldy SPRITE_MOVE_POS,x lda PATH_8_DY,y beq .NoYMoveNeeded sta PARAM1 and #$80 beq .MoveDown ;move up lda PARAM1 and #$7f sta PARAM1 .MoveUp jsr MoveSpriteUp dec PARAM1 bne .MoveUp rts .MoveDown jsr MoveSpriteDown dec PARAM1 bne .MoveDown .NoYMoveNeeded rts   The tables themselves are handmade. For the planned path we just need to make sure we end up where we started: PATH_8_DX !byte $86 !byte $86 !byte $85 !byte $84 !byte $83 !byte $82 !byte $81 !byte 0 !byte 0 !byte 1 !byte 2 !byte 3 !byte 4 !byte 5 !byte 6 !byte 6 !byte 6 !byte 6 !byte 5 !byte 4 !byte 3 !byte 2 !byte 1 !byte 0 !byte 0 !byte $81 !byte $82 !byte $83 !byte $84 !byte $85 !byte $86 !byte $86 PATH_8_DY !byte 0 !byte 1 !byte 2 !byte 3 !byte 4 !byte 5 !byte 6 !byte 6 !byte 6 !byte 6 !byte 5 !byte 4 !byte 3 !byte 2 !byte 1 !byte 0 !byte 0 !byte $81 !byte $82 !byte $83 !byte $84 !byte $85 !byte $86 !byte $86 !byte $86 !byte $86 !byte $85 !byte $84 !byte $83 !byte $82 !byte $81 !byte 0   step18.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 16

Now for some rather small addition, which however feels like a bigger step: Score/Live/Level display.

We already have a text display function, so we add a new default text for the display with initial 00000 values. Note that the score doesn't fit into a byte easily. We only update the numbers on the screen, we do not store the score in another location.
This makes it quite easy to update. For every step we start at the right most digit and increase it. If it hits the digit after '9', set to '0' again and repeat the step on char to the left. For retro sake we don't start at the right most score digit, but the second right most (making increase steps always being 10). If you look closer at a lot of older games you'll see that their right most score digit never changes (Bubble Bobble, etc.)

  Small text entry:
  TEXT_DISPLAY !text " SCORE: 000000 LIVES: 03 LEVEL: 00 *"   Increase score bit:
  ;------------------------------------------------------------ ;increases score by A ;note that the score is only shown ; not held in a variable ;------------------------------------------------------------ !zone IncreaseScore IncreaseScore sta PARAM1 stx PARAM2 sty PARAM3 .IncreaseBy1 ldx #4 .IncreaseDigit inc SCREEN_CHAR + ( 23 * 40 + 8 ),x lda SCREEN_CHAR + ( 23 * 40 + 8 ),x cmp #58 bne .IncreaseBy1Done ;looped digit, increase next lda #48 sta SCREEN_CHAR + ( 23 * 40 + 8 ),x dex ;TODO - this might overflow jmp .IncreaseDigit .IncreaseBy1Done dec PARAM1 bne .IncreaseBy1 ;increase complete, restore x,y ldx PARAM2 ldy PARAM3 rts   Another neat effect is the display of the level number and lives. Due to the hard coded screen position I've made two specialized functions instead of a generic one.

Interesting anecdote:

When I first had to display a decimal number I was stumped due to no available div operator. You actually need to divide by yourself (subtract divisor and increase count until done). That's what the call to DivideBy10 does.
  ;------------------------------------------------------------ ;displays level number ;------------------------------------------------------------ !zone DisplayLevelNumber DisplayLevelNumber lda LEVEL_NR clc adc #1 jsr DivideBy10 pha ;10 digit tya clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 37 ) pla clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 38 ) rts ;------------------------------------------------------------ ;displays live number ;------------------------------------------------------------ !zone DisplayLiveNumber DisplayLiveNumber lda PLAYER_LIVES jsr DivideBy10 pha ;10 digit tya clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 24 ) pla clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 25 ) rts ;------------------------------------------------------------ ;divides A by 10 ;returns remainder in A ;returns result in Y ;------------------------------------------------------------ !zone DivideBy10 DivideBy10 sec ldy #$FF .divloop iny sbc #10 bcs .divloop adc #10 rts   step16.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 15

Now we start with a few game essentials: Progressing to the next level.



Not too complicated. We keep a counter of enemies alive (NUMBER_ENEMIES_ALIVE) which is initially set to 0. Since we already have a lookup table IS_TYPE_ENEMY we simply add a check inside AddObject. If the new object is an enemy, increase the counter:
  ;adjust enemy counter ldx PARAM3 lda IS_TYPE_ENEMY,x beq .NoEnemy inc NUMBER_ENEMIES_ALIVE .NoEnemy   The other spot where this comes in is when we kill an enemy. Inside our FireShot routine we add: .EnemyKilled ldy SPRITE_ACTIVE,x lda IS_TYPE_ENEMY,y beq .NoEnemy dec NUMBER_ENEMIES_ALIVE .NoEnemy jsr RemoveObject   For the level change we add a new control routine (GameFlowControl) in the main game loop. Once the enemy count reaches 0 we increase a level done delay so we don't immediately jump onwards. Once reached, disable all sprites, build next level and continue.

For now there's two simple levels with the last looping back to the first.
  ;------------------------------------------------------------ ;the main game loop ;------------------------------------------------------------ GameLoop jsr WaitFrame jsr GameFlowControl jsr DeadControl jsr ObjectControl jsr CheckCollisions jmp GameLoop ;------------------------------------------------------------ ;controls the game flow ;------------------------------------------------------------ !zone GameFlowControl GameFlowControl inc DELAYED_GENERIC_COUNTER lda DELAYED_GENERIC_COUNTER cmp #8 bne .NoTimedActionYet lda #0 sta DELAYED_GENERIC_COUNTER ;level done delay lda NUMBER_ENEMIES_ALIVE bne .NotDoneYet inc LEVEL_DONE_DELAY lda LEVEL_DONE_DELAY cmp #20 beq .GoToNextLevel inc VIC_BORDER_COLOR .NotDoneYet .NoTimedActionYet rts .GoToNextLevel lda #0 sta VIC_SPRITE_ENABLE inc LEVEL_NR jsr BuildScreen jsr CopyLevelToBackBuffer rts
step15.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 99

And of course lots of little bugs were found and fixed

-Live number display was off on a new level.
-Beams would sometimes not be removed on the final boss
-Disable screen output when saving scores (IRQs go nuts if using kernal save routine)
-Cleaned up "extro" text


Have fun!

step99.zip

Previous Step Next Step  

Endurion

Endurion

 

A C64 game - Step 21

Now some more tidbits. A game is not complete without a neat title screen:


For now this is nothing more than a almost empty screen and a separate control loop. The loop simply waits for a button press and then jumps to the game main loop. Once the player lost all lives he is returned to the title loop.

Note that for both cases we also check if the button has been released before allowing to go forward. Nothing's more annyoing than accidental commencing.

Rather unspectacular, the title screen code. First the screen is displayed and then the check button loop entered. Once the button has been pressed the game variables are reset and the code runs into the main game loop. ;------------------------------------------------------------ ;the title screen game loop ;------------------------------------------------------------ !zone TitleScreen TitleScreen ldx #0 stx BUTTON_PRESSED stx BUTTON_RELEASED sta VIC_SPRITE_ENABLE ;clear screen lda #32 ldy #0 jsr ClearScreen ;display title logo lda #<TEXT_TITLE sta ZEROPAGE_POINTER_1 lda #>TEXT_TITLE sta ZEROPAGE_POINTER_1 + 1 lda #0 sta PARAM1 lda #1 sta PARAM2 jsr DisplayText ;display start text lda #<TEXT_FIRE_TO_START sta ZEROPAGE_POINTER_1 lda #>TEXT_FIRE_TO_START sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #23 sta PARAM2 jsr DisplayText .TitleLoop jsr WaitFrame lda #$10 bit $dc00 bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED bne .Restart jmp .TitleLoop .ButtonNotPressed lda #1 sta BUTTON_RELEASED jmp .TitleLoop .Restart ;game start values lda #3 sta PLAYER_LIVES ;setup level jsr StartLevel lda #0 sta LEVEL_NR jsr BuildScreen jsr CopyLevelToBackBuffer ;------------------------------------------------------------ ;the main game loop ;------------------------------------------------------------   Of course the counter part needs to be added to the DeadControl routine. If the player lost his last life, return to the title screen: !zone DeadControl DeadControl lda SPRITE_ACTIVE beq .PlayerIsDead rts .PlayerIsDead lda #$10 bit $dc00 bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED bne .Restart rts .ButtonNotPressed lda #1 sta BUTTON_RELEASED rts .Restart ;if last live return to title lda PLAYER_LIVES bne .RestartLevel jmp TitleScreen .RestartLevel
step21.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 45

In this version nothing much is added to the code. However an external level editor was added (Windows executable) which helps a lot in churning out pretty levels faster.




Most prominent addition are level elements. Before the level was built mostly of simple primitives (line of an character, etc.). With the editor so called Elements are added. These consist of a variable sized character and color block. Elements can be arranged as single object, lines or areas. This helps a lot in reusing bigger level parts and keeping memory usage down.

The elements are stored in several tables. A major lookup table that points to a elements character and color tables, and two lookup tables holding an elements width and height. The editor tries to fold element tables into each other to save memory. For example if there's a big brick sized 4x2 characters, and you have two smaller elements showing the left and right halfs of the brick, the element data is reused.

Note that the element area code is not implemented in this step.

Moral of the story:
As soon as you see you're going ahead with a project having an easy to use editor is quite important. It aids you in faster content creation, faster testing and overall usually prettier results. Nothing crushes productivity better than annoying tools (or manual boring work without tools). ;------------------------------------------------------------ ;draws a level element ;PARAM1 = X ;PARAM2 = Y ;PARAM3 = TYPE ;returns element width in PARAM4 ;returns element height in PARAM5 ;------------------------------------------------------------ !zone DrawLevelElement DrawLevelElement ldy PARAM3 lda SNELEMENT_TABLE_LO,y sta .LoadCode + 1 lda SNELEMENT_TABLE_HI,y sta .LoadCode + 2 lda SNELEMENT_COLOR_TABLE_LO,y sta .LoadCodeColor + 1 lda SNELEMENT_COLOR_TABLE_HI,y sta .LoadCodeColor + 2 lda SNELEMENT_WIDTH_TABLE,y sta PARAM4 lda SNELEMENT_HEIGHT_TABLE,y sta PARAM5 sta PARAM6 ldy PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,y clc adc PARAM1 sta .StoreCode + 1 sta .StoreCodeColor + 1 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,y adc #0 sta .StoreCode + 2 adc #( ( >SCREEN_COLOR ) - ( >SCREEN_CHAR ) ) sta .StoreCodeColor + 2 .NextRow ldx #0 ;display a row .Row .LoadCode lda $8000,x .StoreCode sta $8000,x .LoadCodeColor lda $8000,x .StoreCodeColor sta $8000,x inx cpx PARAM4 bne .Row ;eine zeile nach unten dec PARAM6 beq .ElementDone ;should be faster? lda .LoadCode + 1 clc adc PARAM4 sta .LoadCode + 1 lda .LoadCode + 2 adc #0 sta .LoadCode + 2 lda .LoadCodeColor + 1 clc adc PARAM4 sta .LoadCodeColor + 1 lda .LoadCodeColor + 2 adc #0 sta .LoadCodeColor + 2 lda .StoreCode + 1 clc adc #40 sta .StoreCode + 1 lda .StoreCode + 2 adc #0 sta .StoreCode + 2 lda .StoreCodeColor + 1 clc adc #40 sta .StoreCodeColor + 1 lda .StoreCodeColor + 2 adc #0 sta .StoreCodeColor + 2 jmp .NextRow .ElementDone rts !zone LevelElement LevelElement LevelElementArea ; !byte LD_ELEMENT,0,0,EL_BLUE_BRICK_4x3 ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha jsr DrawLevelElement jmp NextLevelData
The element line primitives are very similar, they just loop over the element draw routine: !zone LevelElementH LevelElementH ; !byte LD_ELEMENT_LINE_H,x,y,width,element ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;x count iny lda (ZEROPAGE_POINTER_1),y sta PARAM7 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha .NextElement jsr DrawLevelElement dec PARAM7 beq .Done lda PARAM1 clc adc PARAM4 sta PARAM1 jmp .NextElement .Done jmp NextLevelData !zone LevelElementV LevelElementV ; !byte LD_ELEMENT_LINE_V,x,y,num,element ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;y count iny lda (ZEROPAGE_POINTER_1),y sta PARAM7 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha .NextElement jsr DrawLevelElement dec PARAM7 beq .Done lda PARAM2 clc adc PARAM5 sta PARAM2 jmp .NextElement .Done jmp NextLevelData
The editor exports the level structure to a separate file, this is then included in the main file via the !source macro. step45.zip Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 25

Time to spruce up the title screen again.


This time we split the title screen display, upper part will be a bitmap logo, lower path text mode.




Some clarification: The graphic chip of the C64, the VIC II, has some neat features. It displays line per line whatever mode currently is set. This means, with skilled modification you can change the mode while a screen is being displayed.

The VIC II aids the programmer with a raster interrupt. You can provide a line number where a interrupt should occur. This avoids having to actively wait. Since we need to show two modes we need to toggle modes two times.

Once the raster is above the inner visible area we activate the bitmap mode. Once the raster hits the end of the bitmap we want to display switch to text mode again. There's a bit more to it if you want exact splits, but the way the image is set up allows us to approach that rather naively.


The bitmap data itself was created by a Lua script from an actual image. Note that a bitmap is compiled of several parts: The actual image data, the color ram part (for 1 of the three possible colors per cell) and the screen data (for the other two possible colors per cell).

Bitmap data cannot be put in memory randomly, there are only a handful of aligned locations where they need to reside. Nice trick: Since the layout of the code is completely fixed you can actually force the compiler to put data at a specific location. This way you avoid to having to copy the bitmap data somewhere else.

Here we store the bitmap data at $2000: ;place the data at a valid bitmap position, this avoids copying the data * = $2000 TITLE_LOGO_BMP_DATA !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ...   One of the color parts can also be fixed in memory:
  * = $2c00 TITLE_LOGO_SCREEN_CHAR !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 !byte 0,0,0,0,0,0,0,0,0,0,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ...   The final part is copied to its required target location on demand:
  TITLE_LOGO_COLORRAM !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ...   During settup up the title screen state we need to copy the color information to the color RAM. Since we only need 64 pixel height for the logo we only copy 320 bytes:
  ldx #0 .FillColor lda TITLE_LOGO_COLORRAM,x sta SCREEN_COLOR,x inx bne .FillColor .FillColor2 lda TITLE_LOGO_COLORRAM + 256,x sta SCREEN_COLOR + 256,x inx cpx #( 320 - 256 ) bne .FillColor2   Now to set up the initial raster interrupt. Note that before doing that we call jsr WaitFrame once so the raster line is at a specific location. You don't want to hit the interrupt on the wrong side.
  ;----------------------------------- ;init IRQ ;----------------------------------- !zone InitTitleIRQ InitTitleIRQ sei lda #$37 ; make sure that IO regs at $dxxx sta $1 ;are visible lda #$7f ;disable cia #1 generating timer irqs sta $dc0d ;which are used by the system to flash cursor, etc lda #$1 ;tell VIC we want him generate raster irqs sta $d01a lda #$10 ;nr of rasterline we want our irq occur at sta $d012 lda #$1b ;MSB of d011 is the MSB of the requested rasterline sta $d011 ;as rastercounter goes from 0-312 ;set irq vector to point to our routine lda #<IrqSetBitmapMode sta $314 lda #>IrqSetBitmapMode sta $315 ;acknowledge any pending cia timer interrupts ;this is just so we are 100% safe lda $dc0d lda $dd0d cli rts   Note the routine IrqSetBitmapMode (and IrqSetTextMode). These routines set their respective mode and setup the raster interrupt for the other part.
  ;----------------------------------- ;IRQ Title - set bitmap mode ;----------------------------------- !zone IrqSetBitmapMode IrqSetBitmapMode ;acknowledge VIC irq lda $d019 sta $d019 ;install top part lda #<IrqSetTextMode sta $314 lda #>IrqSetTextMode sta $315 ;nr of rasterline we want our irq occur at lda #$71 sta $d012 ;bitmap modus an lda #$3b sta $D011 ;set VIC to bank 0 lda $DD00 and #$fc ora #$3 sta $dd00 ;bitmap to lower half, screen char pos at 3 * 1024 ( + 16384) lda #%10111000 sta $D018 JMP $ea31 ;----------------------------------- ;IRQ Title - set text mode ;----------------------------------- !zone IrqSetTextMode IrqSetTextMode ;acknowledge VIC irq lda $d019 sta $d019 ;install scroller irq lda #<IrqSetBitmapMode sta $314 lda #>IrqSetBitmapMode sta $315 ;nr of rasterline we want our irq occur at lda #$10 sta $d012 ;disable bitmap mode lda #$1b sta $D011 ;set VIC to bank 3 lda $DD00 and #$fc sta $dd00 ;bitmap to lower half, screen char pos at 3 * 1024 ( + 16384) lda #%00111100 sta $D018 jmp $ea31   Once the title screen state is left the interrupt needs to be disabled:
  ;----------------------------------- ;release IRQ ;----------------------------------- !zone ReleaseTitleIRQ ReleaseTitleIRQ sei lda #$37 ; make sure that IO regs at $dxxx sta $1 ;are visible lda #$ff ;enable cia #1 generating timer irqs sta $dc0d ;which are used by the system to flash cursor, etc ;no more raster irqs lda #$00 sta $d01a lda #$31 sta $314 lda #$EA sta $315 ;acknowledge any pending cia timer interrupts ;this is just so we are 100% safe lda $dc0d lda $dd0d cli rts
step25.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 22

The title screen looks somewhat empty, doesn't it? Well, let's fill it with highscores! Lazy as I am I'm storing the highscore scores and names in two separate texts which are simply displayed with the common display routine.

After Game Over we check for a new spot. If we manage to enter the list all lower entries are moved down. For now that happens automatically and only affects the scores. Name entry follows up next.



First the newly added text fields, note that '-' is used as line break and '*' as end of text marker.
Another note: If you default fill high scores always have an easily beatable top score. People like to be better than someone else. HIGHSCORE_SCORE !text "00050000-" !text "00040000-" !text "00030000-" !text "00020000-" !text "00010000-" !text "00001000-" !text "00000300-" !text "00000100*" HIGHSCORE_NAME !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL*"   In the title screen layout code we add the display routines:
  ;display high scores ;x,y pos of name lda #6 sta PARAM1 lda #10 sta PARAM2 lda #<HIGHSCORE_NAME sta ZEROPAGE_POINTER_1 lda #>HIGHSCORE_NAME sta ZEROPAGE_POINTER_1 + 1 jsr DisplayText ;x,y pos of score lda #25 sta PARAM1 lda #10 sta PARAM2 lda #<HIGHSCORE_SCORE sta ZEROPAGE_POINTER_1 lda #>HIGHSCORE_SCORE sta ZEROPAGE_POINTER_1 + 1 jsr DisplayText   And last but not least, the actual checking for a highscore. You'll see that the score is not actually kept in a variable. Instead the screen chars at the display are used to compare against the text.
  ;------------------------------------------------------------ ;check if the player got a new highscore entry ;------------------------------------------------------------ !zone CheckForHighscore CheckForHighscore lda #0 sta PARAM1 ldy #0 .CheckScoreEntry ldx #0 sty PARAM2 .CheckNextDigit lda SCREEN_CHAR + ( 23 * 40 + 8 ),x cmp HIGHSCORE_SCORE,y bcc .NotHigher bne .IsHigher ;need to check next digit iny inx cpx #HIGHSCORE_SCORE_SIZE beq .IsHigher jmp .CheckNextDigit .NotHigher inc PARAM1 lda PARAM1 cmp #HIGHSCORE_ENTRY_COUNT beq .NoNewHighscore ;y points somewhere inside the score, recalc next line pos lda PARAM2 clc adc #( HIGHSCORE_SCORE_SIZE + 1 ) tay jmp .CheckScoreEntry .NoNewHighscore jmp TitleScreen .IsHigher ;shift older entries down, add new entry lda #( HIGHSCORE_ENTRY_COUNT - 1 ) sta PARAM2 ;y carries the offset in the score text, position at start of second last entry ldy #( ( HIGHSCORE_SCORE_SIZE + 1 ) * ( HIGHSCORE_ENTRY_COUNT - 2 ) ) .CopyScore lda PARAM2 cmp PARAM1 beq .SetNewScore ;copy score ldx #0 .CopyNextScoreDigit lda HIGHSCORE_SCORE,y sta HIGHSCORE_SCORE + ( HIGHSCORE_SCORE_SIZE + 1 ),y iny inx cpx #HIGHSCORE_SCORE_SIZE bne .CopyNextScoreDigit tya sec sbc #( HIGHSCORE_SCORE_SIZE + HIGHSCORE_SCORE_SIZE + 1 ) tay dec PARAM2 jmp .CopyScore .SetNewScore ;y points at score above the new entry tya clc adc #( HIGHSCORE_SCORE_SIZE + 1 ) tay ldx #0 .SetNextScoreDigit lda SCREEN_CHAR + ( 23 * 40 + 8 ),x sta HIGHSCORE_SCORE,y iny inx cpx #HIGHSCORE_SCORE_SIZE bne .SetNextScoreDigit jmp TitleScreen   step22.zip

Previous Step Next Step

Endurion

Endurion

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!