Magnetic Tracks
This is how I made the magnetic antigravity tracks in NEODRIVE.

There are four general stages of complexity. Some of this process I had anticipated beforehand, and some was trial and error.
First Inversion

The level to test these magnetic tracks, First Inversion, is fairly simple - a long, continuous track that turns upside down, makes some banked turns, and then proceeds over some hills to the finish. Each track shape is defined by a spline, which controls parameters like track width and bank angle.
Much thanks to Mike Danielsson’s Spline Architect package, which made this whole thing possible.
I wrote some initial code that is able to look at the car’s position on the track and get the basis vectors - the three directions that define right, forward, and up - at the car’s point in the spline.
void GetSplineBasis(Vector3 worldPos,
Spline spline,
out Vector3 right,
out Vector3 normal,
out Vector3 forward
) {
Vector3 splinePosition = spline.WorldPositionToSplinePosition(worldPos);
splinePosition.x = 0;
splinePosition.y = 0;
float time = splinePosition.z / spline.Length;
float fixedTime = spline.TimeToFixedTime(time);
spline.GetNormalNonAlloc(splineNormals, fixedTime);
right = splineNormals[0];
normal = splineNormals[1];
forward = splineNormals[2];
}

These are the foundation of the magnetic track following code. And it ended up being fairly brief - as of the time of writing this post, it’s under 150 lines. Let’s get into it.
Stage 1: Antigravity
When the car starts driving on a magnetic track, we need to remove the effects of gravity on it.
You can look at the game world’s physical gravitational force and just apply a counter-force to the car every physics update, but it’s cleaner to just tell the car’s physics body to ignore the effects of gravity.
if (!onMagnet) {
if (magnetLastStep) {
car.rb.useGravity = true;
}
magnetLastStep = false;
return;
}
car.rb.useGravity = false;
Then, look at the vector basis of this point in the spline, specifically the up vector. This will always point away from the track surface. It’s also called the normal vector.
Side note: The most Unity-native way would be to make a raycast under the car and look at the surface hit’s normal. This doesn’t need any information about the spline, but is prone to errors since the track surface is polygonal and won’t perfectly match the spline’s path. Reading the spline data itself is mathematically perfect and also works at higher speeds, which will be important later.
Once we have the up direction, just flip it, look at the gravity strength in the game’s world, and artificially apply that force in the down direction relative to the track. And now we’ve got artifical gravity anchoring the car.
car.rb.AddForce(-normal * Physics.gravity.magnitude, ForceMode.Acceleration);
With this, the car can park upside down (or at any point in the magnet track) and remain still. It will also be unaffected by gravity as it drives up and down, relative to the game world.

Stage 2: Centripetal Force
New problem: just like if the car is driving over a hill under normal conditions, if the car is going too fast it can fly off the surface of the magnetic track if it makes a convex shape.
I was initially lazy and allowed this to happen, but I did think it would be very cool and trippy if I could have the car follow the outside of a convex turn.
The solution is generally to add more force, but you don’t want to always add more force because then the springs would bottom out and the car would scrape the ground on the non-convex sections.
You actually need to figure out, at every current place on the track, the radius of the imaginary circle the car is driving along the outside of. You can calculate the centripetal force based on that radius and the car’s speed, and then negate it.

