[Sonic 1] Basics of Dynamic Level Events, and how to use them in your levels.

Discussion in 'Tutorials' started by giovanni.gen, Feb 4, 2020.

  1. giovanni.gen

    giovanni.gen It's still Joe-vanni, not Geo-vanni. Member

    Joined:
    Apr 16, 2015
    Messages:
    313
    Location:
    Italy
    What are Dynamic Level Events?

    Dynamic Level Events are events that happen during a level when specific conditions are met. These conditions are checked every frame, and are generally unique to every level.

    Dynamic Level Events are found in "_inc\DynamicLevelEvents.asm" if you're a GitHub user, or the DynScrResizeLoad subroutine if you're a Hivebrain user, and are generally used to resize a level when you reach specific coordinates, but they can be used for pretty much anything you want. The game, in fact, uses them to spawn bosses in the level, or as a passageway to other levels.

    Here's an example of Dynamic Level Event routine taken straight from Scrap Brain Zone act 3:

    Code:
    DLE_SBZ3:
            cmpi.w    #$D00,(v_screenposx).w ; check if the screen has reached $D00 on the X coordinate
            bcs.s    locret_6F8C ; if it hasn't, branch
            cmpi.w    #$18,(v_player+obY).w ; has Sonic reached the top of the level?
            bcc.s    locret_6F8C    ; if not, branch
            clr.b    (v_lastlamp).w
            move.w    #1,(f_restart).w ; restart level
            move.w    #(id_SBZ<<8)+2,(v_zone).w ; set level number to 0502 (FZ)
            move.b    #1,(f_lockmulti).w ; freeze Sonic
    
    locret_6F8C:
            rts
    

    Code:
    Resize_SBZ3:
            cmpi.w    #$D00,($FFFFF700).w ; check if the screen has reached this X coordinate
            bcs.s    locret_6F8C ; if it hasn't, branch
            cmpi.w    #$18,($FFFFD00C).w ; has Sonic reached the top of the level?
            bcc.s    locret_6F8C    ; if not, branch
            clr.b    ($FFFFFE30).w ; clear the lamp post counter
            move.w    #1,($FFFFFE02).w ; restart level
            move.w    #$502,($FFFFFE10).w ; set level    number to 0502 (FZ)
            move.b    #1,($FFFFF7C8).w ; freeze Sonic
    
    locret_6F8C:
            rts    

    This snippet of code represents the way SBZ3 takes you to FZ. What all the code means is that if Sonic reaches $18 on the Y coordinate while his X coordinate is higher than $D00, he will be transported to the Final Zone (The "last checkpoint" variable is reset, and the restart flag is enabled to properly reset Sonic, so that he can reach the Final Zone without in game issues).

    You can do so much more with Dynamic Level Events, as long as you have imagination and programming abilities. While this guide stops at changing level boundaries, you can use it for whatever comes to mind.

    How to use DLEs to change the level boundaries.

    I never really liked that section in GHZ2 with the large moving platforms and the spikes at the bottom. I always felt it was too frustrating, so I decided to get rid of it and replace it with something else.

    level.png

    You'll notice that the area enclosed in the red rectangle has been changed. It has two routes, each connecting to a different part of the original level.

    However, there's a problem with the bottom route, and if you've played enough Sonic 1, I think you already know how things are going to end. If you're confused, here's a video.



    Yup, Sonic dies upon entering the S-tube. That's because of how the original DLEs are set up.

    Code:
    DLE_GHZ2:
            move.w    #$300,(v_limitbtm1).w ; initialize the target level boundary (this first value MUST match the one in the level size array)
            cmpi.w    #$ED0,(v_screenposx).w ; check if the screen has reached this X coordinate
            bcs.s    locret_6E3A ; if not, branch
            move.w    #$200,(v_limitbtm1).w ; else, update the target level boundary
            cmpi.w    #$1600,(v_screenposx).w ; do it again and again
            bcs.s    locret_6E3A
            move.w    #$400,(v_limitbtm1).w
            cmpi.w    #$1D60,(v_screenposx).w
            bcs.s    locret_6E3A
            move.w    #$300,(v_limitbtm1).w
    
    locret_6E3A:
            rts
    

    Code:
    Resize_GHZ2:
            move.w    #$300,($FFFFF726).w ; initialize the target level boundary (this first value MUST match the one in the level size array)
            cmpi.w    #$ED0,($FFFFF700).w ; check if the screen has reached this X coordinate
            bcs.s    locret_6E3A ; if not, branch
            move.w    #$200,($FFFFF726).w ; else, update the target level boundary
            cmpi.w    #$1600,($FFFFF700).w; do it again and again
            bcs.s    locret_6E3A
            move.w    #$400,($FFFFF726).w
            cmpi.w    #$1D60,($FFFFF700).w
            bcs.s    locret_6E3A
            move.w    #$300,($FFFFF726).w
    
    locret_6E3A:
            rts    

    When the screen reaches $ED0 on the X coordinate, the bottom boundary goes up to $200 on the Y coordinate. This corresponds to the bottomless pit that was originally on GHZ2.

    What you need to change ultimately depends on how you design your level, so you'll need to figure out things by yourself. If you find yourself in trouble, you can always use Debug mode or a RAM viewer to help yourself find the right coordinates.

    Here's how I changed the code for my own level:

    Code:
    DLE_GHZ2:
            move.w    #$300,(v_limitbtm1).w
            cmpi.w    #$1450,(v_screenposx).w
            bcs.s    locret_6E3A
            move.w    #$400,(v_limitbtm1).w
            cmpi.w    #$1D60,(v_screenposx).w
            bcs.s    locret_6E3A
            move.w    #$300,(v_limitbtm1).w
    
    locret_6E3A:
            rts    

    Code:
    DLE_GHZ2:
            move.w    #$300,($FFFFF726).w
            cmpi.w    #$1450,($FFFFF700).w
            bcs.s    locret_6E3A
            move.w    #$400,($FFFFF726).w
            cmpi.w    #$1D60,($FFFFF700).w
            bcs.s    locret_6E3A
            move.w    #$300,($FFFFF726).w
    
    locret_6E3A:
            rts    

    What I did was remove the trigger that caused the bottom limit to go up to $200, and moved the trigger that increases such limit earlier into the level. This new placement will cause the bottom boundary to move when you reach that part in the level near the new staircase with the crabmeat.

    Here's how the rest of the bottom route plays:



    These new Dynamic Level Event routine will allow the player to actually take the bottom route as I intended. Of course, all edits you must make depend entirely on how YOU design your level and how you intend it to be played.

    ----------------

    Dynamic Level Event subroutines

    Some levels use more elaborate routines to perform more elaborate operations with level boundaries. Have a look at an example, explained with comments.

    Code:
    Resize_MZ1:
            moveq    #0,d0 ; clear the content of data register 0
            move.b    ($FFFFF742).w,d0 ; move the internal DLE routine counter's current value to d0
            move.w    off_6FB2(pc,d0.w),d0 ; load the proper instruction based on the value on d0 (only use multiples of 2)
            jmp    off_6FB2(pc,d0.w) ; jump to the appropriate instruction
    ; ===========================================================================
    off_6FB2:    dc.w loc_6FBA-off_6FB2 ; routine counter is 0
            dc.w loc_6FEA-off_6FB2 ; routine counter is 2
            dc.w loc_702E-off_6FB2 ; routine counter is 4
            dc.w loc_7050-off_6FB2 ; routine counter is 6
    ; ===========================================================================
    
    loc_6FBA: ; beginning of the level
            move.w    #$1D0,($FFFFF726).w ; move the target bottom boundary
            cmpi.w    #$700,($FFFFF700).w ; check the screen's X coordinate
            bcs.s    locret_6FE8 ; if too low, branch
            move.w    #$220,($FFFFF726).w ; move the target bottom boundary
            cmpi.w    #$D00,($FFFFF700).w ; check the screen's X coordinate
            bcs.s    locret_6FE8 ; if too low, branch
            move.w    #$340,($FFFFF726).w ; move the target bottom boundary
            cmpi.w    #$340,($FFFFF704).w ; check the screen's Y coordinate
            bcs.s    locret_6FE8 ; if too low, branch
            addq.b    #2,($FFFFF742).w ; add 2 to the routine counter
    
    locret_6FE8:
            rts
    ; ===========================================================================
    
    loc_6FEA: ; the part with the pillars that crush you
            cmpi.w    #$340,($FFFFF704).w ; check the screen's Y coordinate
            bcc.s    loc_6FF8 ; if too high, branch
            subq.b    #2,($FFFFF742).w ; subtract 2 from the routine counter
            rts ; return, so that none of the following code is run
    ; ===========================================================================
    
    loc_6FF8:
            move.w    #0,($FFFFF72C).w ; update the actual top level boundary
            cmpi.w    #$E00,($FFFFF700).w ; check the screen's X coordinate
            bcc.s    locret_702C ; if too high, branch
            move.w    #$340,($FFFFF72C).w ; update the actual top level boundary
            move.w    #$340,($FFFFF726).w ; update the target bottom level boundary
            cmpi.w    #$A90,($FFFFF700).w ; check the screen's X coordinate
            bcc.s    locret_702C ; if too high, branch
            move.w    #$500,($FFFFF726).w ; update the target bottom boundary
            cmpi.w    #$370,($FFFFF704).w ; check the screen's Y coordinate
            bcs.s    locret_702C ; if too low, branch
            addq.b    #2,($FFFFF742).w ; add 2 to the routine counter
    
    locret_702C:
            rts
    ; ===========================================================================
    
    loc_702E: ; the part right after the button operated spiked chandelier
            cmpi.w    #$370,($FFFFF704).w ; check the screen's Y coordinate
            bcc.s    loc_703C; if too high, branch
            subq.b    #2,($FFFFF742).w ; subtract 2 from the routine counter
            rts ; return, so that none of the following code is run
    ; ===========================================================================
    
    loc_703C:
            cmpi.w    #$500,($FFFFF704).w ; check the screen's Y coordinate
            bcs.s    locret_704E ; if too low, branch
            move.w    #$500,($FFFFF72C).w ; update the actual upper boundary
            addq.b    #2,($FFFFF742).w ; and add 2 to the routine counter
    
    locret_704E:
            rts
    ; ===========================================================================
    
    loc_7050: ; the part with the lava pool and the falling blocks, all the way to the ending
            cmpi.w    #$E70,($FFFFF700).w ; check the screen's X coordinate
            bcs.s    locret_7072 ; if too low, branch
            move.w    #0,($FFFFF72C).w ; update level's actual top boundary
            move.w    #$500,($FFFFF726).w ; update level's target bottom boundary
            cmpi.w    #$1430,($FFFFF700).w ; check the screen's X coordinate
            bcs.s    locret_7072 ; if too low, branch
            move.w    #$210,($FFFFF726).w ; update the level's target bottom boundary
    
    locret_7072:
            rts    
    The Github version is the same, except with different labels for RAM addresses and subroutines.

    Each routine has a check to make sure that you can go back to previous routines, and its own series of instructions. However, not all routines may need them. In fact, once you reach routine 6, you can never go back to the previous routines, and no issue effectively occurs with the level (you can't even actually backtrack, seeing that your path is blocked by the spiked chandelier).

    A while ago, I had made a post detailing why you should use routines in your Dynamic Level Events, which also contains a fix to an issue that could result in a fatal crash when using them. Give it a read. It's quite lengthy, but it contains some valuable information.
     
    Last edited: Jun 22, 2022
    DeltaWooloo, Mr_ESN, maple_t and 5 others like this.