Sonic and Sega Retro Message Board: "Sub Sprites" in Sonic 1 - Sonic and Sega Retro Message Board

Jump to content

Hey there, Guest!  (Log In · Register) Help
Page 1 of 1
    Locked Forum

"Sub Sprites" in Sonic 1

#1 User is offline Ralakimus 

Posted 08 September 2018 - 06:07 PM

  • Posts: 292
  • Joined: 16-April 13
  • Gender:Male
"What are these 'sub sprites' you speak of" you may ask. In Sonic 2 and 3K, if you set a bit in an object's "render flags", you were able to sacrifice a bit of the object's RAM to display multiple sprites in one object. With this, you could save quite a number of object slots.

For example, in Sonic 1, each log in a bridge was an individual object, so it would potentially use up several object slots, depending on how long the bridge was. However, in Sonic 2, it would only use 1 or 2 objects and just draw multiple sprites for them.

Posted Image Posted Image

However, this is not a feature in Sonic 1. So, here, I will show you how you can implement such a feature. For this, I will primarily be using Sonic 2 as an example for simplicity's sake. With this, it would also open up the possibility of porting other objects from Sonic 2 and 3K to Sonic 1 without needing to make them use more object slots (such as the invincibility stars). You can also optimize certain objects, such as the swinging chained platforms or the Green Hill boss' swinging ball, by making the chains sub sprites (which is what Sonic 2 does).

I will be using the Sonic 2 GitHub disassembly and the Sonic 1 GitHub disassembly (the sprite rendering routine (BuildSprites) in that isn't documented well as of September 8, 2018, so currently it's very similar to the 2005 Hivebrain version, even has the same labels).

The purpose of this tutorial is to just port the sub sprite functionality to Sonic 1. There will be no changes to the mappings format and no other features or optimizations will be implemented.

How Does It Work?​
In Sonic 2, if you set bit 6 of the render flags SST in an object, it'll enable sub sprites for the object. For that object, it will render a "main sprite" and then its sub sprites. To set it all up, you set the main sprite's size and frame ID, and then the number of sub sprites you want to render. Then, you can set the sub sprites' position and frame ID.

These specific properties you set DO override some standard object SSTs. Specifically, the last byte in x_pos, the last 2 bytes in y_pos, x_vel , y_vel, intertia (for Sonic and Tails), y_radius, priority, anim_frame, anim, anim_frame_duration, collision_property, status, routine, and subtype. What is overridden really depends on the amount of sub sprites you have set to display (for example, only having 2 sub sprites will not override anim_frame_timer and onward).

Take a look here:

mainspr_mapframe    = $B
mainspr_width        = $E
mainspr_childsprites     = $F    ; amount of child sprites
mainspr_height        = $14
sub2_x_pos        = $10    ;x_vel
sub2_y_pos        = $12    ;y_vel
sub2_mapframe        = $15
sub3_x_pos        = $16    ;y_radius
sub3_y_pos        = $18    ;priority
sub3_mapframe        = $1B    ;anim_frame
sub4_x_pos        = $1C    ;anim
sub4_y_pos        = $1E    ;anim_frame_duration
sub4_mapframe        = $21    ;collision_property
sub5_x_pos        = $22    ;status
sub5_y_pos        = $24    ;routine
sub5_mapframe        = $27
sub6_x_pos        = $28    ;subtype
sub6_y_pos        = $2A
sub6_mapframe        = $2D
sub7_x_pos        = $2E
sub7_y_pos        = $30
sub7_mapframe        = $33
sub8_x_pos        = $34
sub8_y_pos        = $36
sub8_mapframe        = $39
sub9_x_pos        = $3A
sub9_y_pos        = $3C
sub9_mapframe        = $3F
next_subspr       = $6