Vector3 deltaPos = car.rb.position - positionLastStep;
float deltaS = deltaPos.magnitude;
if (deltaS > 0.001f) {
float deltaAngle = Vector3.Angle(normalLastStep, normal) * Mathf.Deg2Rad;
if (deltaAngle > 0.0001f) {
float radius = deltaS / deltaAngle;
float speed = Vector3.ProjectOnPlane(car.rb.velocity, normal).magnitude;
float calculatedAcc = (speed * speed) / radius;
Vector3 deltaNormal = normal - normalLastStep;
if (Vector3.Dot(deltaNormal, deltaPos) > 0) {
centripetalMod = calculatedAcc;
}
}
}
Looking at the car’s change in position (and the spline’s change in rotation between this frame and the last frame) gives us the necessary info we need. And thus, we alter the gravity based on this centripetal force modifier:
float totalAcceleration = Mathf.Max(0, Physics.gravity.magnitude + centripetalMod);
car.rb.AddForce(-normal * totalAcceleration, ForceMode.Acceleration);
If you know your Unity stuff, you may be wondering where the
* Time.fixedDeltaTimeparts are in this code. They cancel each other out nicely here. The force we’re applying is based on the car’s distance traveled, which is framerate-dependent.
Stage 3: Other Forces
That’s not quite the end of the story. There are also other forces acting on the car while it drives, which I will get into in another blog post. Suffice it to say, simply adding force won’t keep the car on the track under conditions like drifting, boosting, or just driving fast.
The time for delicately telling Unity to apply a physics timestep-integrated force is gone. Now it’s time to grab the car’s velocity in our hands and bend it ourselves.
Second Inversion
See, the car can have some force applied to it, sure, but there’s no guarantee that the force will be enough to counteract other applied forces during travel if it’s accelerating or turning. Thankfully, all the information about the car’s position on the spline is available. We just need to use it.
Generally speaking, this is the algorithm to really nail the car to the track:
- Look at the difference in the spline’s rotation (Δθ) from the last two frames
- Rotate the car’s velocity vector itself in the car’s up/down direction to match that
The immediate question is, how do you figure out the difference in the spline’s rotation? You can’t just look at the difference in the forward vectors, for reasons that are a bit tricky to explain with only 2D space.
Basically, just checking the angle between the forward vectors only works if the track is shaped like a straight line following the outside of a sphere. In that sense, the only way the track is turning (and thus, the only way the forwad vector is changing) is up/down. And you only need to rotate the car’s velocity up/down to match the track surface, so that’s cool!

What about if the track is on the surface of a sphere, but it’s making a right turn?

The forward vector is suddenly changing in two directions:
- Up/down (to match the sphere’s surface)
- Left/right (to make the right turn)
So with this extra axis difference, Δθ between now and the last frame will be larger than we expect.
Vector projection to the rescue!
The solution is to isolate the change in the forward vector to only its own up/down direction. In practical terms, you do this by projecting the forward vectors of the current frame and the last frame onto the current frame’s right vector.
float rotationAmount = Vector3.SignedAngle(
Vector3.ProjectOnPlane(forwardLastFrame, right),
Vector3.ProjectOnPlane(forward, right),
right
);
car.rb.velocity = Quaternion.AngleAxis(rotationAmount, right) * car.rb.velocity;
Not even going to get into quaternions here. You just need to know that the last line there rotates the car’s velocity by
rotationAmounton the axis of rotation defined by therightbasis vector.
Another small complication: we only want to do this for when the car needs to be rotated positively (clockwise on the axis defined by the right vector), aka downwards in the spline’s forward direction. A negative rotation amount means the car is traveling through a valley and the nose will be tilted up.
In that case, we want to let the car’s own suspension system take care of pushing it away from the ground or else risk throwing the car away from the magnetic track altogether. So:
// don't rotate car velocity for concave parts
if (rotationAmount > 0) {
car.rb.velocity = Quaternion.AngleAxis(rotationAmount, right) * car.rb.velocity;
}
Stage 4: Transfers
While working on Second Inversion, I wanted to put in a cool bit: the spline loops back on itself and you have to do a midair transfer to another vertical wall as you’re hurtling downwards.
The problem is that the landing is finicky - the car collision hull can bounce off the track, or only one wheel can hit the track, and since all that is happening as the springs are pushing the car away and the track is curving away from the car’s velocity, it can bounce off the surface and crash into the ground unless you make a perfect four-point landing with all wheels at once.
That would have been an interesting skill check in another game, but NEODRIVE isn’t a rage game or a precision platformer. The difficulty comes from managing gears and drift lines and being at the edge of control for your current speed, and not making precision jumps. Plus, this transfer was at the end of the track, and it feels bad to have a good run ruined by some physics bullshit.
The solution? A simple landing assist force.
This was actually a very simple solution compared to the last step. If the car has just landed on the magnet track, then apply extra force to glue it towards the track for half a second. Also, for good system hygiene, I cleared the previous basis vectors if the car was just landing on the track, since they’d be out of date.
// if re-establishing magnet contact, clear previous basis vectors
if (!magnetLastStep) {
forwardLastFrame = forward;
rightLastFrame = right;
normalLastFrame = normal;
magnetHitTime = Time.time;
}
if (Time.time < magnetHitTime + 0.5f) {
car.rb.AddForce(5f * Physics.gravity.magnitude * -normal, ForceMode.Acceleration);
}
And with that, the magnetic tracks are rock-solid. You can still knock the car off them if you bounce off a wall, since the wall geometry can sometimes send you upwards, but even that only happens some of the time.
Thanks for reading!