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

About this blog

Musings of a hobbyist

Entries in this blog

 

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

 

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.


The Bad

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.

I added a simple hack: On loading levels I auto fixed these broken parts.



The Ugly

Windows 7 auto volume adjustment

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?

Endurion

Endurion

 

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  

Endurion

Endurion

 

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

Endurion

Endurion

 

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

 

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

Endurion

Endurion

 

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

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

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

Endurion

Endurion

 

A C64 Game - Step 66

Nothing new code wise again, just the next 10 stages!



Have fun!
  step66.zip
Previous Step Next Step

Endurion

Endurion

 

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.

Some pieces had to be written anew from scratch; for example the fire code for Sam. Sam does not use a shotgun, he uses his demonic forces instead. Sam needs to look at an enemy, and keep fire pressed. Doing that a enemy will get frozen and take damage. However Sam cannot move during that phase. .FireSam ldy PLAYER_JOYSTICK_PORT,x lda JOYSTICK_PORT_II,y and #$10 bne .SamNotFirePushed lda #1 sta PLAYER_FIRE_PRESSED_TIME,x stx PARAM6 jsr SamUseForce beq .NoEnemyHeld ;Sam needs to keep pressed inc PLAYER_SHOT_PAUSE,x lda PLAYER_SHOT_PAUSE,x cmp #40 beq .EnemyHurtBySam ldy SPRITE_HELD dey lda #2 sta VIC_SPRITE_COLOR,y .NoEnemyHeld .EnemyWasHurt ;restore sprite index ldx PARAM6 jmp .NotFirePushed .EnemyHurtBySam lda #0 sta PLAYER_SHOT_PAUSE,x ldx SPRITE_HELD dex lda #0 sta VIC_SPRITE_COLOR,x dec SPRITE_HP,x bne .EnemyWasHurt .EnemyKilledBySam lda #5 jsr IncreaseScore ldx SPRITE_HELD dex jsr KillEnemy ldx PARAM6 lda #0 sta SPRITE_HELD jmp .NotFirePushed .SamNotFirePushed lda #0 sta SPRITE_HELD sta PLAYER_SHOT_PAUSE,x sta PLAYER_FIRE_PRESSED_TIME,x jmp .NotFirePushed step38.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 40

And onwards we go. This time we add spawn spots. These are a different kind of object which spawn a number of one specific enemy type. The level is clear once all enemies are killed and any existing spawn spots are empty.



In case you wonder about the .C64 file inside there: It's a project file for C64Studio, an .NET based IDE I've written for development and debugging alongside with mostly WinVICE.


Spawn spots are handled pretty similar to objects, tables with its data with code to iterate and process them.

Note that for the spawn spot tables we use the !fill macro. It fills memory with n bytes of value x. ;number of possible spawn spots SPAWN_SPOT_COUNT = 8 SPAWN_SPOT_X !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_Y !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_ACTIVE !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_TYPE !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_SPAWN_COUNT !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_DELAY !fill SPAWN_SPOT_COUNT,0 NUMBER_SPAWN_SPOTS_ALIVE !byte 0
In GameFlowControl we add the call to handle the spawn spots, and modify the level-done check: jsr ProcessSpawnSpots ;------------------------ ;slow events 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 lda NUMBER_SPAWN_SPOTS_ALIVE bne .NotDoneYet
Handling a spawn spot is easy. If a spawn spots delay counter reaches zero we check if the number of alive enemies is below 4. If it is, spawn an enemy, if it isn't reset the delay counter. Once the spawn spot object counter reaches zero the active spawn spot is removed.
  ;------------------------------------------------------------ ;handle spawn spots ;------------------------------------------------------------ !zone ProcessSpawnSpots ProcessSpawnSpots ldx #0 .SpawnSpotLoop lda SPAWN_SPOT_ACTIVE,x beq .NextSpawnSpot lda SPAWN_SPOT_DELAY,x beq .TryToSpawn dec SPAWN_SPOT_DELAY,x jmp .NextSpawnSpot .RemoveSpawnSpot lda #0 sta SPAWN_SPOT_ACTIVE,x dec NUMBER_SPAWN_SPOTS_ALIVE .NextSpawnSpot inx cpx #SPAWN_SPOT_COUNT bne .SpawnSpotLoop rts .TryToSpawn lda #128 sta SPAWN_SPOT_DELAY,x lda NUMBER_ENEMIES_ALIVE cmp #3 bpl .DoNotSpawn stx PARAM4 lda SPAWN_SPOT_TYPE,x sta PARAM3 lda SPAWN_SPOT_X,x sta PARAM1 lda SPAWN_SPOT_Y,x sta PARAM2 ;spawn object jsr FindEmptySpriteSlot beq .DoNotSpawn ;x is sprite slot ;PARAM1 is X ;PARAM2 is Y ;PARAM3 is object type jsr SpawnObject ;restore x ldx PARAM4 dec SPAWN_SPOT_SPAWN_COUNT,x beq .RemoveSpawnSpot .DoNotSpawn jmp .NextSpawnSpot