One thing that Sonic 2 does to avoid issues with SSTs is to load an object whose purpose is only to hold these sub sprite properties and to display the sprites, while the main object handles all the programming and stuff, and when it needs to handle sprites, it sets the properties in the other object. Let's take a look at Obj15 in Sonic 2 for example. It starts with this:

    btst    #6,render_flags(a0)
    bne.w    +
    moveq    #0,d0
    move.b    routine(a0),d0
    move.w    Obj15_Index(pc,d0.w),d1
    jmp    Obj15_Index(pc,d1.w)
; ---------------------------------------------------------------------------
    move.w    #$200,d0
    bra.w    DisplaySprite3

It checks if the "sub sprite enable" flag is set and if so, make it so that it just runs DisplaySprite3, which is a routine that takes a precalculated offset for the object display queue instead of using the priority SST. Like I explained, the priority SST can be overridden by sub sprite data, so this routine exists, so it doesn't have to use the SST. However, if that flag is not set, it will then go to the normal Obj15 code, where upon initialization, it will load an Obj15 with that flag set.

Depending on how you use your SSTs, you may not need to do something like that, but that is an option just in case.

So, now let's go ahead an implement this, shall we? First thing you should do is define the sub sprite SSTs from above somewhere in your disassembly.

Now, once you have the SSTs defined, let's get into the code. The magic really happens in "BuildSprites" (it's the same in both Sonic 1 and 2 disassemblies), so let's go there. It's in the main ASM file for all disassemblies. Do note that you'll want to change any RAM names or SST names to the ones defined in your disassembly (i.e. render_flags -> obRender for S2 GitHub to S1 GitHub).

Also keep in mind that ASM68K does not support temporary labels (those "+" or "-" labels you may see occasionally in the Sonic 2 disassembly). You'll need to replace them with actual labels.

Here is where we need to make our first change:

        move.b    obRender(a0),d0
        move.b    d0,d4
        andi.w    #$C,d0
        beq.s    loc_D6DE

This code grabs the render flags and does flag checks. In Sonic 2, this piece of code looks like this:

    move.b    render_flags(a0),d0
    move.b    d0,d4
    btst    #6,d0    ; is the multi-draw flag set?
    bne.w    BuildSprites_MultiDraw    ; if it is, branch
    andi.w    #$C,d0    ; is this to be positioned by screen coordinates?
    beq.s    BuildSprites_ScreenSpaceObj    ; if it is, branch

As you may see, Sonic 2 adds 2 additional lines between the ANDI instruction and the MOVE instruction. Those 2 lines of code check the "sub sprite enable" flag and branches to "BuildSprites_MultiDraw" if it is set. You'll want to add that in to Sonic 1's BuildSprites.

In Sonic 2, "BuildSprites_MultiDraw" is located right after before the routine that copies mapping data to the sprite buffer. In Sonic 1, that routine is called "sub_D750", so right before that routine, create the "BuildSprites_MultiDraw" label.

And now for the actual sub sprite rendering code. In Sonic 2, it starts off with:

    move.l    a4,-(sp)
    lea    (Camera_X_pos).w,a4
    movea.w    art_tile(a0),a3
    movea.l    mappings(a0),a5
    moveq    #0,d0

If you notice, it store the address of start of the main camera's position data in a4. However, if we take a look at how Sonic 1 handles grabbing the camera position data, we'll see this:

movea.l    BldSpr_ScrPos(pc,d0.w),a1

Which is located directly after the code that grabs the render flags and checks it. What it's doing is choosing which camera in which the object will be drawn relative to. Which camera it picks depends on bits 2 and 3 in the render flags, which is exactly what that piece of code from before was doing (with both bits cleared just making it branch to loc_D6DE, meaning it should not be relative to any camera).

The main issue here is that in Sonic 2, if an object is set to handle sub sprites, it is forced to be drawn relative to a camera, as it does not check those 2 bits in the render flags. For the sake of simplicity and staying true to the original functionality in Sonic 2, we will not handle multiple camera rendering for sub sprites.

