[Sonic 1 GitHub] How to port Sonic CD's Extended Camera to Sonic 1

Discussion in 'Tutorials' started by Nat The Porcupine, Apr 12, 2018.

  1. Nat The Porcupine

    Nat The Porcupine Newcomer Member

    Joined:
    Jun 23, 2017
    Messages:
    23
    Location:
    Harrisburg, Pennsylvania (USA)
    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.

    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:
    Code:
    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:
    Code:
    ...
    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++"):
    Code:
    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:
    Code:
    ; ---------------------------------------------------------------------------
    ; 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):
    Code:
    ; ||||||||||||||| 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:
    Code:
    ; ||||||||||||||| 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:
    Code:
    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:
    Code:
    ; ---------------------------------------------------------------------------
    ; 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
    ; ===========================================================================
    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.
     
    Last edited: Apr 12, 2018
  2. Clownacy

    Clownacy UP - ON - CPU Staff

    Joined:
    Aug 15, 2014
    Messages:
    842
    Personally, and I know I'm not alone on this, I find the extended camera a little disorienting. All the constant-moving aside, the SCD implementation doesn't really take U-turns into account, so if you get flung into the air after speeding to the right, but then you decide to head left while in the air, the camera will stay on the right, leaving you with absolutely no visibility. A few of Hydrocity's big rings rely on doing movements like this, so it can be a problem there.

    That problem's all over the place in Spring Yard as well: a secret at the start of Act 1 is way harder to see, and riding a half-pipe will cause the camera to look away from the half-pipe. There's also a bug where any of the sections with a spring that launches Sonic upward into a slope, allowing him to roll along the ceiling, causes the camera to look behind him.

    I think it's also worth mentioning that I think the problems of having little screen-space can be avoided by just designing the levels around it: if you find yourself running straight into enemies and hazards all the time, then that's a failure on the level designer's end (or you're just bad at Sonic, I guess). Not to say the extended camera doesn't have its uses, but I think people should just be careful not to use it as a crutch. This is something that's bothered me when people compare the original games to the TaxStealth versions, as if the originals being 4:3 somehow makes them unplayable, despite the levels being built around it.

    My problems with the camera aside, great guide.
     
    Last edited: Apr 13, 2018
  3. Nat The Porcupine

    Nat The Porcupine Newcomer Member

    Joined:
    Jun 23, 2017
    Messages:
    23
    Location:
    Harrisburg, Pennsylvania (USA)
    Even though I did sings the camera's praises a little at the very beginning of the guide, I actually agree with most of your points. By no means should this act as a crutch for poor level design. In fact, in many cases, it can actually make the overall experience of a poorly designed level much worse. That's why Wacky Workbench Zone is the only level in Sonic CD to completely omit the extended camera; the bouncy floor is already way too disorienting on it's own, so adding in the extended camera would make playing through the zone of eternal infamy even more nauseating than it already is. The extended camera really is hit-&-miss in it's implementation; it's great when it does what you want, but when it doesn't act as expected, it really is an unbelievable annoyance.

    To be honest with you though, I wrote this guide for 2 main reasons:

    1. Selbi's code & tutorial for his implementation of the extended camera are no longer available for reasons that I'm not entirely clear on (I think a few people stole & redistributed it?). In addition, Selbi's version of the camera seemed to jerk around a bit too much and lacked Spin Dash support by default. This kind of left a few people who did want this feature without any options once he locked his code down.

    2. This is supposed to act as a bit of a preview of things to come in my work-in-progress disassembly of Sonic CD that I've been spearheading by myself for the past 3 months or so. I have plans to release the source for R11A__.MMD publicly by the end of the year, complete with an assembler environment, detailed comments, variable names, and revisional differences between the prototypes, JP/EU final, and the US final with build options for each. Hopefully, releasing that will have a ripple effect that will allow us to, as a community, finally put together a full disassembly of Sonic CD.

    Anyway, I'm really glad you liked the guide; it's good to know that after a period of inactivity, I'm still able make a meaningful contribution that'll hopefully solidify my status as a member here.
     
    FireRat, Spiritmaster, Calvin and 3 others like this.