Extending Sprite Limits - RetroKoH/S1Fixed GitHub Wiki
(Original guide by MarkeyJester)
Source: SCHG Page
Commit: 3f958d1
While editing Sonic's sprites for Sonic 1, you may have noticed a few issues in-game when Sonic has more than a certain number of sprites. By default, Sonic 1 has a sprite map limit of 127 ($7F) sprites, and several parts of the game engine will prevent you from having more. This becomes problematic if you wish to add sprites for new abilities, or additional frames to make animations smoother. This guide shows you how to expand that limit.
Extending Sonic's Map/Sprite Limit
The first thing we want to do is extend the number of sprite frames Sonic can have. In sonic.asm, We need to go to BuildSprites:
and find .drawObject:
. The code looks like this:
.drawObject:
movea.l obMap(a0),a1
moveq #0,d1
btst #5,d4 ; is static mappings flag on?
bne.s .drawFrame ; if yes, branch
move.b obFrame(a0),d1
add.b d1,d1 ; --- NOTICE THIS LINE ---
adda.w (a1,d1.w),a1 ; get mappings frame address
move.b (a1)+,d1 ; number of sprite pieces
subq.b #1,d1
bmi.s .setVisible
What happens here is the mapping frame ID is loaded into d1 move.b obFrame(a0),d1
, and it is then multiplied by 2 add.b d1,d1
, but this only multiplies one byte of data, so frame ID's higher than $7F won't multiply properly (e.g. $80 x $02 = $100, only the the right two digits are read $00). To fix this, simply change the .b in add.b d1,d1
to .w for word. This will ensure values from $80 onward will multiply by 2 properly.
Not so fast! Because we changed that value from a byte to a word, we have caused a potential fatal error that will cause the game to crash if a sprite map ID from $80 onward is set to show. This is because of the line move.b (a1)+,d1
that will load the number of sprite pieces in the current frame. It only moves a single byte to d1, which IS what we want, but this is now a problem because our new add.w d1,d1
instruction has changed a word of data in d1, so the left side of the word is still in d1 and needs to be cleared. Adding moveq #0,d1
above move.b (a1)+,d1
will ensure that the register is cleared and ready to use. Here is the modified code:
.drawObject:
movea.l obMap(a0),a1
moveq #0,d1
btst #5,d4 ; is static mappings flag on?
bne.s .drawFrame ; if yes, branch
move.b obFrame(a0),d1
! add.w d1,d1 ; result is now a word value
adda.w (a1,d1.w),a1 ; get mappings frame address
+ moveq #0,d1 ; clear d1 so we can use it safely
move.b (a1)+,d1 ; number of sprite pieces
subq.b #1,d1
bmi.s .setVisible
Extending Sonic's Animation Frame Limit
Now that we've extended Sonic's mapping frame limit, we need to expand Sonic's animation frame limit to suit. Sonic 1 normally allows only frames with indices $00-$7F as animation frames. We are going to expand this to $FC. First, let's go to incObj/Sonic Animate.asm and find this code under the label .loadframe
:
.loadframe:
moveq #0,d1
move.b obAniFrame(a0),d1 ; load current frame number
move.b 1(a1,d1.w),d0 ; read sprite number from script
bmi.s .end_FF ; if animation is complete, branch
SAnim_Next:
move.b d0,obFrame(a0) ; load sprite number
addq.b #1,obAniFrame(a0) ; next frame number
The instruction with "bmi" is what prevents Sonic's animation frame from going beyond $7F. bmi
is an instruction that reads a value as a signed value, (can be positive or negative), and branches if that value is negative ($80 to $FF), In order to allow the code to work with these values, we need to treat the frame index as an unsigned (always positive) number. To do this, tweak the code like so:
.loadframe:
moveq #0,d1
move.b obAniFrame(a0),d1 ; load current frame number
move.b 1(a1,d1.w),d0 ; read sprite number from script
+ cmpi.b #$FD,d0 ; is it a flag from FD to FF? - Added comparison check
! bhs.s .end_FF ; if so, branch to flag routines - modified conditional branch
SAnim_Next:
move.b d0,obFrame(a0) ; load sprite number
addq.b #1,obAniFrame(a0) ; next frame number
This will now make values $FD to $FF branch to .end_FF:
while all other values ($00 to $FC) will continue down to SAnim_Next:
and be treated as proper animation frame indices. NOTE: Values $FD, $FE and $FF are special flags for things like walking/running speed/rotation, etc. so we don't want those flags to be interpreted as animation frames.
Extending Sonic's Art Limit
OK, so Sonic's animations now support up to $FC, and the mappings can be read up to $FC mappings too. However, having $FC sprites means you'll likely be using a lot of art. Unfortunately, Sonic 1 can only load Sonic's art with an offset range of $0000-$FFFF (for a maximum of $7FF tiles). We're going to expand this to $0000-$1FFFF (a new maximum of $FFF tiles).
In _incObj/Sonic LoadGfx.asm, we will go to Sonic_LoadGfx:
and find the label .readentry
. Here is the code:
.readentry:
moveq #0,d2
move.b (a2)+,d2 ; d2 = number of tiles to load
move.w d2,d0
lsr.b #4,d0
lsl.w #8,d2 ; tile count is now in the upper byte
move.b (a2)+,d2 ; d2 = tile offset ID (lower byte), tile count (upper byte)
lsl.w #5,d2 ; multiply tile offset by $20 (Each tile is $20 bytes)
lea (Art_Sonic).l,a1 ; load Sonic's art file to a1
adda.l d2,a1 ; a1 = (Sonic's art file + tile offset)
This section loads data from Sonic's DPLC, stored in register a2
. The tile offset ID is multiplied by $20 to get the offset of Sonic's art, but only a word is read so the offset may only go up to $FFFF, let's fix this by changing the .w in lsl.w #5,d2
to .l, this will ensure that it multiplies the value into long-word rather than just word.
However, d2 still contains the tile count on the far left nybble, so let's clear that nybble by putting andi.w #$0FFF,d2
above lsl.l #5,d2
. Here's the resulting code:
.readentry:
moveq #0,d2
move.b (a2)+,d2 ; d2 = (number of tiles to load - 1; "tile count")
move.w d2,d0
lsr.b #4,d0 ; d0 = tile count
lsl.w #8,d2 ; "tile count" is now in the upper byte, left nybble
move.b (a2)+,d2 ; d2 = tile offset ID (lower byte), tile count (uppermost nybble)
+ andi.w #$0FFF,d2 ; clear the counter to remove tile count from far left nybble
! lsl.l #5,d2 ; changed to .l (allows for more than $FFFF bytes)
lea (Art_Sonic).l,a1 ; load Sonic's art file to a1
adda.l d2,a1 ; a1 = (Sonic's art file + tile offset)
With these changes, you can now have Sonic utilize sprite mappings of up to 252 ($FC) frames, and art the size of up to 4,095 ($FFF) tiles. Almost double the previous amount.
(OPTIONAL) Extending Animation Frame Limit For All Objects
This is not a necessary change, but based on the fact that the modification to BuildSprites:
has actually extended the sprite mapping limit for all objects, not just Sonic, we may as well extend animations for all objects as well. Open incObj/sub AnimateSprite.asm and find this code under the label Anim_Run:
. It will look like this:
Anim_Run:
subq.b #1,obTimeFrame(a0) ; subtract 1 from frame duration
bpl.s Anim_Wait ; if time remains, branch
add.w d0,d0
adda.w (a1,d0.w),a1 ; jump to appropriate animation script
move.b (a1),obTimeFrame(a0) ; load frame duration
moveq #0,d1
move.b obAniFrame(a0),d1 ; load current frame number
move.b 1(a1,d1.w),d0 ; read sprite number from script
bmi.s Anim_End_FF ; if animation is complete, branch
Just like we did in Sonic_Animate:
, we need to change that bmi
code in the same way. There is one key difference, however. We can only extend the limit to $F9 for objects as they have additional animation flags ($FA, $FB and $FC) for altering the routine counter, but it's still better than $7F, so let's change it like so:
Anim_Run:
subq.b #1,obTimeFrame(a0) ; subtract 1 from frame duration
bpl.s Anim_Wait ; if time remains, branch
add.w d0,d0
adda.w (a1,d0.w),a1 ; jump to appropriate animation script
move.b (a1),obTimeFrame(a0) ; load frame duration
moveq #0,d1
move.b obAniFrame(a0),d1 ; load current frame number
move.b 1(a1,d1.w),d0 ; read sprite number from script
+ cmp.b #$FA,d0 ; is it a flag from FA to FF?
! bhs.s Anim_End_FF ; if so, branch to flag routines
Now objects can animate sprite mappings up to $F9 frames!