Work with Objects
From Sonic Retro
I decided I wanted to start some tutorials on objects because there aren't too many hacks with new objects. I figure I can start off with the really basic things then move on to more intricate subjects such as bosses. For now here is the first tutorial on very basic object concepts:
Lesson 1: The Basic Object
Section A: Overview
So, you want to start creating objects using the sonic engine? (We'll be using S1's but most of the ideas cover S1 and S2) Let's see firstly how most objects start out. I'll be using Hivebrain's 2005 disassembly for these examples:
; --------------------------------------------------------------------------- ; Object 18 - platforms (GHZ, SYZ, SLZ) ; --------------------------------------------------------------------------- Obj18: moveq #0,d0 move.b $24(a0),d0 move.w Obj18_Index(pc,d0.w),d1 jmp Obj18_Index(pc,d1.w)
First you see a little description of the object (commented out of course), followed by the object's code itself. It starts off by making sure there is currently no value in d0. After that it puts the value of the routine counter into d0 (whatever's in $24(a0) is in d0 such as 0, 2, 4, ect...). From there it moves the index's value with the value of d0 into d1, followed by jumping to the correct routine based on d1.
Section B: Routines
Routines are used to organize where certain code is and to be able to branch to those sections easily. Most objects used $24(a0) as the main routine counter and $25 as a secondary, but any scratch ram (ram not used by the object) can be used as a routine counter. The value in the routine counter needs to be even in order to work.
;=========================================================================== Obj18_Index: dc.w Obj18_Main-Obj18_Index ; Jumped to if #0 is in $24(a0) dc.w Obj18_Solid-Obj18_Index ; Jumped to if #2 is in $24(a0) dc.w Obj18_Action2-Obj18_Index ; Jumped to if #4 is in $24(a0) dc.w Obj18_Delete-Obj18_Index ; Jumped to if #6 is in $24(a0) dc.w Obj18_Action-Obj18_Index ; Jumped to if #8 is in $24(a0) ; ===========================================================================
So, when this section is run (as soon as the object is loaded):
Obj18_Main: ; XREF: Obj18_Index addq.b #2,$24(a0)
When it gets to the next rts instead of going to Obj18_Main again, it will skip over that code and go to Obj18_Solid. Be warned if you put an odd value into the routine counter it won't work properly or if you put a number greater then the amount of routines, your game will crash.
Section C: Displaying/Basic SSTs
Alright, so you know the basis of routines, now where should we go? Well, some of the basic building blocks in objects are what are called SSTs. Every object in S1 and S2 is allotted $40 bytes of RAM to do whatever they want with, but some SSTs are already used for certain things (the game engine checks a few of the object's SSTs once each frame). Let's see an example:
Obj18_Main: addq.b #2,$24(a0) ; adds to the routine so this isn't run again move.w #$4000,2(a0) ; moves #$4000 to the art tile's SST (it's 2 in S1 and S2) move.l #Map_obj18,4(a0) ; moves the mappings into the mapping's SST move.b #$20,$19(a0) ; width of object in pixels cmpi.b #4,($FFFFFE10).w ; check if level is SYZ bne.s Obj18_NotSYZ move.l #Map_obj18a,4(a0) ; SYZ specific code (use different mappings) move.b #$20,$19(a0) ; this really isn't needed since $19(a0) already has #$20 in it from the code before
What this basically does is define the width of the object in pixels and loads the starting art tile and palette and mappings. Down more in the code you'll see:
Obj18_NotSLZ: move.b #4,1(a0) ; use screen coordinates (such as the ones you see in debug mode) move.b #4,$18(a0) ; set priority (if other objects have a priority of a number less then 4 then the other object will be seen over this one if they interact) move.w $C(a0),$2C(a0) ; store a copy of the y coordinate ($C(a0) is y coordingate in S1 and S2 and $2C(a0) is scratch ram) move.w $C(a0),$34(a0) ; store another copy of the y coordinate move.w 8(a0),$32(a0) ; store a copy of the x coordinate move.w #$80,$26(a0) ; move #$80 into $26(a0) (to be used later)
This is a continuation of the loading of the object (the priority and using screen coordinates) and it saves the x and y pos and a value which will be used later. All you need to do to display an object it to fill in 1(a0), 2(a0), 4(a0) and jump to DisplaySprite.
Lesson 2: More Intricate Things With Objects
Section A: Movement
Welcome to lesson 2 of my object tutorials, in this section we're going to talk about other things that can be done with objects. So say you have an object displaying now and would like it to move. What you'll have to do is set its X and/or Y speed which are the SSTs $10(a0) and $12(a0) consecutively and then simply call a SpeedtoPos (or ObjectMove in S2)
move.w #-$40,$10(a0) ; make the object have a speed which moves it to the left slowly move.w #$400,$12(a0) ; make the object have a speed which moves it down quickly jmp SpeedToPos ; update the object's position (move the object)
If SpeedToPos is not called the object will stay immobilized. Also as a note, if you have a positive speed in the Y speed SST, the object will move down and if you have a negative speed in the Y speed SST, the object will move up. This is just a simple explanation of movement and I will eventually cover things such as how to make objects move in circular motions, but for now it's not necessary.
Section B: Timers
Another pretty basic idea used quite frequently is timers. It's what the GHZ boss uses to turn around and go back and forth. To use a timer what you'll have to do is take an unused SST and make sure it's set aside. In this example code, let's use $38(a0). What you want to do is somewhere before the timer starts (say the main loading code) where the code won't be used again is fill this with a number:
ObjXX_Main: move.w #$100,$38(a0)
When you come to an area where you want to start counting down, you'll want to set up a code that gets repeated until the time is up, as in:
ObjXX_CountDown: sub.w #1,$38(a0) ; subtracts from the timer beq.s ObjXX_Next ; tests if timer has hit 0 rts
Now, in ObjXX_Next you can increase the routine, change the speed/reverse it and you can reset the timer there as well so that it keeps changing speed:
ObjXX_Next: neg.w $10(a0) move.w #$100,$38(a0) rts
Lesson 3: Making MappingsOk, in this section we’re gonna cover the aspects of making object mappings (Note: this is written for Sonic 1 primarily, however Sonic 2 and to an extend Sonic 3 (&K) mappings will be quite similar bar a few changes), so what are mappings? Mappings are a way of presenting art tiles on an object in a certain way, now going back to “Section C: Displaying/Basic SSTs” in “Lesson 1” you’ll notice the line:
move.l #Map_obj18,4(a0) ; moves the mappings into the mapping's SST
So, we need to make this routine and the mappings for it, now it doesn’t really matter where you put this but for now let’s put it directly under our object code (it just makes sense this way).
Next we need an index for this routine similar to the one explain in “Lesson 1”
Map_obj18: dc.w ObjectMap_00-Map_obj18 dc.w ObjectMap_01-Map_obj18 dc.w ObjectMap_02-Map_obj18 ObjectMap_00: dc.b $00 ObjectMap_01: dc.b $00 ObjectMap_02: dc.b $00
And now to make the mappings, you’ll notice three sections that look like “ObjectMap_00: dc.b $00” under this is where our set of mappings are going to go, now let me just set one out for you and explain what’s what:
ObjectMap_00: dc.b $01 dc.b $YY, $SS, $VV, $VV, $XX
So, you’ll notice that first of all the $00 that was previously there is now a $01, this is because a line has been added below it “dc.b $YY, $SS, $VV, $VV, $XX” that is one sprite, if you had two of these below, then you would put $02 at the top there, that top value indicates the number of sprites to use in these mappings, we’re only gonna use one for now, so $01 it is.
Now to the actual mapping, you notice I’ve put YY SS VV VV and XX, this is just to explain what each byte does, so lets take a look at YY, YY sets how many pixels up or down to present the tiles from where the object is, it is signed so negative values from FF down to 80 will move the sprite up while positive values from 00 to 7F will move the sprite down, for example:
ObjectMap_00: dc.b $01 dc.b $08, $SS, $VV, $VV, $XX
This will move the tile mappings down 8 pixels from where the object is, so if the object is on the Y axis of $0200, the tile mappings will present on the Y axis of $0208.
Next let’s skip over to XX, this is the same as YY except it’s on the X axis (left or right) not the Y axis, so:
dc.b $YY, $SS, $VV, $VV, $08
This will move the tile mappings right 8 pixels from where the object is, so if the object is on the X axis of $0340, the tile mappings will present on the X axis of $0348.
Ok, now to SS, this is the "shape" so to speak, it's how the tiles should present themselves (in other words how the block is made), now lets say we have 16 tiles in VRam (from 00 to 0F), depending on what code is in SS will change how those 16 tiles are stacked on each other:
So if we put $00 the tiles will map:
If we put $01 the tiles will map:
If we put $02:
If we put $03:
If we put $04:
If we put $05:
If we put $06:
If we put $07:
If we put $08:
If we put $09:
If we put $0A:
If we put $0B:
If we put $0C:
If we put $0D:
If we put $0E:
If we put $0F:
That should be self explanatory for you:
dc.b $YY, $SS, $VV, $VV, $XX
Next let's look at VV VV, this is actually a word of data and is the V-Ram location to read the tiles for the sprite, it also has specific settings (if it is flipped, mirrored, and or uses a certain palette line, and if it's high or low plane), this is a "Map ID" and is used for map planes too, let's break VVVV up into sections of XYZZ:
YZZ is broken into bits: ABBB BBBB BBBB
B = the tile ID (or V-Ram location divided by 20 if you'd prefer) A = the mirror flag, if clear (bit 0), the "map tile"/"sprite" is normal, if set (bit 1), the "map tile"/"sprite" is mirrored.
X is broken into bits: PCCF
P = the plane flag, if clear (bit 0), the "map tile"/"sprite" is low plane, if set (bit 1), the "map tile"/"sprite" is high plane. CC = the palette line flag:
00 = line 0 01 = line 1 10 = line 2 11 = line 3
F = the flip flag, if clear (bit 0), the "map tile"/"sprite" is normal, if set (bit 1), the "map tile"/"sprite" is flipped.
And there you have it, that’s how to map art on an object.
Lesson 4: Working with animations
After getting an object working and individual mappings made, you may get the idea of wanting to animate several individual mappings in a specific order at a certain speed, luckily there just so happens to be routines in the engine to use and deal with animation scripts, in this section; we take a look at that.
As described in Lesson 1 by Malevolence, the object may call a routine known as “DisplaySprite”, in order to use animation scripts, just before the instruction calling “DisplaySprite”, the following will be used:
lea (Ani_obj18).l,a1 ; load animation script address to a1 jsr AnimateSprite ; run routine to change the map frame ID using the scripts
This will ensure that the script is ran through “before” the display routine is called.
The next thing is the animation scripts and their format:
Ani_obj18: dc.w ObjectAni_00-Ani_obj18 dc.w ObjectAni_01-Ani_obj18 dc.w ObjectAni_02-Ani_obj18 dc.w ObjectAni_03-Ani_obj18 ObjectAni_00: dc.b $02,$00,$01,$02,$FF ObjectAni_01: dc.b $02,$00,$01,$02,$FE,$02 ObjectAni_02: dc.b $02,$02,$01,$00,$02,$02,$00,$01,$FF ObjectAni_03: dc.b $02,$00,$01,$02,$FD,$01 even
Just like the mappings, the animations also have an index, and each entry is selected by the value in $1C(a-) of the object’s ram, so if 02 was moved to $1C(a-) of the object, then “ObjectAni_02” script would be used, if 00 was moved to $1C(a-), then “ObjectAni_00” script would be used.
In every script, the first byte is the delay (or better yet “speed”) of the animation, it sets how many frames to show the same map frame before it should move on to the next one, I’ve set all of them to 02, meaning it always waits 2 frames before it shows the next frame in the script. 00 is the fastest while FF is the slowest.
Every byte “after” the first one is the animation itself, if it is a positive number (from 00 to 7F) then it is a map frame number to use, for example, our mappings used in this tutorial, if the number is 00, then the mappings “ObjectMap_00” would be displayed, if the number is 02, then the mappings “ObjectMap_02” would be displayed.
So in the script “ObjectAni_02”, mappings 02 01 00 02 02 00 then 01 would be displayed in that order showing for over 2 frames each.
If however the number is negative (from 80 to FF) then it is a “flag”, flags are used to set how the animation will change, loop, stop, etc, However there are only 6 flags used:
- FF, this resets the entire animation script, looping all of it over and over again,
- FE, this jumps back through the script a certain number of bytes, for example in script “ObjectAni_01” you’ll see an “$FE,$02” indicating that it’ll move back 2 bytes in the script to the “$01” and loop.
- FD, this sets the next animation script to use, for example in script “ObjectAni_03” you’ll see “$FD,$01” indicating to move 01 to $1C(a-) of the object, this will set “ObjectAni_01” to be ran next.
- FC, this increases the routine counter $24(a-) by 02.
- FB, this resets the “sub”-routine counter $25(a-) to 00.
- FA, this increases the “sub”-routine counter $25(a-) by 02.
That is all for animation scripts.
Note by DelayHacks
If you wanted to create object at the level, you need to add this object to objects' pointers (_inc/Object Pointers.asm):
dc.l Obj01, ObjectFall, ObjectFall, ObjectFall
As you can see, there is free slots for objects, you can replace "ObjectFall" with your object's routine:
dc.l Obj01, ObjXX, ObjectFall, ObjectFall
If you want to create object as the gameplay starts, you need to find free slots for object in Objects's ram (aka RAM from $D000 to $D800, every object has a $3F bytes).
After you find one, you need to add this line into sonic's code:
move.b #$XX,$FFFFXXXX.w ; create object
replace $XX with your object's id, also replace XXXX with RAM you find.