The following only describes how Sonic collides and interacts with solid tiles. Solid objects, such as Monitors, Moving Platforms, and Blocks each have their own collision routines with Sonic, and don't necessarily behave exactly the same as the tiles do.
What are solid tiles? While there are often solid objects in Sonic zones, the zone itself would require far too much object memory if the environment were contructed entirely of solid objects, each with their own 64 bytes of RAM. A clever shortcut is used - the zone is constructed out of tiles, anyway, so all that needs be done is have each tile know whether or not it is solid.
You may know that zones are broken down into 128x128 pixel chunks (or 256x256 pixel chunks in Sonic 1 and Sonic CD), which are in turn broken into 16x16 pixels tiles, which are again in turn broken into even smaller 8x8 pixel tiles. All of the solidity magic happens with the 16x16 tiles, so those are the only ones we'll be interested in throughout this guide.
It's Sonic's collisions and interactions with these solid tiles that makes up his basic engine. They dictate how he handles floors, walls, ceilings, slopes, and loops. Because this is such a large subject, and so complex, this guide is more proximate than other Sonic Physics Guides, but I've kept speculation to a minimum.
Assuming the ground level to be at a Y position of $02E0, Sonic stands atop it at $02CC. That's 20 pixels above ground level.
Sonic has to stop when he bumps into walls. This can be achieved by checking for a collision with a line. The sensor line shouldn't be at his Y position, but a little lower - otherwise it won't "notice" shorts steps. It can't be too low down, though, or it will "notice" ramps and curves, which Sonic shouldn't push against. Positioning it at Sonic's Y+4 is good, because there usually aren't stairs lower than 16 pixels high (when there are, they seem to be made out of solid objects, not tiles).
How wide should the sensor line be?
Assuming the wall's left side to be at an X position of $02C0, Sonic can't get closer than $02B5. Assuming the wall's right side to be at an X position of $033F, Sonic can't get closer than $034A. That's 11 pixels difference, in either direction.
Thus the sensor line should be 20 pixels wide, stretching from Sonic's X-10 to X+10. Anytime it detects a solid tile, Sonic should be "popped out", set to the edge of the tile minus (or plus) 11, and his ground speed set to 0. (He can't be popped out by only 10, because then a point at his X+10 would still lie within the edge pixel of the tile. This would register a continuous collision, and he'd stick to the wall.)
Though the tile's edge minus Sonic's position might be 11, there are only 10 free pixels between Sonic's centre and the tile's edge. The eleventh pixel away is the tile's edge itself. So Sonic is effectively only 20 pixels wide.
Sonic also has to be able to run off of ledges. It wouldn't do to just keep walking like Wile E. Coyote, not noticing that there's nothing beneath him.
This means Sonic has to check to check for Solid tiles beneath him, as well. This can be achieved with two more sensor lines, pointing downward. One (A) should be on Sonic's left side, at X-9. The other (B) should be on his right, at X+9. They should start at his Y position and extend at least 16 pixels below his feet at ground level, which is at Y+20 (but not too far, or he'll pop down when descending low steps and stairs, which you don't want).
If both the A and B sensors detect no solid tiles, Sonic will "fall" - a flag will be set telling the engine he's now in the air.
You may remember that Sonic is 20 pixels wide when pushing into walls. But these ground sensors are only 18 pixels apart. So Sonic is skinnier by 2 pixels when running off of ledges than when bumping into walls.
One nice touch is that Sonic goes into a balancing animation when near to the edge of a ledge. This only happens when he's stopped (his ground speed is 0).
How does the engine know? It's simple - any time only one of the ground sensors is activated, Sonic must be near a ledge. If A is active and B is not, the ledge is to his right. If vice versa, the ledge is to his left.
But if Sonic began balancing the instant one of the sensors found nothing, he'd do it too "early", and it would look silly. So it only happens when only one sensor is active, and his X position is greater than the edge of the solid tile found by the active sensor.
Assuming the right edge of the ledge to be $0A5F, Sonic will only start to balance at $0A60. He'll fall off at $0A69, when both sensors find nothing.
In Sonic 2, Sonic 3, and Sonic & Knuckles, Sonic has yet a third balancing animation, for when he's even further out on the ledge. Assuming the same values as above, this would start when he's at $0A66.
Note: While balancing, certain abilities are not allowed (ducking, looking up, spindashing, etc). In Sonic 3 & Knuckles, the player is still allowed to duck and spindash (but not to look up) when balancing on ground, but not when balancing on an object.
Sonic the Hedgehog was one of the first games to use curved surfaces and even entire 360 degree loops. Most other games of the era composed their environments entirely of blocks (and the occasional ramp).
The ability to cope with smoothly shaped environments is one of the fundamental aspects of the Sonic games' novelty and appeal. Unfortunately, it is also perhaps the most difficult and complex aspect to recreate in a fan game.
So how does it work?
Any time the A or B sensors find a solid tile, they return the height of that tile.
How is the height of the tile found?
Each tile has a value associated with it that references a mask stored in memory. Each mask is simply an array of 16 height values that range from $00 to $10, and an angle value.
This height mask, for example, has the height array $00 00 01 02 02 03 04 05 05 06 06 07 08 09 09 09, and the angle $E8.
Which value of the height array is used? Subtract the tile's X position from the sensor's X position. The result is the index of the height array to use.
If the height value found is $10, then the sensor has to check for another tile above the first one found, and search for that one's height value.
Whichever sensor finds the highest height, Sonic's Y is set to that height minus 20 pixels. His angle is also set to the angle of the solid tile that returned the highest height.
When no solid tile is found by a sensor, foot level (Y+20) is returned by default.
Unfortunately, there are a couple of annoying bugs in the original engine because of this method.
If Sonic stands on a slanted ledge, one sensor will find no tile, and return a height of foot level. This causes Sonic to be set to the wrong position.
Sonic raises up with the B sensor as he moves right. When B drops off the ledge, Sonic defaults to the level of the A sensor. Then he raises up with the A sensor as he moves further right. So he'll move up, drop down, and move up again as he runs off the ledge.
There are few areas where this is noticeable, but it's true in all the Genesis/Mega Drive titles, and it's pretty tacky.
The B sensor starts climbing down the ramp on the right, but Sonic still defaults to the level of the previous ramp found by the A sensor. Because these ramps are usually shallow, this only causes him to dip down in the middle by about 1 pixel.
But that's not all. Because the highest sensor is the one Sonic gets the angle from, even though it looks like he should be considered to be at the angle of the ramp on the right (because he's closer to it), he'll still have the angle of the ramp on the left. When you jump, he'll jump at that angle, moving backward, not forward like you'd expect.
Well, that's all very well and good for having Sonic move smoothly over terrain with different heights. But's that's not all there is to the engine. Sonic's speed has to be attenuated by angled ground in order to be realistic.
There are two ways in which Sonic's speed is affected on angles. The first makes sure that he doesn't traverse a hill in the same amount of time as walking over flat ground of an equal width. The second slows him when going uphill, and speeds him up when going downhill. Let's look at each of these in turn.
If Sonic were a simple platformer that required nothing but blocks, you would only need two speed variables: X speed (Xsp) and Y speed (Ysp), the horizontal and vertical components of Sonic's velocity. Acceleration (acc), deceleration (dec), and friction (frc) are added to Xsp; jump velocity (jmp) and gravity (grv) are added to Ysp (when Sonic is in the air).
But when slopes are involved, while Sonic moves along a slope, he's moving both horizontally and vertically. This means that both Xsp and Ysp have a non-zero value. Simply adding acc, dec, or frc to Xsp no longer works; imagine Sonic was trying to run up a wall - adding to his horizontal speed would be useless, because he needs to move upward.
The trick is to employ a third speed variable (as the original engine does), so let's call it Ground speed (Gsp). This is the speed of Sonic along the ground, disregarding the angle altogether. acc, dec, and frc are applied to Gsp, not Xsp or Ysp.
While on the ground, Xsp and Ysp are derived from Gsp every step before Sonic is moved. Perhaps a pseudo-code example is in order:
Xsp = Gsp*cos(angle); Ysp = Gsp*-sin(angle); X += Xsp; Y += Ysp;
No matter what happens to the angle, Gsp is preserved, so the engine always knows what speed Sonic is "really" moving at.
By this point, Sonic ought to be able to handle any hills with an accurate velocity. But he still needs to be slowed when going uphill, and sped up when up going downhill.
Fortunately, this is simple to achieve - with something called the Slope Factor (slp). Just add slp*sin(angle) to Gsp at the beginning of every step. The value of slp is always 0.125 ($0020) when running, but not so when rolling. When Sonic is rolling uphill (the sign of Gsp is not equal to the sign of sin(angle)), slp is 0.078125 ($001E). When Sonic is rolling downhill (the sign of Gsp is equal to the sign of sin(angle)), slp is 0.3125 ($0050).
Note: In Sonic 1, it appears that slp doesn't get added if Sonic is stopped, and in his standing/waiting cycle. But in Sonic 3 & Knuckles, slp seems to be added even then, so that Sonic can't stand on steep slopes - they'll force him to walk backward down them.
Jumping is also affected by the angle Sonic is at when he does it. He can't simply set Ysp to jmp - he needs to jump away from the angle he's standing on. Instead, both Xsp and Ysp must be set to jmp, using cos() and sin() to get the right values.
Xsp -= jmp*sin(angle); Ysp -= jmp*cos(angle);
So Sonic can run over hills and ramps and ledges, and all that's great. But it's still not enough. He can't make his way from the ground to walls and ceilings without more work.
Why not? Well, because the A and B ground sensors check straight downward, finding the height of the ground. There's just no way they can handle the transition to walls when everything is built for moving straight up and down on the Y axis.
How can we solve this? By using four different modes of movement. This will take a little explaining.
It seems pretty reasonable to assume that, because Sonic can traverse ground in 360 degrees, the engine handles all 360 degrees in much the same way. But, in fact, the engine splits the angles into four quadrants, greatly simplifying things.
To better understand what I'm talking about, imagine a simpler platformer without full loops, just a few low hills and ramps. All the character would need to do is, after moving horizontally, move up or down until they met the level of the floor. The angle of the floor would then be measured. The angle would be used to attenuate Gsp, but nothing more. The character would still always move horizontally, and move straight up and down to adhere to floor level.
This is much like how Sonic does things. Only, when the angle gets too steep, Sonic switches "quadrant", moving from Floor mode to Right Wall mode (to Ceiling mode, to Left Wall mode, and back around to Floor mode, etc). At any one time, in any one mode, Sonic behaves like a simpler platformer. The magic happens by combining all four modes, and cleverly switching between them smoothly.
So how and when does Sonic switch mode?
When in Floor mode, and an angle steeper than $E0 is detected, the engine switches into Right Wall mode. Everything is basically the same, only the sensors check to the right instead of downward, and Sonic is moved to "floor" level horizontally instead of vertically.
Now that he's in Right Wall mode, if an angle shallower than $E0 is detected, the engine switches back into Floor mode.
The other transitions work in exactly the same way.
You might rightly ask where the ground sensors are when in Right Wall mode. They're in exactly the same place, only rotated 90 degrees. Sensor A is now at Sonic's Y+9 instead of X-9. Sensor B is now at Sonic's Y-9, instead of X+9. Instead of vertical sensor lines, they are now horizontal, stretching 16 pixels beyond his foot level (which is now 20 pixels "below" him, at X+20).
Yes, because the sensors move so far, it is possible for Sonic to be "popped" out to a new position in the step in which he switches mode. However, this is hardly ever more than a few pixels and really isn't noticeable at all during normal play.
One more thing: I said that solid tiles were made of height arrays. Operative word: height. How do they work when in Right Wall mode? Well, rather gobsmackingly, it turns out that in the original engine, each solid tile has two complementary height arrays, one used for when moving horizontally, the other for when moving vertically.
What about Left Wall and Ceiling mode? Wouldn't there need to be four height arrays? No, because tiles of those shapes simply use normal height arrays, just inverted. When in Ceiling mode, Sonic knows that the height value found should be used to move him down and not up.
With these four modes, Sonic can go over all sorts of shapes. Inner curves, outer curves, you name them. Here are some example images with their angle values to help give you some idea of what I've been talking about:
When in Right Wall, Left Wall, or Ceiling mode, Sonic will fall any time absolute Gsp falls below 2.5 ($0280) (Gsp is set to 0 at this time, but Xsp and Ysp are unaffected, so Sonic will continue his trajectory through the air). This happens even if there is ground beneath him.
When Sonic falls off in the manner described above, the horizontal control lock timer is set to $1E (it won't begin to count down until Sonic lands back on the ground). While this timer is non-zero and Sonic is on the ground, it prevents the player from adjusting Sonic's speed with the left or right buttons. The timer counts down by 1 every step, so the lock lasts about half a second. During this time only slp and the speed Sonic fell back on the ground with are in effect, so Sonic will slip back down the slope.
Any time Sonic is in the air, he doesn't have to worry about angles, Gsp, slp, or any of that jazz. All he has to do is move using Xsp and Ysp until he detects the ground, at which point he re-enters the ground state.
But Sonic does have an unusual set of collision sensors while moving through the air, and it's worth going over them in detail.
Sonic has to be able to bump into walls, just like when he's on the ground. So there's a horizontal sensor line, originating at his Y position, and stretching from X-10 to X+10. Anytime it's colliding with a solid tile, Sonic's "popped" out to the edge of the tile plus/minus 11, just like on the ground.
The thing is, this sensor line is wider than his vertical A and B sensors that detect the ground (we'll be getting to those in a minute). This means it's possible for the horizontal sensor line to detect a block that's Sonic is falling past, even if he has no Xsp at all. This causes him to slip around solids when he hits them at their extreme edges.
So when Sonic detects a collision with his horizontal sensor line, his Xsp is only set to 0 if he moving in the direction of the wall, not away from it. If it were otherwise, he'd stick to walls as he fell past them.
Sonic's A and B sensors are much the same in the air as they are on the ground. The difference is, when they detect a solid tile, Sonic isn't immediately set to the height found in the tile minus 20. Instead, he's only set to it if he's already lower than that height. Otherwise he'd suck to the floor when he got close to it.
Since the sensors stretch so far below his feet, what stops them from detecting the ground again in the step in which he jumps, never letting him leave the floor? Simple. He ignores them unless Ysp is 0 or greater, so he'll only detect the ground while moving down.
Because Sonic can move both up and down while in the air, there have to be two more sensors checking upward (C and D), so that he can bump into ceilings and curves that are above him. (C and D are perfect mirror images of A and B - they have the same X position and length, they just point up instead of down.) Sonic will detect ceilings and be pushed out of them whether he's moving up or down, unlike floors which are only detected when moving down. It is possible to hit a "ceiling" (which is just the bottom side of a block) while moving down - by pressing toward a wall with a gap in it, or jumping toward the side of an upper curve.
There are some ledges that Sonic can jump up "through". These are often in the hilly, green zones such as Green Hill Zone, Emerald Hill Zone, Palmtree Panic Zone, and so on. The solid tiles that make up these ledges are flagged by the engine as being a certain type that should only be detected by Sonic's A and B sensors. They're totally ignored by C and D, as well as the horizontal sensor line.
Xsp and Ysp are derived from Gsp when Sonic is on the ground. When he falls or otherwise leaves the ground, Xsp and Ysp are already the proper values for him to continue his trajectory through the air. But when Sonic lands back on the ground, Gsp must be calculated from the Xsp and Ysp that he has when it happens. You might think that they'd use cos() and sin() to get an accurate value, but not so. In fact, something much more basic happens, and it's different when hitting into a curved ceiling as opposed to landing on a curved floor, so I'll cover them separately.
When the floor angle detected is in the range of $F0~$FF (and their mirror images, $00~$0F), Gsp is set to the value of Xsp. When the angle is in the range of $E0~$EF ($10~$1F), Gsp is set to Xsp but only if absolute Xsp is greater than Ysp. Otherwise, Gsp is set to Ysp*0.5*-sign(cos(angle)). When the angle is in the range of $C0~$DF ($20~$3F), Gsp is set Xsp but only if absolute Xsp is greater than Ysp. Otherwise, Gsp is set to Ysp*-sign(cos(angle)).
When the ceiling angle detected is in the range of $A0~BF ($40~$5F), Sonic reattaches to the ceiling, and Gsp is set to Ysp*-sign(cos(angle)). When the angle is in the range of $60~9F, Sonic hits his head like any ceiling, and doesn't reattach to it. Ysp is set to 0, and Xsp is unaffected.
The Genesis/Mega Drive games use angles in hex, $00 through $FF, meaning that there are only 256 divisions of a circle, not 360 like we're used to. Worse, the direction is anti-clockwise compared to other languages like GML, so $20 isn't 45° like it should be - it's 315°.
In order to convert the original hex angles into angles you can use in GML, use this calculation (rendered here in pseudo-code):