• entries
104
102
• views
260396

Musings of a hobbyist

## 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

## The Week of Awesome II - Post Mortem

Post Mortem to "Building Blocks"

So I entered the Week of Awesome II competition last week. I simply love this tight deadline competitions. And I couldn't help it again.
The theme came up as "The Toys are alive". Yay.

Usually I don't jump into coding right away to think about the game to be. And as usual I always go for the nearest idea. It's very rare that I try to outwit the theme and do something really creative. And so it was decided, a simple Lemmings style game, with tin soldiers walking about building blocks.

The Good

Level design

The common start is to think of how to keep the level in memory. The base of the level is tile based. I did think of bigger blocks right away, and this worked out fine after a few tries. This led me to the next point pretty fast, the

Level editor

Esp. for these competitions, when I go level based, I need a way to churn out levels very fast. Since this competition lasted for a week I went out of my way to make a decent editor. It was a bit more work in the beginning, but helped me tremendously during the last two days to play with level design and add some more stages. I wouldn't have managed 10 stages without it.

Base Library

For Ludum Dare we have to provide the full source. This makes me hesitate to actually use my home grown engine and I work with a very simple cut down framework. For the GameDev compo I can use my full framework and it does help quite a lot. I've got game assets, GUI and game states from the start, and especially, I'm comfortable with it.

Generated Music

Previous to the last Ludum Dare I was on the search for music generators once more. I encountered cgMusic, which made some really nice songs. It's a bit piano centric, and I feel that some song parts to repeat, but it was good enough for me. Adding a midi to ogg converter I could generate songs in a few minutes.

Early Preview

As suggested I put up an early version. And it was a good idea. I had some very valuable feedback, as there were a few things I didn't even think of. I think I did add all suggestions.

Collision system

The sloped tiles were made out as a first issue to tackle. For some reason I thought using polygons would work out nice. Since I have a math library with polygon collision code I wrote polygon creators for the different blocks. Collision with a unit's bounds rectangle (=polygon) worked very nicely.
However problems appeared when I actually implemented the movement system. I had my units move in pixel units. Obviously mixing pixel based movement with "exact" mathematics (think of the slope of a diagonal block) does not work out. Units fell one pixel, ended up inside the block polygon and were stuck.

I ended up adding a simple hack: return the pixel based height of a tile depending on the x position in the tile. Ugly, but worked.

Ugly bug

