Actions

SCHG How-to

Add Extended Camera to Sonic 1

From Sonic Retro

Guide by Nat The Porcupine from SSRG, Hivebrain 2005 disasm by DelayHacks.

In Sonic CD, whenever Sonic reaches and/or exceeds his top speed, the camera will gradually shift in front of him. As you may have guessed, this gives the player a better view of upcoming obstacles and more time to react to them. This is a feature that I believe plenty of ROM hacks would benefit from, especially in high-speed sections. Unfortunately, due to the fact that there isn't even a partial disassembly of Sonic CD available publicly(yet), implementations of this feature are rather scarce.

Well, that fact kind of bothered me, so I decided to disassemble Sonic CD's R11A__.MMD file, grab the necessary code for the extended camera, and port to Sonic 1. Today, I'm going to teach you how to add it yourself.

Github

Step 1: Adding a New Variable

This step is pretty simple; we need to add a new variable definition for the place in RAM that we're going to store the camera's view-port offset value. So open up the "Variables.asm" of your Sonic 1 disassembly and add the following line after the "v_palss_time" variable:

v_camera_pan:    equ $FFFFF7A0    ; Extended Camera - how far the camera/view is panned to the left or right of Sonic (2 bytes)

Keep in mind, you can use any free RAM address you want; I just chose to use "$FFFFF7A0" because that's the same one that Sonic CD uses and, AFAIK, it goes unused in Sonic 1 during normal gameplay (it IS apparently used for the Special Stages for something, but that shouldn't matter here.) Your variable definitions should look something like this:

...
v_palss_num:    equ $FFFFF79A    ; palette cycling in Special Stage - reference number (2 bytes)
v_palss_time:    equ $FFFFF79C    ; palette cycling in Special Stage - time until next change (2 bytes)

v_camera_pan:    equ $FFFFF7A0    ; Extended Camera - how far the camera/view is panned to the left or right of Sonic (2 bytes)

v_obj31ypos:    equ $FFFFF7A4    ; y-position of object 31 (MZ stomper) (2 bytes)
v_bossstatus:    equ $FFFFF7A7    ; status of boss and prison capsule (01 = boss defeated; 02 = prison opened)
...

Step 2: Adding the Ported Routine

Next, open up "sonic.asm" and find the section labeled "Sonic_Control" and make the following additions (labeled with "++add this++"):

Sonic_Control:    ; Routine 2
     
        bsr.s    Sonic_PanCamera    ; ++add this++
     
        tst.w    (f_debugmode).w    ; is debug cheat enabled?
        beq.s    loc_12C58    ; if not, branch
        btst    #bitB,(v_jpadpress1).w ; is button B pressed?
        beq.s    loc_12C58    ; if not, branch
        move.w    #1,(v_debuguse).w ; change Sonic into a ring/item
        clr.b    (f_lockctrl).w
        rts
     
        include    "_incObj\Sonic_PanCamera.asm"    ; ++add this++

Now, go to the _incObj folder and create a new asm file called "Sonic_PanCamera". Go ahead and copy-paste the following code into that new file:

; ---------------------------------------------------------------------------
; Subroutine to    horizontally pan the camera view ahead of the player
; (Ported from the US version of Sonic CD's "R11A__.MMD" by Nat The Porcupine)
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||


Sonic_PanCamera:
        move.w    (v_camera_pan).w,d1        ; get the current camera pan value
        move.w    obInertia(a0),d0        ; get sonic's inertia
        bpl.s    @abs_inertia            ; if sonic's inertia is positive, branch ahead
        neg.w    d0                        ; otherwise, we negate it to get the absolute value

    @abs_inertia:

; These lines were intended to prevent the Camera from panning while
; going up the very first giant ramp in Palmtree Panic Zone Act 1.
; However, given that no such object exists in Sonic 1, I just went
; ahead and commented these out.
;        btst    #1,$2C(a0)                ; is sonic going up a giant ramp in PPZ?
;        beq.s    @skip                    ; if not, branch
;        cmpi.w    #$1B00,obX(a0)            ; is sonic's x position lower than $1B00?
;        bcs.s    @reset_pan                ; if so, branch

; These lines aren't part of the original routine; I added them myself.
; If you've ported the Spin Dash, uncomment the following lines of code
; to allow the camera to pan ahead while charging the Spin Dash:
;        tst.b    $39(a0)                    ; is sonic charging up a spin dash?
;        beq.s    @skip                    ; if not, branch
;        btst    #0,obStatus(a0)            ; check the direction that sonic is facing
;        bne.s    @pan_right                ; if he's facing right, pan the camera to the right
;        bra.s    @pan_left                ; otherwise, pan the camera to the left

    @skip:
        cmpi.w    #$600,d0                ; is sonic's inertia greater than $600
        bcs.s    @reset_pan                ; if not, recenter the screen (if needed)
        tst.w    obInertia(a0)            ; otherwise, check the direction of inertia (by subtracting it from 0)
        bpl.s    @pan_left                ; if the result was positive, then inertia was negative, so we pan the screen left

    @pan_right:
        addq.w    #2,d1                    ; add 2 to the pan value
        cmpi.w    #224,d1                    ; is the pan value greater than 224 pixels?
        bcs.s    @update_pan                ; if not, branch
        move.w    #224,d1                    ; otherwise, cap the value at the maximum of 224 pixels
        bra.s    @update_pan                ; branch
; ---------------------------------------------------------------------------

    @pan_left:
        subq.w    #2,d1                    ; subtract 2 from the pan value
        cmpi.w    #96,d1                    ; is the pan value less than 96 pixels?
        bcc.s    @update_pan                ; if not, branch
        move.w    #96,d1                    ; otherwise, cap the value at the minimum of 96 pixels
        bra.s    @update_pan                ; branch
; ---------------------------------------------------------------------------

    @reset_pan:
        cmpi.w    #160,d1                    ; is the pan value 160 pixels?
        beq.s    @update_pan                ; if so, branch
        bcc.s    @reset_left                ; otherwise, branch if it greater than 160
     
    @reset_right:
        addq.w    #2,d1                    ; add 2 to the pan value
        bra.s    @update_pan                ; branch
; ---------------------------------------------------------------------------

    @reset_left:
        subq.w    #2,d1                    ; subtract 2 from the pan value

    @update_pan:
        move.w    d1,(v_camera_pan).w        ; update the camera pan value
        rts                                ; return
     
; End of function Sonic_PanCamera

You may have noticed that I commented out some code, you don't need to worry about that unless you've added the Spin Dash to your hack and would like the camera to pan in-front of the player when charging one up. If you have, you can un-comment the lines of code that I labeled inside the asm file.

Step 3: Odds and Ends

Now that we have the routine setup, we just have to make a few more adjustments to get this working. First, go to the _inc folder and open up both "DeformLayers.asm" and "DeformLayers (JP1).asm"; we're going to make the same change to both of them. In both files the location labeled "MoveScreenHoriz" and replace the entire routine (aka this):

; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||


MoveScreenHoriz:
        move.w    (v_player+obX).w,d0
        sub.w    (v_screenposx).w,d0 ; Sonic's distance from left edge of screen
        subi.w    #144,d0        ; is distance less than 144px?
        bcs.s    SH_BehindMid    ; if yes, branch
        subi.w    #16,d0        ; is distance more than 160px?
        bcc.s    SH_AheadOfMid    ; if yes, branch
        clr.w    (v_scrshiftx).w
        rts 
; ===========================================================================

SH_AheadOfMid:
        cmpi.w    #16,d0        ; is Sonic within 16px of middle area?
        bcs.s    SH_Ahead16    ; if yes, branch
        move.w    #16,d0        ; set to 16 if greater

    SH_Ahead16:
        add.w    (v_screenposx).w,d0
        cmp.w    (v_limitright2).w,d0
        blt.s    SH_SetScreen
        move.w    (v_limitright2).w,d0

SH_SetScreen:
        move.w    d0,d1
        sub.w    (v_screenposx).w,d1
        asl.w    #8,d1
        move.w    d0,(v_screenposx).w ; set new screen position
        move.w    d1,(v_scrshiftx).w ; set distance for screen movement
        rts 
; ===========================================================================

SH_BehindMid:
        add.w    (v_screenposx).w,d0
        cmp.w    (v_limitleft2).w,d0
        bgt.s    SH_SetScreen
        move.w    (v_limitleft2).w,d0
        bra.s    SH_SetScreen
; End of function MoveScreenHoriz

...with this:

; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||


MoveScreenHoriz:
        move.w    (v_player+obX).w,d0
        sub.w    (v_screenposx).w,d0 ; Sonic's distance from left edge of screen
        sub.w    (v_camera_pan).w,d0    ; Horizontal camera pan value
        beq.s    SH_ProperlyFramed    ; if zero, branch
        bcs.s    SH_BehindMid    ; if less than, branch
        bra.s    SH_AheadOfMid    ; branch
; ===========================================================================

SH_ProperlyFramed:
        clr.w    (v_scrshiftx).w
        rts 
; ===========================================================================

SH_AheadOfMid:
        cmpi.w    #16,d0        ; is Sonic within 16px of middle area?
        blt.s    SH_Ahead16    ; if yes, branch
        move.w    #16,d0        ; set to 16 if greater

SH_Ahead16:
        add.w    (v_screenposx).w,d0
        cmp.w    (v_limitright2).w,d0
        blt.s    SH_SetScreen
        move.w    (v_limitright2).w,d0

SH_SetScreen:
        move.w    d0,d1
        sub.w    (v_screenposx).w,d1
        asl.w    #8,d1
        move.w    d0,(v_screenposx).w ; set new screen position
        move.w    d1,(v_scrshiftx).w ; set distance for screen movement
        rts

; ===========================================================================

SH_BehindMid:
        cmpi.w    #-16,d0        ; is Sonic within 16px of middle area?
        bge.s    SH_Behind16    ; if no, branch
        move.w    #-16,d0        ; set to -16 if less

SH_Behind16:
        add.w    (v_screenposx).w,d0
        cmp.w    (v_limitleft2).w,d0
        bgt.s    SH_SetScreen
        move.w    (v_limitleft2).w,d0
        bra.s    SH_SetScreen
      
; End of function MoveScreenHoriz

This new version of the routine factors our camera pan value in to the horizontal scrolling, makes sure that the center of the screen is always 160 pixels when not panning, and includes a bug fix from Sonic 2 (there was already a guide for that last one, but since we're working with the same routine, I included it for convenience).

Finally, open up both "LevelSizeLoad & BgScrollSpeed.asm" and "LevelSizeLoad & BgScrollSpeed (JP1).asm" files; again, we're making the same change to both. Just before the branch at the end of the first section of "LevelSizeLoad", add the following line:

move.w    #160,(v_camera_pan).w    ; reset the horizontal camera pan value to 160 pixels

This makes sure that the camera pan value is reset to the center any time the level is loaded, which makes it so that the camera doesn't have to re-adjust itself if sonic re-spawns from a checkpoint after dying. Technically, we could have placed this anywhere within the first block of the routine; I just decided to have you place it before the branch to "LevSz_ChkLamp" because that's exactly where that line was placed in Sonic CD. Just to reiterate, your code should look like this:

; ---------------------------------------------------------------------------
; Subroutine to    load level boundaries and start    locations
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||


LevelSizeLoad:
        moveq    #0,d0
        move.b    d0,($FFFFF740).w
        move.b    d0,($FFFFF741).w
        move.b    d0,($FFFFF746).w
        move.b    d0,($FFFFF748).w
        move.b    d0,(v_dle_routine).w
        move.w    (v_zone).w,d0
        lsl.b    #6,d0
        lsr.w    #4,d0
        move.w    d0,d1
        add.w    d0,d0
        add.w    d1,d0
        lea    LevelSizeArray(pc,d0.w),a0 ; load level    boundaries
        move.w    (a0)+,d0
        move.w    d0,($FFFFF730).w
        move.l    (a0)+,d0
        move.l    d0,(v_limitleft2).w
        move.l    d0,(v_limitleft1).w
        move.l    (a0)+,d0
        move.l    d0,(v_limittop2).w
        move.l    d0,(v_limittop1).w
        move.w    (v_limitleft2).w,d0
        addi.w    #$240,d0
        move.w    d0,(v_limitleft3).w
        move.w    #$1010,($FFFFF74A).w
        move.w    (a0)+,d0
        move.w    d0,(v_lookshift).w
        move.w    #160,(v_camera_pan).w    ; reset the horizontal camera pan value to 160 pixels
        bra.w    LevSz_ChkLamp

Hivebrain's 2005

Step 1: Adding the ported routine

We need to add main code to game once. Go to "Obj01_Control" and below it add this.

; ---------------------------------------------------------------------------
; Subroutine to    horizontally pan the camera view ahead of the player
; (Ported from the US version of Sonic CD's "R11A__.MMD" by Nat The Porcupine, ported to Hivebrain disasm by DelayHacks)
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||

Sonic_PanCamera:
        move.w    ($FFFFF7A0).w,d1        ; get the current camera pan value
        move.w    $14(a0),d0        ; get sonic's inertia
        bpl.s    Sonic_PanCamera_abs_inertia            ; if sonic's inertia is positive, branch ahead
        neg.w    d0                        ; otherwise, we negate it to get the absolute value

Sonic_PanCamera_abs_inertia:

; These lines were intended to prevent the Camera from panning while
; going up the very first giant ramp in Palmtree Panic Zone Act 1.
; However, given that no such object exists in Sonic 1, I just went
; ahead and commented these out.
;        btst    #1,$2C(a0)                ; is sonic going up a giant ramp in PPZ?
;        beq.s    Sonic_PanCamera_skip                    ; if not, branch
;        cmpi.w    #$1B00,($FFFFD008).w            ; is sonic's x position lower than $1B00?
;        bcs.s    Sonic_PanCamera_reset_pan                ; if so, branch

; These lines aren't part of the original routine; I added them myself.
; If you've ported the Spin Dash, uncomment the following lines of code
; to allow the camera to pan ahead while charging the Spin Dash:
        tst.b    $39(a0)                    ; is sonic charging up a spin dash?
        beq.s    Sonic_PanCamera_skip                    ; if not, branch
        btst    #0,$22(a0)            ; check the direction that sonic is facing
        bne.s    Sonic_PanCamera_pan_right                ; if he's facing right, pan the camera to the right
        bra.s    Sonic_PanCamera_pan_left                ; otherwise, pan the camera to the left

Sonic_PanCamera_skip:
        cmpi.w    #$600,d0                ; is sonic's inertia greater than $600
        bcs.s    Sonic_PanCamera_reset_pan                ; if not, recenter the screen (if needed)
        tst.w    $14(a0)            ; otherwise, check the direction of inertia (by subtracting it from 0)
        bpl.s    Sonic_PanCamera_pan_left                ; if the result was positive, then inertia was negative, so we pan the screen left

Sonic_PanCamera_pan_right:
        addq.w    #2,d1                    ; add 2 to the pan value
        cmpi.w    #224,d1                    ; is the pan value greater than 224 pixels?
        bcs.s    Sonic_PanCamera_update_pan                ; if not, branch
        move.w    #224,d1                    ; otherwise, cap the value at the maximum of 224 pixels
        bra.s    Sonic_PanCamera_update_pan                ; branch
; ---------------------------------------------------------------------------

Sonic_PanCamera_pan_left:
        subq.w    #2,d1                    ; subtract 2 from the pan value
        cmpi.w    #96,d1                    ; is the pan value less than 96 pixels?
        bcc.s    Sonic_PanCamera_update_pan                ; if not, branch
        move.w    #96,d1                    ; otherwise, cap the value at the minimum of 96 pixels
        bra.s    Sonic_PanCamera_update_pan                ; branch
; ---------------------------------------------------------------------------

Sonic_PanCamera_reset_pan:
        cmpi.w    #160,d1                    ; is the pan value 160 pixels?
        beq.s    Sonic_PanCamera_update_pan                ; if so, branch
        bcc.s    Sonic_PanCamera_reset_left                ; otherwise, branch if it greater than 160
     
Sonic_PanCamera_reset_right:
        addq.w    #2,d1                    ; add 2 to the pan value
        bra.s    Sonic_PanCamera_update_pan                ; branch
; ---------------------------------------------------------------------------

Sonic_PanCamera_reset_left:
        subq.w    #2,d1                    ; subtract 2 from the pan value

Sonic_PanCamera_update_pan:
        move.w    d1,($FFFFF7A0).w        ; update the camera pan value
        rts                                ; return
     
; End of function Sonic_PanCamera

And then, find and replace Obj01_Control label to this:

Obj01_Control:                ; XREF: Obj01_Index
        bsr.s   Sonic_PanCamera    ; ++add this++
	tst.w	($FFFFFFFA).w	; is debug cheat enabled?
	beq.s	loc_12C58	; if not, branch
	btst	#4,($FFFFF605).w ; is button C pressed?
	beq.s	loc_12C58	; if not, branch
	move.w	#1,($FFFFFE08).w ; change Sonic	into a ring/item
	clr.b	($FFFFF7CC).w
	rts

Step 2: Changing camera's code and reset camera at start

Now find "ScrollHoriz2" routine and replace all code from this label to loc_65CC with this:

ScrollHoriz2:
        move.w    ($FFFFD008).w,d0
        sub.w    ($FFFFF700).w,d0 ; Sonic's distance from left edge of screen
        sub.w    ($FFFFF7A0).w,d0    ; Horizontal camera pan value
        beq.s    SH_ProperlyFramed    ; if zero, branch
        bcs.s    SH_BehindMid    ; if less than, branch
        bra.s    SH_AheadOfMid    ; branch
; ===========================================================================

SH_ProperlyFramed:
        clr.w    ($FFFFF73A).w
        rts 
; ===========================================================================

SH_AheadOfMid:
        cmpi.w    #16,d0        ; is Sonic within 16px of middle area?
        blt.s    SH_Ahead16    ; if yes, branch
        move.w    #16,d0        ; set to 16 if greater

SH_Ahead16:
        add.w    ($FFFFF700).w,d0
        cmp.w    ($FFFFF72A).w,d0
        blt.s    SH_SetScreen
        move.w    ($FFFFF72A).w,d0

SH_SetScreen:
        move.w    d0,d1
        sub.w    ($FFFFF700).w,d1
        asl.w    #8,d1
        move.w    d0,($FFFFF700).w ; set new screen position
        move.w    d1,($FFFFF73A).w ; set distance for screen movement
        rts

; ===========================================================================

SH_BehindMid:
        cmpi.w    #-16,d0        ; is Sonic within 16px of middle area?
        bge.s    SH_Behind16    ; if no, branch
        move.w    #-16,d0        ; set to -16 if less

SH_Behind16:
        add.w    ($FFFFF700).w,d0
        cmp.w    ($FFFFF728).w,d0
        bgt.s    SH_SetScreen
        move.w    ($FFFFF728).w,d0
        bra.s    SH_SetScreen
      
; End of function MoveScreenHoriz

; ===========================================================================
        tst.w    d0
        bpl.s    loc_6610
        move.w    #$FFFE,d0
        bra.s    SH_Behind16
; ===========================================================================

loc_6610:
        move.w    #2,d0
        bra.s    SH_AheadOfMid

And the last one, find routine "LevelSizeLoad" and paste this line at the end:

        move.w  #160,($FFFFF7A0).w    ; reset the horizontal camera pan value to 160 pixels

Conclusion

Well, that's everything! Just run the build script and everything should be smooth sailing from there. I plan on updating this guide later on with some optional optimizations to make the code run ever so slightly faster as well as adapting it for use with Sonic 2 and Sonic 3&K. However, at the time of writing, I'm on a bit of a tight schedule (college, work, family, ect.), so that update will have to come at a much later date. Even so, if anyone has any question, concerns, or snide remarks, I'll do my best to respond within a reasonable time frame. Also, I'd like to thank for even reading the guide; the first tutorial I ever posted here was a complete disaster (it got purged, so just take my word for it), so I was a little afraid to post another one because of that. Hopefully, my guides will only get better from here.

P.S. Since I know that at least a few of you are going to find the fact that a I, a Trialist member, managed to single-handedly disassemble a file from Sonic CD and extract some code from it a little more than hard to believe, I'll just address your main concern right out the gate: this code IS indeed from Sonic CD. If you have a JP or EU version of Sonic CD, you can find the routine at address 0x4126 of the R11A__.MMD file. However, if you have the US version, the routine is actually at address 0x412A of R11A__.MMD instead, due to some revisional between the two. I'd encourage anybody who is skeptical of the code's origins to disassemble the file themselves (whether it be by hand or with a tool like IDA pro) if they would like confirmation from a source other than myself. ]]