Physics Overhaul

Release: In-dev
Repository: GitHub

Project Overview

Brief: This mod overhauls the character movement system, adding rotation dampening, slope physics, momentum/inertia, new character actions, and countless other tweaks to the base game.

In a long running series of high speed, physics based platformers that let the player use the terrain to fling themselves through levels, Sonic The Hedgehog (2006) is notable for lacking almost all of that.

Slopes don't affect your speed, leaving the ground locks you to a pre-set speed, there's no momentum or inertia, characters turn on a dime without slowing down, and plenty of other staples are simply absent from this title.
For years, overhauling the controls was a pipedream, but in early 2024 the modding scene saw a breakthrough which allowed manipulating pointer data directly through the game's native Lua framework, so as a showcase of this new tech, I began implementing a physics system for character movement, and over time the project grew to include restoring cut content, fixing broken features, and even adding new ones to polish up the entire experience.

While a proper deep dive is outside the scope of this page, I've included some of the more interesting challenges below.

In character movement, "physics" are somewhat loose; mechanics are often fudged (or even excluded) to create a smoother player experience, so I had to decide what to even implement in the first place. After analyzing other games in the series, I settled on the following core mechanics:

  • "Slope Physics": Running uphill should slow you down and vice versa. This should also apply if you jump off a slope, increasing or decreasing your jump height respectively. This is absent in the Retail game.
  • Momentum/Inertia: Speed should be preserved between actions/states. In Retail, the player typically snaps between pre-determined values.
  • Slope Launches: Related to the above, when the player runs off an incline, they should receive an upward/downward boost based on speed and angle, creating a natural launch.
  • Rotation Dampening: Turning resistance should increase directly with speed, and speed should decrease during sharp turns. The former somewhat exists in Retail, but it's a static resistance and negligible.

Additional features were added as the project continued, such as different physics when rolling and balancing mechanics for rail grinding, but the four listed above form the foundation for everything else.

NOTE: While SEGA/Sonic Team has a very friendly relationship with modders and projects like these, I'll be omitting a lot of information about the reverse engineering process out of respect.

When it came to disassembling the game's code and reverse engineering the structure, my contributions were limited. Another member of the community (the one who set up our DLL system in the first place) generally took care of that.
For my part, I focused on finding specific variables and how they were used, such as documenting all the character-specific flags or hunting down parameters stored in submodules rather than on the character themselves.

The process was pretty similar to poking around in cheat engine, except I used Lua and a disassembler.
Typically, I would use Lua to get the address of a pointer to some common structure I wanted to investigate, then I would jump to that address using a disassembler. From there, I'd look for any bytes, floats or other promising values, make note of the offsets, then retrieve them back in Lua and poke at the data until I could figure out what it handled.
Thankfully, the guess-and-check part wasn't always needed. Since so many character parameters and modules are set up and called from Lua, I could just search for a string using the disassembler then check its relation to the player object and see how its value was assigned.

For example, most character attacks are set up using "weapons modules." These are loaded via Lua in the player's script, so the variables for attack damage and whatnot are written along with everything else, but internally the data is held inside the module, not on the player object.
Luckily, each module has a string associated with it, and the DLL implements a function that returns a pointer to a given module using that string. Thus, if I wanted to modify the Homing Attack's damage value, the process would go like this:

  1. Search the executable for the "c_homing_damage" string.
  2. Check which subroutines load the string (the same string can be used in multiple modules, so I need to get the right one.)
  3. Make note of the offset used to store the variable.
  4. Jump out of the subroutine, back to the main module.
  5. Each module stores this, and other related subroutines, inside a VFTable. Make note of the offset inside the table to access the subroutine.
Putting it all together, in Lua that would look like this:


                            -- Examples, not the actual offsets
                            local vft_offset = 0x2C
                            local variable_offset = 0xF3
                            local homing_damage = PlayerObject:GetIPluginByName("homing"):Move(vft_offset + variable_offset):GetDWORD()
                        

(In the actual project I properly stored this information in a table then wrote a function to easily get/set data from any module.)
This wasn't used terribly often for the core behavior outlined in the Project Goals, but it was used extensively when I began doing character-specific tweaks later in the project.

I faced two major issues when implementing this mod: compatibility and the game itself.
Usually, an overhaul like this would involve extensive code injection (making the game load a DLL where you can write your own C++ code, hooking existing game functions and whatnot).
This is possible in Sonic 06, it's how we can access pointers via Lua in the first place, but creating one is a rather involved process and requires some software that's quite hard to come by. The method used for it also only supports loading one DLL at a time, meaning mods could run into conflicts if the community starts using their own DLLs in their projects.

