Nice, thanks for sharing! I've never programmed the Commodore systems so it's interesting to see how it's done.
Some additional comments on general 6502 programming style, based only on looking at game.asm. These are maybe nit-picky optimisations, and everyone has their own opinion, but personally I think these are good things to bear in mind and do habitually, as it costs little in mental effort but produces more optimal - and to a 6502 expert, more immediately understandable - code. I also don't always remember to do these things up front, but when I don't, I often find myself regretting it later on.
It's worth reading Garth Wilson's advice here:
https://wilsonminesco.com/6502primer/PgmTips.htmlCode:
98 lda collision_flg
99 and #$01
100 cmp #$01
101 beq collide
Generally you don't need the "cmp" on line 100 - after the "and", the zero flag will be set if the result was 0. So you could remove the "cmp" and use "bne" instead of "beq" on line 101.
Depending what else it's used for, you could also consider "lsr collision_flg" on line 98 - then the bit you're interested in goes into the carry, but it modifies the variable as well which could be a good or a bad thing.
Or if you could arrange for the interesting bit to be bit 6 or bit 7 then simply "bit collision_flg" would be enough to set a flag depending upon the state of the bit, very cheaply without any "and", "cmp", or modification of the variable. It looks like it came from a hardware register though which I don't know the meaning of, so perhaps it's not possible to change which bit gets set here.
Code:
212 - lda ($64),y
213 sta ($66),y
214 lda ($62),y
215 sta ($60),y
216 iny
217 bne -
What are these addresses used for? It'd be nice to give them names. You can see from the code what is happening, but names would make it clearer why it's being done. "scr_data_ptr" and "screen_ram_ptr" for example for the first two.
Code:
449 lda joy_cache
450 and #$08
451 bne +
Similar to the case above for testing bit 1 - if it's possible to change the order of these tests in joystick_handler, then you could just "ror joy_cache" to get the bottom bit into the carry and then handle upward movement first; and then "ror joy_cache" again to get the next bit and handle downward movement, etc. But it looks like you only want to process one direction, and this change would alter the gameplay, so maybe not something you'd actually want to do here.
Code:
513 ; we are looking one space to the right
514 ; so add 1 to the location
515 lda screen_addr_lo
516 clc
517 adc #$01
518 sta screen_addr_lo
519 lda screen_addr_hi
520 adc #$00
521 sta screen_addr_hi
This can be written as "inc screen_addr_lo", "bne +", "inc screen_addr_hi" - fewer instructions and fewer cycles at runtime as well!
Similarly:
Code:
541 clc
542 lda SPRITE_0_X_POSITION
543 adc #$01
544 bcc +
545 ldx #$01
546 stx SPRITE_XMSB
547 + sta SPRITE_0_X_POSITION
Here you did use the branch to detect the carry case, but could have used "inc SPRITE_0_X_POSITION" and then "bne +" instead of "bcc +", and saved a few more instructions and cycles.
Code:
892 ldy #40 ; Number of columns per row
893 jsr mult_8b_by_8b
This is tough on a CPU without hardware multiplication. Note though that 40 = 5*8, and only has two bits set. So you can hardcode a multiply-by-5 routine with 16-bit carry (asl+rol, asl+rol, adc+adc#0) and then just shift three more bits, to mulitply by 40.
If you only usually move the object relatively (e.g. one square or pixel left/right/up/down at a time), then it's even better to maintain a separate variable tracking the address of the object on the screen at all times, and add or subtract 40 to that at the same time as altering its coordinates. The movement is slightly more expensive, but the rendering is much cheaper as you don't need to multiply. In some cases you don't even need the unmultiplied coordinate any more, you can just use the screen address to track the object's position! But it's harder to get your head around. A BBC Micro game I disassembled a year or so ago did this a lot - even tracking the player's lives by the screen memory address of the greatest life "pip" on the screen, and the game time remaining by the screen address of the top of the "time remaining" bar that's displayed for the player.