Recently I’ve been messing around in Unity with something intended for the Rift, though I don’t yet have my kit (and won’t for several months by the sounds of things). I’ve always been a fan of the innovative and beautiful Super Mario Galaxy, especially the planetoids and other structures that comprised its levels, each providing its own gravity. I wanted to try a first-person version of that concept, in a less fantastical setting. You can see an early version of what I came up with in this Youtube video. Along the way I found and overcame a few problems that I thought it was probably worth sharing the solutions for, so here’s a quick review.
GRAVITY
So the first thing to do is to replace the simple default PhysX-provided gravity source with something a lot more flexible. I wanted to mix my planetoids with a more traditional “flat” terrain, so created two types of gravity source – spheres and infinite planes. A spherical source will pull objects towards its specific position (usually the center of a sphere), whereas the plane sources pull objects to the surface of the plane. If the plane pulled objects towards its position, they would be pulled horizontally inwards to the center of the plane object which is obviously not what we want.
So for a spherical source, the gravity vector applied to a given object is just (object position - gravity source position)
. The gravity will naturally fall off over distance, so we’ll want the magnitude of that vector to calculate the gravity strength. For an infinite plane the gravity direction is always the local down vector (ie. -transform.up
), and for distance we want the shortest distance to the plane, which you could also describe as the vertical component (in the gravity source’s local space) of the same vector. We can get this with the dot product of the vector from an object to the plane’s position, and the local up vector:
float distanceToPlane = Vector3.Dot(objectPos - planeTransform.position,
planeTransform.up);
Then all we need is a gravity manager object which is responsible for calculating the gravity vector at any given point. This is easily done by totaling all forces which act on that point (ie. where the magnitude of the gravity vector returned from a source is > 0) and then dividing the resulting vector by the number of active sources we found. To get an object to be affected by the gravity, add a rigid body to it, turn off “Use gravity” to disable the standard effect, and finally add a script to each object which has the following function:
void FixedUpdate()
{
Vector3 gravityVector = GravityManager.CalculateGravity(transform.position);
rigidbody.AddForce(gravityVector, ForceMode.Acceleration);
}
A couple of things to note there – one, we’re using FixedUpdate
, because it occurs right after the physics have been calculated and at a nice predictable steady rate. Always add forces in FixedUpdate
. And the second thing is to apply the force as an acceleration to get the right effect.
An interesting note about Super Mario Galaxy – it has all kinds of oddly-shaped landmasses, such as eggs, toruses, Mario’s face etc, which apply gravity to characters in a way which feels natural as you run over and around them. I suspect what they’re doing is using the inverted normal of a base layer of ground polygons that Mario is currently above as the gravity vector. The raycast to find the ground would ignore all other types of objects that would be in the way. This would be easy to add to this system and would be a nice experiment.
MOVEMENT AND ORIENTATION
When moving the player (or any character) around a planetoid, presumably you want it to orient to the surface on which it’s standing. You also want movement to be completely smooth and to feel the same whether you’re on the top or bottom of the sphere. Now I have to break some bad news to you: the CharacterController
that Unity provides won’t work for this use case. It doesn’t reorient the capsule it uses – it’s always upright no matter what the player object’s orientation is. In addition it comes with certain assumptions that don’t fit our needs, such as a world-space maximum slope value.
My initial solution to this problem was to take advantage of the rigid body which was already attached to the player object, and use a movement system which pushed the player around with forces. This worked great but ultimately it proved unworkable as the planetoid code moved along to the obvious next step. I’ll keep you in suspense until the next article if you can’t guess why, but for now just take my word for it – a force-based system isn’t going to work for us.
So, we’re going to reinvent a large part of the CharacterController
wheel here. First make sure the player object has a capsule collider as well as the rigid body; rigid bodies don’t have any physical presence in the world so you still need the collider. The rigid body needs Is Kinematic
set to true
– that means we’ll be updating the transform directly rather than using forces to move it. More on this in my next article!
We need to decide on a step height, which will control the maximum height the capsule can “snap” if it finds an obstacle or a drop in the path of movement. Mine is set to 0.5m. My cheap step height shortcut is to pretend the capsule is actually at its height + stepHeight when moving it – the obvious disadvantage here is that you need at least a clearance of stepHeight above the player to move, but that’s not a problem in my case.
To handle possible collisions, each frame we’re going to move the capsule in a number of iterations. I won’t go into too much more detail as this is a large topic that probably requires an article of its own, but the basic pseudocode algorithm is:
while (movementVector.sqrMagnitude > 0.0f)
{
Vector3 movementDirection = movementVector.normalized;
float movementDistance = movementVector.magnitude;
RaycastHit hitInfo;
if (Physics.CapsuleCast(capsuleTop, capsuleBottom,
capsuleRadius, movementDirection, out hitInfo, movementDistance))
{
// Hit something!
Move the capsule to this point (remembering to subtract the radius first,
as we've hit with our edge but the position is the capsule center).
Pick a new movementDirection (maybe a vector reflection or a direction
perpendicular to the normal of the face we've hit).
The length of the new movement vector is movementDistance minus the
distance we've just moved in this iteration.
}
else
{
// Reached our destination (transform.position + movementVector)
Cast a ray (or capsule) downwards from the destination to find the ground
height, using a maximum distance to avoid snapping when the ground is too
far away, eg. when walking off ledges.
Move the capsule to this location at the ground height (plus half the
capsule height to account for the capsule center being our position).
movementVector = Vector3.zero;
}
}
The reason we keep moving once we’ve hit something is that otherwise the character would just hit an obstacle and stick to it. It’s always nicer to slide along it.
That should take care of movement. The ground snapping required while walking along a curved surface is taken care of by the algorithm above. So now we just have orientation to worry about! This can be surprisingly tricky until you hit on the right approach, especially around the poles as you might encounter ugly rotational snapping issues. The key is one of Unity’s math functions; Quaternion.FromToRotation
takes two rotations and returns you a quaternion which will transform one to the other. Don’t forget to apply that to your original rotation afterwards.
Quaternion desiredRotation = Quaternion.FromToRotation(transform.up,
-vGravity.normalized) * transform.rotation;
transform.rotation = Quaternion.Slerp(transform.rotation,
desiredRotation, maxGravityOrientationSpeed);
The above code finds the ideal rotation for the current gravity direction, then applies a slerp blend to make sure the transition is smooth.
OK, so now you should be able to set up a planet with spherical gravity and have a player character walk around it. In the next article, I’ll talk about extending the system to support moving and rotating planets, and moving the character between them. Thanks for reading!