Next up, we have this bit of code. Go ahead and implement this.

    ; check if object is within X bounds
    move.b    mainspr_width(a0),d0    ; load pixel width
    move.w    x_pos(a0),d3
    sub.w    (a4),d3
    move.w    d3,d1
    add.w    d0,d1
    bmi.w    BuildSprites_MultiDraw_NextObj
    move.w    d3,d1
    sub.w    d0,d1
    cmpi.w    #320,d1
    bge.w    BuildSprites_MultiDraw_NextObj
    addi.w    #128,d3

Here, it checks the main sprite's X position to see if it's on screen horizontally. You may notice it's very similar to this in Sonic 1:

        move.b    obActWid(a0),d0
        move.w    obX(a0),d3
        sub.w    (a1),d3
        move.w    d3,d1
        add.w    d0,d1
        bmi.w    loc_D726
        move.w    d3,d1
        sub.w    d0,d1
        cmpi.w    #$140,d1
        bge.s    loc_D726
        addi.w    #$80,d3

The main difference really are the labels it branches to and the fact that it doesn't use the regular width SST ("obWidth"), but rather "mainspr_width".

Next up is this. Go ahead and implement this.

    ; check if object is within Y bounds
    btst    #4,d4
    beq.s    +
    moveq    #0,d0
    move.b    mainspr_height(a0),d0    ; load pixel height
    move.w    y_pos(a0),d2
    sub.w    4(a4),d2
    move.w    d2,d1
    add.w    d0,d1
    bmi.w    BuildSprites_MultiDraw_NextObj
    move.w    d2,d1
    sub.w    d0,d1
    cmpi.w    #224,d1
    bge.w    BuildSprites_MultiDraw_NextObj
    addi.w    #128,d2
    bra.s    ++
    move.w    y_pos(a0),d2
    sub.w    4(a4),d2
    addi.w    #128,d2
    andi.w    #$7FF,d2
    cmpi.w    #-32+128,d2
    blo.s    BuildSprites_MultiDraw_NextObj
    cmpi.w    #32+128+224,d2
    bhs.s    BuildSprites_MultiDraw_NextObj

Basically like the previous set of code, but it checks the Y position. One main difference, however, is that it handles another flag from the render flags, bit 4. This bit, if clear, makes it so that it doesn't take into account the height of the sprite/object when checking if it's onscreen vertically or not. Again, you may notice it's similar to this piece of code from Sonic 1:

        btst    #4,d4
        beq.s    loc_D6E8
        moveq    #0,d0
        move.b    obHeight(a0),d0
        move.w    obY(a0),d2
        sub.w    4(a1),d2
        move.w    d2,d1
        add.w    d0,d1
        bmi.s    loc_D726
        move.w    d2,d1
        sub.w    d0,d1
        cmpi.w    #$E0,d1
        bge.s    loc_D726
        addi.w    #$80,d2
        bra.s    loc_D700
        move.w    obY(a0),d2
        sub.w    obMap(a1),d2
        addi.w    #$80,d2
        cmpi.w    #$60,d2
        blo.s    loc_D726
        cmpi.w    #$180,d2
        bhs.s    loc_D726


Pretty much the same deal as the previous set of code, different labels and the fact it uses "mainspr_height" instead of the normal height SST ("obHeight"). And if you notice, Sonic 1 does not have a "andi.w #$7FF,d2" instruction for this kind of code. If you really want to keep true to Sonic 1, go ahead and remove that instruction from the piece of code from Sonic 2.

After that, it draws the main sprite with this piece of code:

    moveq    #0,d1
    move.b    mainspr_mapframe(a0),d1    ; get current frame
    beq.s    +
    add.w    d1,d1
    movea.l    a5,a1
    adda.w    (a1,d1.w),a1
    move.w    (a1)+,d1
    subq.w    #1,d1
    bmi.s    +
    move.w    d4,-(sp)
    bsr.w    ChkDrawSprite    ; draw the sprite
    move.w    (sp)+,d4