A new level element LD_SPAWN_SPOT is added to add spawn spots to the stage table. The code simply stores the data in the next free spawn spot slot.
  !zone LevelSpawnSpot LevelSpawnSpot ;find free spot ldx #0 .ExamineNextSpot lda SPAWN_SPOT_ACTIVE,x beq .EmptySpotFound inx cpx SPAWN_SPOT_COUNT bne .ExamineNextSpot jmp NextLevelData .EmptySpotFound inc NUMBER_SPAWN_SPOTS_ALIVE lda #1 sta SPAWN_SPOT_ACTIVE,x ;X pos iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_X,x ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_Y,x ;type iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_TYPE,x ;count iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_SPAWN_COUNT,x tya pha jmp NextLevelData step40.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 69

And yet again, the next bunch of stages, this time a deserty area.



Have fun! step69.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 46

This time we'll add a chapter intro. The game is supposed to be separated in themed chapters with an intro for each. There's nothing much to it, we'll display a short text while the boys drive to their new target.



For now we simply show the first page after starting the game: lda #0 jsr ShowStory
Most of the code is spent to actually setup the impala and driver sprites. The text itself is displayed by our trusty DisplayText routine. Followed by the obligatory wait for button press and release. ;------------------------------------------------------------ ;story pages ;------------------------------------------------------------ !zone ShowStory ShowStory ;clear screen lda #32 ldy #1 jsr ClearScreen lda # sta ZEROPAGE_POINTER_1 lda #>TEXT_STORY_1 sta ZEROPAGE_POINTER_1 + 1 lda #1 sta PARAM1 lda #1 sta PARAM2 jsr DisplayText lda #41 sta PARAM1 lda #20 sta PARAM2 lda #TYPE_IMPALA_1 sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #44 sta PARAM1 lda #TYPE_IMPALA_DRIVER sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #44 sta PARAM1 lda #TYPE_IMPALA_2 sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #47 sta PARAM1 lda #TYPE_IMPALA_3 sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #48 sta PARAM1 lda #TYPE_IMPALA_DEBRIS sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #12 sta VIC_SPRITE_MULTICOLOR_1 lda #11 sta VIC_SPRITE_MULTICOLOR_2 lda #0 sta BUTTON_RELEASED .StoryLoop jsr WaitFrame jsr ObjectControl ;ldx #0 ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft lda #$10 bit JOYSTICK_PORT_II bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED beq .StoryLoop lda #0 sta VIC_SPRITE_ENABLE lda #11 sta VIC_SPRITE_MULTICOLOR_1 lda #1 sta VIC_SPRITE_MULTICOLOR_2 rts .ButtonNotPressed lda #1 sta BUTTON_RELEASED jmp .StoryLoop

The impala objects are simple game objects with the same behaviour. Move to the center, wait for a while and then move off the left side. ;------------------------------------------------------------ ;drive left/pause/drive off left ;------------------------------------------------------------ !zone BehaviourImpala BehaviourImpalaDebris inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x and #$04 lsr lsr clc adc #SPRITE_DEBRIS_1 sta SPRITE_POINTER_BASE,x BehaviourImpala lda SPRITE_STATE,x beq .DriveFirstHalf cmp #1 beq .HandlePause ;drive off jsr MoveSpriteLeft lda SPRITE_POS_X,x beq .DriveDone rts .DriveDone jsr RemoveObject rts .DriveFirstHalf jsr MoveSpriteLeft inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #200 beq .NextState rts .NextState inc SPRITE_STATE,x lda #0 sta SPRITE_MOVE_POS,x rts .HandlePause inc SPRITE_MOVE_POS,x beq .NextState rts
The text is stored as usual. Nice to see, a - acts as CR, * as end of text. TEXT_STORY_1 !text "A LOCAL NEWSPAPER MENTIONS SEVERAL-" !text "MISSING PEOPLE",59," THIS SEEMS TO BE A-" !text "RECURRING PATTERN EVERY 44 YEARS",59,"-" !text "WE SHOULD INVESTIGATE THE TOWN-" !text "CEMETARY",59,"*"
step46.zip

Previous Step Next Step

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 76