With that in mind, I decided to write this project in Lua.

The advantages to running everything through the game's Lua were that I sidestepped DLL compatibility concerns and gained a few other perks, such as faster prototyping (functions could be modified or added on the fly since nothing is compiled) and full compatibility with any patches or other mods that may edit character behavior through the executable. It also provided a chance to make modding the game more accessible as Lua is a very beginner friendly language. I wrote a library alongside the project, allowing one to easily modify character attributes, adjust hitboxes, and more as well as a framework for creating new character states and modifying existing ones.
I also implemented a manager script that would load any custom files when given a filepath; this way, a mod only needs to include a 1 line change to a single existing file (to load the manager in the first place), keeping conflicts at a minimum.

Unfortunately, using Lua meant I was completely beholden to the game's existing behavior. My mod acts like a layer sitting on top of the code in the executable; while it can utilize existing systems, and even implement new ones for its own use, it can't change what it's sitting on. For example, when you jump, the game snaps you to a set horizontal speed defined in a parameter file. This state performs that snap every frame while you remain in it, meaning if I try to write to your horizontal speed, my change would get ignored, and since the state is lower level than my script, I can't easily change the behavior.

To fix this, I found where that value was stored then wrote some code to update it directly as the player ran along the ground, that way you'd keep the correct speed when entering the air.
From there, acceleration/deceleration was as simple as smoothly lerping the value.