When I had the first preview out someone mentioned that the game crashes on exit. I despise things like that (Gamemaker games, I'm looking at you). In the end the real bug was a bug in my level block handling. I managed to access out of bounds memory and as usual this crashed at a totally unrelated position. I had the luck that some code changes led to the bug appearing right away after the wrong access. It took a few seconds to fix, but I noticed that due to that bug some stages had broken data saved.

The Ugly

Windows 7 does some automatic volume adjusting. If there's only very low volume music, it turns it up. But if the music suddenly gets louder it gets adjusted down. I do understand the reasoning behind this feature, but it doesn't lend itself to gaming too well. I wish there was a way for my game to tell Windows to not mess with my volume.

This feature is really really annoying. I'm not entirely sure how to work around it beside starting to play a loud sound effect at the start.

TL;DR

As final thought I'm pretty happy how the game turned out. It's not my best, but also not my worst. I wish I had more stages, as the first 5 are simple tutorials. At least the last two stages are a bit of a puzzle though.

Soooo, when does the next compo start?

## A C64 Game - Step 95

Now a little update that adds a change that was long overdue: Zombies do not wake up all of a sudden, but peek out of the ground before. Now players should be able to escape if they're keeping an eye out.

We change the .WakeUp part of BehaviourZombie to show up, look left/right a few times and only then rise, like good zombies do: ;only animate head to warn player inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #20 beq .ReallyWakeUp and #$07 bne ++ ;show head lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x lda #SPRITE_ZOMBIE_COLLAPSE_R_2 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x ++ rts .ReallyWakeUp Also, having the spawn animation playing but then "appearing" underground is awkward the start state of zombies is now fully alive. Which is simply a change in the TYPE_START_STATE table. That was simple now step95.zip Previous Step Next Step ## A C64 Game - Step 68 Added a first version of Sams dark force, making it visible. It's not looking quite what I wanted it, but it is getting there. To make things faster for removal of the beam we calc the required length and direction of the beam: ;redraw black beam ldx PARAM6 ldy SPRITE_CHAR_POS_Y,x dey sty SAM_FORCE_START_Y 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 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 ldy SPRITE_CHAR_POS_X,x lda SPRITE_DIRECTION,x sta PARAM1 lda #0 sta SAM_FORCE_LENGTH ldx SPRITE_HELD dex - lda #253 sta (ZEROPAGE_POINTER_1),y lda #6 sta (ZEROPAGE_POINTER_2),y inc SAM_FORCE_LENGTH lda PARAM1 bne ++ iny jmp + ++ dey + tya cmp SPRITE_CHAR_POS_X,x bne - ;store for later removal ldx PARAM6 lda PARAM1 bne + ;going right lda SPRITE_CHAR_POS_X,x sta SAM_FORCE_START_X jmp ++ + ;going left sty SAM_FORCE_START_X inc SAM_FORCE_START_X ++ ldy SPRITE_HELD dey The beam needs to be removed once Sam stops pressing the fire button or he or his enemy dies. Aren't we glad that we already have a back buffer we can use to restore the background properly? ;restore play field from force beam !zone RemoveForceBeam RemoveForceBeam ldy SAM_FORCE_START_Y 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 ldy SAM_FORCE_START_X - lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y iny dec SAM_FORCE_LENGTH bne - rts So when Sam dies we modify existing code to: lda SPRITE_HELD beq + jsr RemoveForceBeam lda #0 sta SPRITE_HELD + Voila! Carnage made more visible step68.zip Previous Step Next Step ## 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 ## A C64 Game - Step 48 This update doesn't really add anything mentionable code wise, however there's a full chapter of 10 stages. Have fun! step48.zip Previous Step Next Step ## A C64 Game - Step 52 To make spawning enemies a bit more player friendly spawns are now shown before the enemy actually appears. The way to implement this is quite simple. We've got a neat object system running already, so we make the spawn animation just another type. Once the spawn life time is up the object is replaced in spot with the proper object type. During a spawn point process we store the target type in SPRITE_ANNOYED (since spawns do not get annoyed): ;store spawn type in SPRITE_ANNOYED ldx PARAM7 lda PARAM5 sta SPRITE_ANNOYED,x The spawns behaviour is straight forward, animate, count life time down and finally spawn the final object: ;------------------------------------------------------------ ;Spawn ;------------------------------------------------------------ !zone BehaviourSpawn BehaviourSpawn inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 beq .UpdateAnimation rts .UpdateAnimation lda #0 sta SPRITE_ANIM_DELAY,x lda SPRITE_POINTER_BASE,x eor #$01 sta SPRITE_POINTER_BASE,x inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #20 beq .SpawnNow rts .SpawnNow lda SPRITE_ANNOYED,x sta PARAM3 lda SPRITE_CHAR_POS_X,x sta PARAM1 lda SPRITE_CHAR_POS_Y,x sta PARAM2 stx PARAM7 lda #1 jsr SpawnObject ldx PARAM7 rts
The rest is too simple to show it here line by line, add new constants for the sprite, add the entry to the behaviour and hurt tables, add entries to the type start tables (color, sprite, etc.) and we're done.

And now the player is not killed by suddenly appearing enemies. step52.zip
Previous Step Next Step

## 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

## A C64 Game - Step 71

Now a little polish for Sams dark powers, they were a tad too dark to see

Realistic colors are one thing, but if it hampers the gameplay they have to go. So dark powers are now white.
And animated.

Core routine is this, it redraws every char of the beam with one of four random characters: ;redraw force beam (randomly) !zone RedrawForceBeam RedrawForceBeam ldy SAM_FORCE_START_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 SAM_FORCE_LENGTH sta PARAM1 ldy SAM_FORCE_START_X - jsr GenerateRandomNumber and #$03 clc adc #252 sta (ZEROPAGE_POINTER_1),y iny dec PARAM1 bne - rts Most of the other changes include a call to the routine above, some are fixes for crashes that were introduces. For one, when removing the beam item images could be clipped, a simple call to jsr RedrawItems fixes that. And a little change to gameplay, if Sam moves (falls/jumps) while holding an enemy and moves too far, he loses grip on the enemy. This is done via a little comparison between the beam start Y pos and the players pos. Too far, and the enemy is released. ;release beam when moving lda SAM_FORCE_START_Y clc adc #1 cmp SPRITE_CHAR_POS_Y,x bne .SamNotFirePushed ;Sam needs to keep pressed jsr RedrawForceBeam Have fun! step71.zip Previous Step Next Step ## 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 ## A C64 Game - Step 83 And here's a new extra for Sam. It works similar to the super bullet. For every demon blood picked Sam can destroy an enemy with one touch. The code is quite similar to the super bullet. Add a new counter variable (DEMON_BLOOD), a new item image, make it possible for the item to spawn as well. At the item pickup we add this. If the player is Sam (means x = 1) increase the counter. .EffectDemonBlood cpx #1 bne .DeanDoesNotUseForce inc DEMON_BLOOD jmp .RemoveItem At the hurt enemy part we add this little snippet. If we have a DEMON_BLOOD, decrease the count and kill the enemy right away. lda DEMON_BLOOD beq + dec DEMON_BLOOD jmp .EnemyKilled + dec SPRITE_HP,x bne .EnemyWasHurt jmp .EnemyKilled Have fun! step83.zip Previous Step Next Step ## A C64 Game - Step 66 Nothing new code wise again, just the next 10 stages! Have fun! step66.zip Previous Step Next Step ## A C64 Game - Step 38 Now this is one huge step that took me a week to implement: A second player. Can't have Supernatural with only Dean, now there's Sam as well. In the title screen, press up to toggle game modes (single Dean, single Sam, coop). Note that Sam does not use a shot gun, he uses his dark powers. We start by adding the game modes: ;game mode types GT_SINGLE_PLAYER_DEAN = 0 GT_SINGLE_PLAYER_SAM = 1 GT_COOP = 2 Of course the current mode needs to be displayed in the main menu. We use GAME_MODE as index into a text mode description table. ldx GAME_MODE lda TEXT_GAME_MODE_LO,x sta ZEROPAGE_POINTER_1 lda TEXT_GAME_MODE_HI,x sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #21 sta PARAM2 jsr DisplayText ...and let the player toggle through the game modes with a joystick up movement: lda #$01 bit JOYSTICK_PORT_II bne .NotUpPressed lda UP_RELEASED beq .UpPressed inc GAME_MODE lda GAME_MODE cmp #3 bne .NoGameModeWrap lda #0 sta GAME_MODE .NoGameModeWrap ;redisplay game mode ldx GAME_MODE lda TEXT_GAME_MODE_LO,x sta ZEROPAGE_POINTER_1 lda TEXT_GAME_MODE_HI,x sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #21 sta PARAM2 jsr DisplayText lda #0 jmp .UpPressed .NotUpPressed lda #1 .UpPressed sta UP_RELEASED
The game mode incurs a few changes in the score display: ;score display according to game mode lda GAME_MODE cmp #GT_SINGLE_PLAYER_DEAN beq .DeanOnly cmp #GT_SINGLE_PLAYER_SAM beq .SamOnly lda #<TEXT_DISPLAY_DEAN_AND_SAM sta ZEROPAGE_POINTER_1 lda #>TEXT_DISPLAY_DEAN_AND_SAM sta ZEROPAGE_POINTER_1 + 1 jmp .DisplayDisplay .DeanOnly lda #0 sta PLAYER_LIVES + 1 lda #<TEXT_DISPLAY_DEAN_ONLY sta ZEROPAGE_POINTER_1 lda #>TEXT_DISPLAY_DEAN_ONLY sta ZEROPAGE_POINTER_1 + 1 jmp .DisplayDisplay .SamOnly lda #0 sta PLAYER_LIVES lda #<TEXT_DISPLAY_SAM_ONLY sta ZEROPAGE_POINTER_1 lda #>TEXT_DISPLAY_SAM_ONLY sta ZEROPAGE_POINTER_1 + 1 .DisplayDisplay

..and the joystick port a player uses. This snippet makes sure that for single player modes the used joystick port is port 2.
;settings per game mode ;default ports lda #0 sta PLAYER_JOYSTICK_PORT lda #1 sta PLAYER_JOYSTICK_PORT + 1 lda GAME_MODE cmp #GT_SINGLE_PLAYER_SAM bne .NoPortChange lda #0 sta PLAYER_JOYSTICK_PORT + 1 .NoPortChange
A lot of changes from Dean to Same are quite simple to implement. Reuse Dean's code, but add an index to the sprite tables. Since Dean is always in slot 0, the ",x" was omitted to speed code up. Now we just put ,x for every sprite table access (Sam is always in slot 1), and the basic code just works.

to enable: lda #$1b sta VIC_CONTROL_MODE We already saved all sprite information, but we only restored the first two sprites (the players) and rely on the usual object initialisation for the other objects. Unfortunately some levels start out with more. So we modify the RemoveGetReady method to restore all sprites: ;turn off sprite expansion bits ldx #0 stx VIC_SPRITE_EXPAND_X stx VIC_SPRITE_EXPAND_Y ;restore all sprite positions and colors - txa asl tay lda SPRITE_POS_X,x sta VIC_SPRITE_X_POS,y lda SPRITE_POS_Y,x sta VIC_SPRITE_Y_POS,y lda SPRITE_COLOR,x sta VIC_SPRITE_COLOR,x ldy SPRITE_ACTIVE,x cpy #0 bne + -- inx cpx #8 bne - jmp GameLoop + lda TYPE_START_SPRITE,y sta SPRITE_POINTER_BASE,x jmp -- Things can be so easy to fix! step64.zip Previous Step Next Step ## A C64 Game - Step 55 Yet another step without much details. The next 10 stages await you. If you want to check them out, don't forget, that you can press '1' to advance to the next stage. Since this step is somewhat short, find the level editor I'm using attached as well. Open the file "Supernatural.elementeditorproject". The editor requires the charset and spriteset files in the same folder as the editor project. The editor is a quick hack job and therefore a bit buggy. Save often. It worked good enough for Soulless though If you add new stages and want to test them, go to the export tab, chose the proper location for the export file (level_data.asm) and press "Export". A simple recompile with C64Studio will incorporate the new stages. Careful, do not have elements going outside the screen boundaries! step55.zip Previous Step Next Step ## A C64 Game - Step 50 In this step the powerups for range increase and decrease reload delay are made permanent. They won't fade with time. You can also collect up to five extras to reach the maximum. And the powerup stays even when you get killed. (Don't you hate it when you die in Bubble Bobble and you're slow again). Previously we had a flag that stored the time left of a faster reload. The max times are now kept in a table, and a faster reload step value is stored instead. The speed table RELOAD_SPEED_MAX and other counters for this update: PLAYER_RELOAD_SPEED !byte 0 RELOAD_SPEED !byte 1,1,1,1,1 RELOAD_SPEED_MAX !byte 40,35,30,25,20 Initialising on game restart: lda #0 sta PLAYER_RELOAD_SPEED During standing still the reload speed value is now used to count down the time. ldy PLAYER_RELOAD_SPEED lda PLAYER_STAND_STILL_TIME clc adc RELOAD_SPEED,y cmp RELOAD_SPEED_MAX,y bcs .ReloadTimeDone sta PLAYER_STAND_STILL_TIME jmp .HandleFire .ReloadTimeDone lda #0 sta PLAYER_STAND_STILL_TIME During pickup of the increase reload speed the counter needs to be updated: lda PLAYER_RELOAD_SPEED cmp #4 beq .SpeedHighestAlready inc PLAYER_RELOAD_SPEED .SpeedHighestAlready Making the force range increase permanent is even easier, we simply remove all instances where it was reset to the start value on respawning and starting the next level. step50.zip Previous Step Next Step ## A C64 Game - Step 37 Currently the player is simply vanishing when getting killed. Kinda looks odd, doesn't it? We add a nice new animation and then send the player back in. First we remove the previous player kill code (which removed the sprite and displayed a GET READY message) and replace it with this. Note that we keep the sprite object but set the state to invincible (>= 128). .PlayerCollidedWithEnemy ;player killed ldx PARAM6 lda #129 sta SPRITE_STATE,x lda #SPRITE_PLAYER_DEAD sta SPRITE_POINTER_BASE,x lda #0 sta SPRITE_MOVE_POS,x On top of the PlayerControl routine we add this part. Note that the routine is jumped into PlayerControl, but .PlayerIsDying is above. The reason for this is the limited range of branch mnemonics. They only allow for -128/127 byte jumps. Just by rearranging the code the branch can be kept. A different solution would be to replace the branch by a reverse branch with a followup jmp. .PlayerIsDying inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #64 beq .PlayerRespawn and #$03 bne .NoUpMove jsr MoveSpriteUp .NoUpMove rts .PlayerRespawn dec PLAYER_LIVES jsr DisplayLiveNumber ;game over? lda PLAYER_LIVES bne .RestartPlayer jmp CheckForHighscore .RestartPlayer lda SPRITE_ACTIVE ;refill shells ldy #0 .RefillShellImage lda #2 sta SCREEN_COLOR + 23 * 40 + 19,y lda #7 sta SCREEN_COLOR + 24 * 40 + 19,y iny cpy PLAYER_SHELLS_MAX bne .RefillShellImage lda PLAYER_SHELLS_MAX sta PLAYER_SHELLS ;respawn at correct position lda PLAYER_START_POS_X sta PARAM1 lda PLAYER_START_POS_Y sta PARAM2 ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ;enable sprite lda BIT_TABLE ora VIC_SPRITE_ENABLE sta VIC_SPRITE_ENABLE ;initialise enemy values lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE lda #0 sta PLAYER_FAST_RELOAD sta PLAYER_INVINCIBLE sta SPRITE_STATE ;look right per default lda #0 sta SPRITE_DIRECTION lda #0 sta SPRITE_JUMP_POS sta SPRITE_FALLING rts PlayerControl lda SPRITE_STATE,x cmp #129 bne .NotDying jmp .PlayerIsDying .NotDying
Now that the whole dying sequence is handled by PlayerControl directly the full routine DeadControl can be removed. This change also helps a lot in the next step step37.zip
Previous Step Next Step