And yet another 10 stages. A few animation bugs show up with the water, in general there seems to be another fixing step coming up.



Have fun! step76.zip

Previous Step Next Step  

Endurion

Endurion

 

A C64 Game - Step 53

Here comes our first boss. Nothing too difficult, but different. Let's see what this works out to



The boss moves quite similar to the ghost however he's got a special attack. If nothing else happens the boss is homing in on you. If you shoot him two times he goes into attack mode (and you better step back).

For performance reason the beams are made of characters, a horizontal and vertical line at the boss position. ;------------------------------------------------------------ ;boss ;------------------------------------------------------------ !zone BehaviourBoss BehaviourBoss BOSS_MOVE_SPEED = 1 lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x ldy SPRITE_HITBACK,x lda BOSS_FLASH_TABLE,y sta VIC_SPRITE_COLOR,x cpy #0 bne .NoHitBack ;make vulnerable again lda SPRITE_STATE,x cmp #128 bne .NoHitBack lda #0 sta SPRITE_STATE,x .NoHitBack lda DELAYED_GENERIC_COUNTER and #$03 bne .NoAnimUpdate lda SPRITE_POINTER_BASE,x eor #$01 sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_STATE,x and #$7f bne .NotFollowPlayer jmp .FollowPlayer .NotFollowPlayer cmp #1 beq .AttackMode rts .AttackMode ;Attack modes (more modes?) inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #4 beq .NextAttackStep rts .NextAttackStep lda #0 sta SPRITE_MOVE_POS,x inc SPRITE_MODE_POS,x lda SPRITE_MODE_POS,x cmp #11 bcc .BeamNotDangerous cmp #29 bcs .BeamNotDangerous ;does player hit beam? ldy #0 jsr CheckIsPlayerCollidingWithBeam ldy #1 jsr CheckIsPlayerCollidingWithBeam .BeamNotDangerous lda SPRITE_MODE_POS,x cmp #11 beq .BeamStep1 cmp #12 beq .BeamStep2 cmp #13 beq .BeamStep3 cmp #16 beq .BeamStep4 cmp #17 beq .BeamStep3 cmp #18 beq .BeamStep4 cmp #19 beq .BeamStep3 cmp #20 beq .BeamStep4 cmp #21 beq .BeamStep3 cmp #22 beq .BeamStep4 cmp #23 beq .BeamStep3 cmp #24 beq .BeamStep4 cmp #25 beq .BeamStep3 cmp #26 beq .BeamStep4 cmp #27 beq .BeamStep3 cmp #28 beq .BeamStep4 cmp #29 beq .BeamStep3 cmp #30 beq .BeamEnd rts .BeamStep1 ;beam lda #BEAM_TYPE_DARK jsr .DrawBeam rts .BeamStep2 ;beam lda #BEAM_TYPE_MEDIUM jsr .DrawBeam rts .BeamStep3 ;beam lda #BEAM_TYPE_LIGHT jsr .DrawBeam rts .BeamStep4 ;beam lda #BEAM_TYPE_LIGHT2 jsr .DrawBeam rts .BeamEnd jsr .RestoreBeam lda #0 sta SPRITE_STATE,x rts .DrawBeam tay lda BEAM_CHAR_H,y sta PARAM1 lda BEAM_CHAR_V,y sta PARAM2 lda BEAM_COLOR,y sta PARAM3 ldy SPRITE_CHAR_POS_Y,x 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 stx PARAM6 ldy #1 .HLoop lda PARAM1 sta (ZEROPAGE_POINTER_1),y lda PARAM3 sta (ZEROPAGE_POINTER_2),y iny cpy #39 bne .HLoop ;vertical beam ldy SPRITE_CHAR_POS_X,x ldx #1 .NextLine lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 lda PARAM2 sta (ZEROPAGE_POINTER_1),y lda PARAM3 sta (ZEROPAGE_POINTER_2),y inx cpx #22 bne .NextLine ldx PARAM6 rts .RestoreBeam ldy SPRITE_CHAR_POS_Y,x 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 sec sbc #( ( SCREEN_CHAR - SCREEN_BACK_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_3 + 1 sec sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 ) sta ZEROPAGE_POINTER_4 + 1 stx PARAM6 ldy #1 - lda (ZEROPAGE_POINTER_2),y sta (ZEROPAGE_POINTER_1),y lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_3),y iny cpy #39 bne - ;vertical beam ldy SPRITE_CHAR_POS_X,x ldx #1 .NextLineR lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_BACK_CHAR - SCREEN_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_3 + 1 sec sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 ) sta ZEROPAGE_POINTER_4 + 1 lda (ZEROPAGE_POINTER_2),y sta (ZEROPAGE_POINTER_1),y lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_3),y inx cpx #22 bne .NextLineR ldx PARAM6 rts .FollowPlayer inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #10 beq .DoCheckMove jmp .DoGhostMove .DoCheckMove lda #0 sta SPRITE_ANIM_DELAY,x txa and #$01 tay lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_DEAN beq .FoundPlayer cmp #TYPE_PLAYER_SAM beq .FoundPlayer ;check other player tya eor #1 tay lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_DEAN beq .FoundPlayer cmp #TYPE_PLAYER_SAM beq .FoundPlayer ;no player to hunt rts .FoundPlayer ;player index in y lda SPRITE_CHAR_POS_X,y cmp SPRITE_CHAR_POS_X,x bpl .MoveRight ;move left lda SPRITE_DIRECTION,x bne .AlreadyLookingLeft lda SPRITE_MOVE_POS,x beq .TurnLNow dec SPRITE_MOVE_POS,x bne .CheckYNow .TurnLNow ;turning now lda #1 sta SPRITE_DIRECTION,x lda #SPRITE_BOSS_L_1 sta SPRITE_POINTER_BASE,x jmp .CheckYNow .AlreadyLookingLeft lda SPRITE_MOVE_POS,x cmp #BOSS_MOVE_SPEED beq .CheckYNow inc SPRITE_MOVE_POS,x jmp .CheckYNow .MoveRight lda SPRITE_DIRECTION,x beq .AlreadyLookingRight lda SPRITE_MOVE_POS,x beq .TurnRNow dec SPRITE_MOVE_POS,x bne .CheckYNow ;turning now .TurnRNow lda #0 sta SPRITE_DIRECTION,x lda #SPRITE_BOSS_R_1 sta SPRITE_POINTER_BASE,x jmp .CheckYNow .AlreadyLookingRight lda SPRITE_MOVE_POS,x cmp #BOSS_MOVE_SPEED beq .CheckYNow inc SPRITE_MOVE_POS,x jmp .CheckYNow .CheckYNow ;player index in y lda SPRITE_CHAR_POS_Y,y cmp SPRITE_CHAR_POS_Y,x bpl .MoveDown ;move left lda SPRITE_DIRECTION_Y,x bne .AlreadyLookingUp lda SPRITE_MOVE_POS_Y,x beq .TurnUNow dec SPRITE_MOVE_POS_Y,x bne .DoGhostMove .TurnUNow ;turning now lda #1 sta SPRITE_DIRECTION_Y,x jmp .DoGhostMove .AlreadyLookingUp lda SPRITE_MOVE_POS_Y,x cmp #BOSS_MOVE_SPEED beq .DoGhostMove inc SPRITE_MOVE_POS_Y,x jmp .DoGhostMove .MoveDown lda SPRITE_DIRECTION_Y,x beq .AlreadyLookingDown lda SPRITE_MOVE_POS_Y,x beq .TurnDNow dec SPRITE_MOVE_POS_Y,x bne .DoGhostMove ;turning now .TurnDNow lda #0 sta SPRITE_DIRECTION_Y,x jmp .DoGhostMove .AlreadyLookingDown lda SPRITE_MOVE_POS_Y,x cmp #BOSS_MOVE_SPEED beq .DoGhostMove inc SPRITE_MOVE_POS_Y,x jmp .DoGhostMove .DoGhostMove ;move X times ldy SPRITE_MOVE_POS,x sty PARAM4 beq .DoY lda SPRITE_DIRECTION,x beq .DoRight .MoveLoopL jsr ObjectMoveLeftBlocking dec PARAM4 bne .MoveLoopL jmp .DoY .DoRight .MoveLoopR jsr ObjectMoveRightBlocking dec PARAM4 bne .MoveLoopR .DoY ;move X times ldy SPRITE_MOVE_POS_Y,x sty PARAM4 beq .MoveDone lda SPRITE_DIRECTION_Y,x beq .DoDown .MoveLoopU jsr ObjectMoveUpBlocking dec PARAM4 bne .MoveLoopU jmp .MoveDone .DoDown .MoveLoopD jsr ObjectMoveDownBlockingNoPlatform dec PARAM4 bne .MoveLoopD .MoveDone rts
Checking the player for collision with the beam is heavily simplified due to the beam being horizontal/vertical. It's a cheap comparison of position values: ;------------------------------------------------------------ ;check player vs. beam ; beam boss index in x ; player index in y ;------------------------------------------------------------ !zone CheckIsPlayerCollidingWithBeam CheckIsPlayerCollidingWithBeam lda SPRITE_ACTIVE,y bne .PlayerIsActive .PlayerNotActive rts .PlayerIsActive lda SPRITE_STATE,y cmp #128 bcs .PlayerNotActive ;compare char positions in x lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X,y beq .PlayerHit clc adc #1 cmp SPRITE_CHAR_POS_X,y beq .PlayerHit sec sbc #2 cmp SPRITE_CHAR_POS_X,y beq .PlayerHit ;compare char positions in y lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y,y beq .PlayerHit clc adc #1 cmp SPRITE_CHAR_POS_Y,y beq .PlayerHit sec sbc #2 cmp SPRITE_CHAR_POS_Y,y beq .PlayerHit ;not hit rts .PlayerHit ;player killed lda #129 sta SPRITE_STATE,y lda #SPRITE_PLAYER_DEAD sta SPRITE_POINTER_BASE,y lda #0 sta SPRITE_MOVE_POS,y lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_SAM bne .PlayerWasDean ;reset Sam specific variables lda #0 sta SPRITE_HELD .PlayerWasDean rts
The boss only enters attack mode for every second hit, therefore he gets a special treatment in his hit function: ;------------------------------------------------------------ ;hit behaviour for boss ;------------------------------------------------------------ !zone HitBehaviourBoss HitBehaviourBoss lda #8 sta SPRITE_HITBACK,x ;make invincible for a short while lda SPRITE_STATE,x ora #$80 sta SPRITE_STATE,x ;boss switches tactic lda SPRITE_HP,x and #$01 beq .SwitchToAttack rts .SwitchToAttack lda #129 sta SPRITE_STATE,x lda #0 sta SPRITE_MODE_POS,x sta SPRITE_MOVE_POS,x sta SPRITE_MOVE_POS_Y,x rts
The less interesting parts are known, add new values for the boss object plus the color/character tables for the beam. step53.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 36