While the game's inherent quirks caused the most issues, there was also the matter of simply attaining the data I needed to make this work.
Luxuries such as local coordinates, vector and quaternion math functions, or even checking if the player is grounded, are not provided (unlike Unity or similar engines), so they all have to be implemented by hand.
A few things were just a matter of reverse engineering (such as ground checks - there's a bitfield that stores ground/air flags), but I often had to thoroughly research the math behind functions so I could implement them myself, or come up with alternative solutions using the data I already had.

For instance, the first feature I worked on were the slope physics. At the time, I could read/write to the player's speed (forward/upward velocity, to be precise), I could check their global coordinates, and I could detect if they were grounded, but I couldn't get any other information about the surface they were on. Detecting slopes was easy enough - I could just check the change in the player's Y coordinate while they were grounded - but getting the exact angle of incline was another matter.
I had a vague understanding that this was typically done with fancy calculations involving the ground normal, but this was long before I could get the vector for that, let alone do the math.

Instead, I realized I could approximate an angle by normalizing the velocity of the Y coordinate with the player's forward velocity then scaling by 90 (the maximum angle the player may encounter outside of scripted sections.) The solution wasn't perfect (the reported angle would change if they ran diagonally on an incline, for example, and I chose to normalize against max velocity instead of current velocity), but it was consistent enough to let me implement the mechanic in a way that felt natural.

Alternatively, consider this sequence. 06 contains an unused upgrade item that was meant to transform Sonic into Super Sonic. The item does have a unique state attached, but the sequence is incredibly unfinished, doing little more than playing an animation. I wanted to fix that.

This two second sequence required writing a scheduler, scripting camera movement, creating custom particles, and more, but I want to highlight the spinning emeralds.
Sonic 06 does not readily provide a way to parent objects, so any local transformations are a pain. Fortunately, we can spawn in game objects directly via Lua at any given coordinates, and simple havok based objects can be created by just editing a .bin file. With that in mind, I figured I could create a "chaos emerald" object with a lifetime of one frame then make it "move" around the player by spawning a new one each frame, slightly offset from the last position.
From there, I set up a function that would generate a circle around the player using their current position - with a given radius and number of points - and wrote another function to generate an arbitrary number of sub-points between two coordinates via linear interpolation. After that, I could simply generate an initial circle when the Super Transformation process begins, smooth it out by interpolating between the points (storing each set of coordinates as a vector), then storing all the position vectors in an array, spawning the emeralds around the initial circle, then updating their position frame by frame with the smoothed array.

Like most 3D Sonic games, 06 has a gimmick called Rail Grinding where the player attaches to a spline and gets carried along by physics. Typically, the player can tilt the control stick to balance themselves on the rail, gaining speed by leaning into turns or flailing around if they mess up.
Of course, with 06 lacking physics, this was stripped down to moving the player forward at a fixed speed, leaving rails as a rather uninteresting mechanic.

While interactive rail mechanics are usually well received in the series, the actual implementation has been hit-or-miss, so I decided to simplify things.

  • The player has 3 balance conditions: Balanced, Unbalanced, and Flailing.
  • Correctly balancing (no lean on straightaways, or leaning into curves) increases your speed. Small mistakes (no lean on a curve, leaning on a straightaway) make you Unbalanced and gradually reduce speed. Major mistakes (incorrect lean) quickly reduce speed.
  • The player can crouch (and lean while crouching), increasing the effect of physics and balance.
But before I could start on any of that, I had a fundamental issue:
Detecting curves on a spline.

Usually, one would be able to read information about a spline as the player moves along it, but once again, I didn't have that option.
In fact, I didn't even have rotation information for the player. All I could do was measure the player's global coordinates.

I broke the problem down like this:

  • Since rail movement is automated, and the Y axis is irrelevant, I can think of the map as a 2D grid.
  • On a grid, a curve happens when both axes are changing at different, non-zero, rates.
  • Thus, while grinding, you will always be primarily traveling along the X or Z axis, with the other axis determining the direction of the curve.
I could find the primary axis by comparing the deltas of your X and Z coordinates (taking whichever was greater), and I could determine direction by seeing how each moved towards positive or negative infinity.

As for detecting straight movement, I just compared the delta of your secondary axis against a threshold.
Of course, rail splines aren't perfectly rounded on curves, meaning that even if the overall trend of the rail was a curve, moment-to-moment your deltas could imply you were traveling straight. To counteract that, I required that a new direction be reported for a few frames before commiting to an update, and I sampled previous position data to better guess at the overall trend. With all that set up, I came very, very close to success.
("Direction" tracks how my code thinks the rail is currently turning. The other variables are less important)

Tragically, as close as this got, it simply wasn't responsive enough, especially at high speed. Any sort of frame delay in updating direction turned out to be unacceptable for a fast paced game like this, but I didn't want to throw away my work, so I went digging through memory.
Eventually, I found floats that updated based on how closely the player faced towards positive X and Z. Using those, I easily adapted my code to quickly and accurately detect any turns as the player moved. Calculating a lack of turn still proved a tad troublesome, but it was within an acceptable margin of error to continue with the project.

While planning the new balancing mechanics, I realized the project would be much easier if I could somehow extend the behavior of existing states. I was still unable to change the underlying state code, but if I could write a state machine in Lua that ran in tandem with the game's character controller, organizing and implementing mechanics like this would become significantly easier.
Thankfully, I could already detect the player's current character state (C_State), so I set up my Lua state machine to work like this:

  • Write a Lua State (L_State). This consists of StateEnter, Update and StateExit functions. All states inherit from a base state, so these functions can be omitted as needed.
  • Register any C_State as a "root_state" (the state to extend) for the L_State.
  • When the player enters a paired C_State, begin running the corresponding L_State. When the player exits that C_State, automatically exit the L_State, returning to an Init state.
  • As a caveat to the above, the automatic entrance is optional, and L_States can have animations associated with them. If the player leaves the root state but is in an associated animation, they'll stay in the L_State.
  • A Manager function handles the automatic entrances and exits, but this can also be invoked manually through Lua, allowing you to pair multiple L_States to the same C_State.
The L_States themselves are handled as objects stored inside a table I created to house custom parameters (for physics and whatnot) for each character. States have a reference to that character, and multiple L_States can be grouped within a class, allowing easy reference to the other grouped states.
(This also lets a Class Exit function be assigned that only runs when you leave the class, and you'll stay within the class as long as you're in any associated animation.)

With this, even though I couldn't disable any existing state transitions or behavior, I could still radically extend what each state could do.
In the case of rail grinding, I opted for three L_States (grinding, crouching, and tricking - to accommodate the game's one existing rail mechanic), grouped within the "Grind" class and rooted to C_State 12 (grinding). In Lua, the declaration looked something like this:


                            PlayerList = {
                                sonic = {
                                    -- Pair a handle to a cue in the character animation sheet.
                                    state_anims = {
                                        Grinding = {
                                            lean_L = "overdrive_l",
                                            lean_R = "overdrive"
                                        },
                                        Grinding_Crouch = {
                                            crouch = "teleport_dash_l",
                                            lean_L = "esp_one_l",
                                            lean_R = "esp_one_r"
                                        }
                                    }
                                },
                                classes = {
                                    Grind = { anims = {}, ExitFunc = ResetRailParams, root_state = StateID.GRIND }
                                },
                                -- Populated by RegisterState
                                states = {},
                                current_state = "Init",
                                current_class = "None",
                                state_id_list = {
                                    [StateID.GRIND] = "Grinding"
                                }
                            }

                            -- Creat and register the "Grinding" state to the "Grind" class on the sonic object.
                            RegisterState(PlayerList.sonic, "Grinding", "Grind")

                            RegisterState(PlayerList.sonic, "Grinding_Crouch", "Grind")

                            function PlayerList.sonic.states.Grinding:StateEnter()
                              self.player.current_state = self.name
                              self.player.current_class = self.class
                            end

                            function PlayerList.sonic.states.Grinding:StateMain()
                              -- Update function, example code
                              if Current_Stick_Angle_X > 0.1 then
                                my_rail_lean = "right"
                                ChangePlayerAnim(self.anim_reference.lean_R)
                              end
                              if GetInput("rt", "hold") then
                                return self:SwitchState("Grinding_Crouch")
                              end
                            end

                            -- Exit function omittted
                        

Then, putting everything together...

This was a strong initial showing, but the true power of this system lies in it being done through Lua. Because none of the C_State code is modified, multiple L_States can be registered to the same root state, each with wildly different behavior.
While documenting the C_State list, I found an unused dummy state called "Invincible." This state has almost no behavior; it has no transitions, preserves whatever speed you entered it with, locks player rotation and makes you invulnerable. While rather pointless on its own, this was the perfect candidate for my state system. I could already handle speed, input detection and changing C_States through Lua, and restoring rotation just required flipping a byte on character context, so this left me with a blank template to manipulate however I wanted. Using Invincible as a root, I added new common abilities, such as Rolling, and new attacks to flesh out character movesets (like a grounded tail-swipe for Tails, or an aerial dive for Rouge).

As nice as those additions were, the biggest accomplishment was creating a "new" character. I won't go into detail as the process was the same as the above (just with running, jumping, falling, and other common and custom states (re)implemented using Invincible), but it allowed me to take one of the game's gimmick character variants (which runs a heavily stripped down version of the common character context) and flesh out their gameplay.

One of the very few limitations left on modding this game is the sandboxing of Lua. In short, the game effectively runs unique instances of Lua to handle enemy behavior (Enemy instance), stage creation (Stage instance), story/level progression (Mission instance), etc. This means that even if you load the same scripts and declare the same globals in each instance, they'll all act independently of each other.
In my time modding the game, I've found a few tricks to communicate between instances, but those require manipulating object layout files and thus aren't always practical. The DLL itself exposes almost every useful game function to the Stage intsance, which is where this mod lives, so this sandboxing can usually be ignored, but it became a major roadblock when trying to implement a series staple - the Badnik Bounce.

In every Sonic game - except for this one - the player will inflict damage and bounce into the air if they jump into an enemy. It's a natural move that merges combat and platforming, yet it was conspicuously absent from 06, and recreating it had two major issues:

  1. We cannot access specific collider information, so there wasn't a good way to tell when an enemy was hit.
  2. The hitbox would harm everything indiscriminately. Sonic 06 often uses destructible physics objects for platforming (such as wooden boxes, bridges or watch towers), so slapping a hitbox on the player's jump would heavily interfere with normal gameplay.

In theory, the latter could be fixed by changing the collision flags for the hitbox attached to Jump, but the only hitbox that every character can access is directly tied to their collider, so adjusting the flags on it breaks most collision interactions.
Instead, I chose to just make it an optional ability, tied to holding down the B button while airborne. This way the original gameplay was preserved for those who prefered it, and I had a little more freedom to turn this into a dedicated ability.
As for detecting enemy collision specifically, the game already provided a way to do that.

Enemies in this game source all their behavior from Lua. The executable will call a few generic state functions as needed (Appear, Update, OnDamage, OnDead, etc.), but the functions themselves are blank; it's up to the programmer to write further behavior. With this, I could add a generic callback function whenever an enemy takes damage or dies, signaling to another script that the event just occurred, and from there I could run some checks to infer whether the cause of that event was from a Badnik Bounce (e.g., checking distance from the player to the sender, seeing if the player was in the Jump state, etc.)
However, this ran into the sandboxing problem. The OnDamage call has to be executed from the Enemy instance, so I needed a way to send a boolean value from Enemy to Stage with some bridge between the two.
As luck would have it, 06 has a function for writing an integer to an index of an array stored temporarily in savedata. These are called TemporaryFlags internally, and usually the function is restricted to the Mission instance, but with the DLL I could use it to set a flag in the Enemy instance then check it in Stage. The trouble was, this opened a lot of doors, but the number of temp flags is rather limited, and since I was creating this mod and its underlying framework with compatibility in mind, I needed a way to store to as much information as possible using as few flags as possible.

Enter bitflags. By reserving one index as a bitfield for Physics, I could store up to 32 unique boolean values, capable of communicating with any environment. And with that, the Badnik Bounce was ready to go.