Keep in mind that there really isn't a "ChkDrawSprite" in Sonic 1, and the fact that Sonic 1 handles sprite limits differently than Sonic 2. So, a change you'll want to make is to create a label in the sub_D750 routine between "movea.w obGfx(a0),a3" and "btst #0,d4", since in Sonic 2, if the sprite limit has not been reached, it would branch to to that "btst #0,d4". Once you have made the label, change the branch to "ChkDrawSprite" to that new label.

Another change that we need to make to that piece of code is to change how it gets the number of sprites to draw. In Sonic 2, the number of sprite pieces in a frame is a word, but in Sonic 1, it's a byte, so you'll want to change the ".w" extension in "move.w (a1)+,d1" to a ".b" extension. To keep true to how Sonic 1 handles this kind of thing, apply the same change to "add.w d1,d1" and "subq.w #1,d1".

For reference, take a look at this piece of code from Sonic 1:

        move.b    obFrame(a0),d1
        add.b    d1,d1
        adda.w    (a1,d1.w),a1
        move.b    (a1)+,d1
        subq.b    #1,d1
        bmi.s    loc_D720

After all that, we can finally render some sub sprites!

So, it begins with:

    ori.b    #$80,render_flags(a0)    ; set onscreen flag
    lea    sub2_x_pos(a0),a6
    moveq    #0,d0
    move.b    mainspr_childsprites(a0),d0    ; get child sprite count
    subq.w    #1,d0        ; if there are 0, go to next object
    bcs.s    BuildSprites_MultiDraw_NextObj

Here, it sets the sprite as "on screen", like it would normally, and then it prepares to get the sub sprite data, and then gets the number of sub sprites to draw.

Next up is this:

-    swap    d0
    move.w    (a6)+,d3    ; get X pos
    sub.w    (a4),d3
    addi.w    #128,d3
    move.w    (a6)+,d2    ; get Y pos
    sub.w    4(a4),d2
    addi.w    #128,d2
    andi.w    #$7FF,d2
    addq.w    #1,a6
    moveq    #0,d1
    move.b    (a6)+,d1    ; get mapping frame
    add.w    d1,d1
    movea.l    a5,a1
    adda.w    (a1,d1.w),a1
    move.w    (a1)+,d1
    subq.w    #1,d1
    bmi.s    +
    move.w    d4,-(sp)
    bsr.w    ChkDrawSprite
    move.w    (sp)+,d4
    swap    d0
    dbf    d0,-    ; repeat for number of child sprites

This goes through each available sub sprite, sets it up properly, and then draws it. You may notice the "ChkDrawSprite" there. Apply the same change we made to the previous set of code that had that. Also, here:

    move.b    (a6)+,d1    ; get mapping frame
    add.w    d1,d1
    movea.l    a5,a1
    adda.w    (a1,d1.w),a1
    move.w    (a1)+,d1
    subq.w    #1,d1
    bmi.s    +

This may looks familar. Apply the same changes as before here, too.

And finally, it ends with this:

    movea.l    (sp)+,a4
    bra.w    BuildSprites_NextObj

Which just goes back to the main sprite rendering loop. "BuildSprites_NextObj" is called "loc_D726" in Sonic 1.

But, we are not quite finished yet!
I did mention a "DisplaySprite3" before, did I not?

In Sonic 2, it looks like this:

    lea    (Sprite_Table_Input).w,a1
    adda.w    d0,a1
    cmpi.w    #$7E,(a1)
    bhs.s    return_16542
    addq.w    #2,(a1)
    adda.w    (a1),a1
    move.w    a0,(a1)


Go ahead and implement this routine under the "DisplaySprite1" routine (located in "_incObj/sub DisplaySprite.asm") in the GitHub disassembly, or "DisplaySprite2" routine in the 2005 Hivebrain disassembly.

And that should be it!

So, before we say we are all done and whatnot, you may want to test that you implemented this right. So, I have created a test object here. Here's the code:

; ---------------------------------------------------------------------------
; Test object that tests out sub sprites
; ---------------------------------------------------------------------------

        btst    #6,1(a0)                ; Is this object set to render sub sprites?
        bne.s    OT_SubSprs                ; If so, branch
        moveq    #0,d0
        move.b    $24(a0),d0                ; Go to current object routine
        move.w    OT_Routines(pc,d0.w),d0
        jmp    OT_Routines(pc,d0.w)

        move.w    #$200,d0                ; Display sprites
        jmp    DisplaySprite3

; ---------------------------------------------------------------------------

        dc.w    OT_Init-OT_Routines
        dc.w    OT_Main-OT_Routines

; ---------------------------------------------------------------------------
; Initialization
; ---------------------------------------------------------------------------

        addq.b    #2,$24(a0)                ; Set as initialized
        jsr    FindFreeObj                ; Find a free object slot
        bne.s    OT_NoFreeObj
        move.w    a1,$3E(a0)                ; Set as child object
        move.b    #[ID],(a1)                    ; Load test object
        move.b    #%01000100,1(a1)            ; Set to render sub sprites
        move.w    #$780,2(a1)                ; Base tile ID
        move.l    #Map_Sonic,4(a1)            ; Mappings
        move.b    #$30,mainspr_width(a1)            ; Set main sprite width
        move.b    #$30,mainspr_height(a1)            ; Set main sprite height
        move.b    #4,mainspr_childsprites(a1)        ; Set number of child sprites
        move.w    8(a0),8(a1)                ; Set position
        move.w    $C(a0),$C(a1)


; ---------------------------------------------------------------------------
; Main
; ---------------------------------------------------------------------------

        movea.w    $3E(a0),a1                ; Get child object

        moveq    #0,d6
        move.b    ($FFFFD01A).w,d6            ; Get frame to use
        move.b    d6,mainspr_mapframe(a1)            ; Set main sprite frame

        moveq    #0,d5
        move.b    mainspr_childsprites(a1),d5        ; Get number of sub sprites
        subq.b    #1,d5
        bmi.s    OT_NoSubSprs                ; If there are none, branch
        lea    sub2_x_pos(a1),a2            ; Get sub sprite data

        move.b    $26(a0),d0                ; Get sine and cosine of the current angle
        jsr    CalcSine
        asr.w    #3,d0                    ; Get Y position
        add.w    $C(a0),d0
        asr.w    #3,d1                    ; Get X position
        add.w    8(a0),d1
        move.w    d1,(a2)+                ; Set X position
        move.w    d0,(a2)+                ; Set Y position
        move.w    d6,(a2)+                ; Set map frame
        addi.b    #$40,$26(a0)                ; Next angle to use
        dbf    d5,OT_SetSubSprs            ; Loop until every sub sprite is set

        addq.b    #1,$26(a0)                ; Increment angle

In the 2005 Hivebrain disassembly, "FindFreeObj" is "SingleObjLoad" and change "[ID]" to whatever object ID you set this object to.

Go ahead and put it in your disassembly and then spawn it in a level. You should see something like this:

If you do, great!, You are all set! If not, go back and see what you need to fix. Any "branch out of range" errors or "illegal value" errors you may get are easy to fix and solutions for them are on this very site somewhere.

Enjoy your sub sprites.
This post has been edited by Ralakimus: 10 September 2018 - 09:17 AM

#2 User is offline KingofHarts 

Posted 10 September 2018 - 02:04 AM

  • Posts: 1610
  • Joined: 07-August 10
  • Gender:Male
  • Project:Project Sonic 8x16
  • Wiki edits:1
Well done! I reckon something like this could also be done for monitors to have the icons come from the box instead of having another object just for the icons (Those objects handle the powerup itself IIRC, but that code can easily be integrated into the monitor object proper)

Page 1 of 1
    Locked Forum

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users