I called this one "Satisfaction Refinement". Enemies when shot are hit back for a few steps, it's a shotgun after all, dammit!
On their final blow enemies now burst in smoke.



For the smoke/explosion we'll add a new object type: SPRITE_EXPLOSION_1 = SPRITE_BASE + 47 SPRITE_EXPLOSION_2 = SPRITE_BASE + 48 SPRITE_EXPLOSION_3 = SPRITE_BASE + 49 TYPE_EXPLOSION = 9
Now, once the enemy loses its last hit point, we don't simply remove the object but rather replace it with an explosion. This time we don't call RemoveObject/SpawnObject but rather put the required values in place right there. lda #TYPE_EXPLOSION sta SPRITE_ACTIVE,x lda #15 sta VIC_SPRITE_COLOR,x lda BIT_TABLE,x ora VIC_SPRITE_MULTICOLOR sta VIC_SPRITE_MULTICOLOR lda #SPRITE_EXPLOSION_1 sta SPRITE_POINTER_BASE,x lda #0 sta SPRITE_ANIM_DELAY,x sta SPRITE_ANIM_POS,x
The explosion behaviour itself is quite simple. Move upwards slowly and animate. Once the end of the animation is reached remove itself. ;------------------------------------------------------------ ;explosion ;------------------------------------------------------------ !zone BehaviourExplosion BehaviourExplosion jsr MoveSpriteUp inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 beq .UpdateAnimation rts .UpdateAnimation lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x cmp #4 beq .ExplosionDone clc adc #SPRITE_EXPLOSION_1 sta SPRITE_POINTER_BASE,x rts .ExplosionDone jsr RemoveObject rts For the hit back of the enemies we'll add this code at the beginning of all participating enemies. The SPRITE_HITBACK counter is checked, if it's set, it's decreased and the object is moved in the hit back direction. lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack
The current HitBack methods are enhanced with the following snippet. Set the hit back counter (to 8) and use the direction from the player as the hit back dir. Since the player shots are instant this correctly holds the direction away from the player. lda #8 sta SPRITE_HITBACK,x ;hitback dir determined from player dir (equal shot dir) lda SPRITE_DIRECTION sta SPRITE_HITBACK_DIRECTION,x   step36.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 64

Bug fixing!

Note how the sprites would flicker about before/after Get Ready?
Fixed. Also, Sam got borked again, esp. during a boss fight. Now that's also fixed.



First part is pretty easy, we disable the screen during build up of the level. Fortunately the VIC allows exactly this. So we add

to disable: lda #$0b sta VIC_CONTROL_MODE
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

Endurion

Endurion

 

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

Endurion

Endurion

 

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

Endurion

Endurion

 

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

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!