16
\$\begingroup\$

I'm trying to create an effect similar to that of Mario Galaxy or Geometry Wars 3 where as the player walks around the "planet" gravity seems to adjust and they don't fall off the edge of the object as they would if the gravity was fixed in a single direction.

enter image description here
(source: gameskinny.com)

Geometry Wars 3

I managed to implement something close to what I'm looking for using an approach where the object that should have the gravity attracts other rigid bodies towards it, but by using the built in physics engine for Unity, applying movement with AddForce and the likes, I just couldn't get the movement to feel right. I couldn't get the player to move fast enough without the player starting to fly off the surface of the object and I couldn't find a good balance of applied force and gravity to accommodate for this. My current implementation is an adaptation of what was found here

I feel like the solution would probably still use physics to get the player grounded onto the object if they were to leave the surface, but once the player has been grounded there would be a way to snap the player to the surface and turn off physics and control the player through other means but I'm really not sure.

What kind of approach should I take to snap the player to the surface of objects? Note that the solution should work in 3D space (as opposed to 2D) and should be able to be implemented using the free version of Unity.

\$\endgroup\$
  • \$\begingroup\$ Possible duplicates: gamedev.stackexchange.com/questions/47220/… and gamedev.stackexchange.com/questions/71585/… \$\endgroup\$ – MichaelHouse Dec 14 '14 at 20:55
  • \$\begingroup\$ I didn't even think of searching for walking on walls. I'll take a look and see if these help. \$\endgroup\$ – SpartanDonut Dec 14 '14 at 20:59
  • 1
    \$\begingroup\$ If this question is specifically about doing this in 3D in Unity, It should be made clearer with edits. (It wouldn't be an exact duplicate of that existing one then.) \$\endgroup\$ – Anko Dec 14 '14 at 21:47
  • \$\begingroup\$ That's my general feeling as well - I'm going to see if I can adapt that solution to 3D and post it as an answer (or if someone else can beat me to the punch I'm fine with that too). I'll try and update my question to be more clear on that. \$\endgroup\$ – SpartanDonut Dec 14 '14 at 22:22
  • \$\begingroup\$ Potentially helpful: youtube.com/watch?v=JMWnufriQx4 \$\endgroup\$ – ssb Dec 17 '14 at 12:51
7
+50
\$\begingroup\$

I managed to accomplish what I needed, primarily with the assistance of this blog post for the surface snapping piece of the puzzle and came up with my own ideas for player movement and camera.

Snapping Player to the Surface of an Object

The basic setup consists of a large sphere (the world) and a smaller sphere (the player) both with sphere colliders attached to them.

The bulk of the work being done was in the following two methods:

private void UpdatePlayerTransform(Vector3 movementDirection)
{                
    RaycastHit hitInfo;

    if (GetRaycastDownAtNewPosition(movementDirection, out hitInfo))
    {
        Quaternion targetRotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
        Quaternion finalRotation = Quaternion.RotateTowards(transform.rotation, targetRotation, float.PositiveInfinity);

        transform.rotation = finalRotation;
        transform.position = hitInfo.point + hitInfo.normal * .5f;
    }
}

private bool GetRaycastDownAtNewPosition(Vector3 movementDirection, out RaycastHit hitInfo)
{
    Vector3 newPosition = transform.position;
    Ray ray = new Ray(transform.position + movementDirection * Speed, -transform.up);        

    if (Physics.Raycast(ray, out hitInfo, float.PositiveInfinity, WorldLayerMask))
    {
        return true;
    }

    return false;
}

The Vector3 movementDirection parameter is just as it sounds, the direction we are going to be moving our player in this frame, and calculating that vector, while ended up relatively simple in this example, was a bit tricky for me to figure out at first. More on that later, but just keep in mind that it's a normalized vector in the direction the player is moving this frame.

Stepping through, the first thing we do is check if a ray, originating at the hypothetical future position directed towards the players down vector (-transform.up) hits the world using WorldLayerMask which is a public LayerMask property of the script. If you want more complex collisions or multiple layers you will have to build your own layer mask. If the raycast successfully hits something the hitInfo is used to retrieve the normal and hit point to calculate the new position and rotation of the player which should be right on the object. Offsetting the player's position may be required depending on size and origin of the player object in question.

Finally, this has really only been tested and likely only works well on simple objects such as spheres. As the blog post I based my solution off of suggests, you will likely want to perform multiple raycasts and average them for your position and rotation to get a much nicer transition when moving over more complex terrain. There may also be other pitfalls I've not thought of at this point.

Camera and Movement

Once the player was sticking to the surface of the object the next task to tackle was movement. I had originally started out with movement relative to the player but I started running into issues at the poles of the sphere where directions suddenly changed making my player rapidly change direction over and over not letting me ever pass the poles. What I wound up doing was making my players movement relative to the camera.

What worked well for my needs was to have a camera that strictly followed the player based solely on the players position. As a result, even though the camera was technically rotating, pressing up always moved the player towards the top of the screen, down towards the bottom, and so on with left and right.

To do this, the following was executed on the camera where the target object was the player:

private void FixedUpdate()
{
    // Calculate and set camera position
    Vector3 desiredPosition = this.target.TransformPoint(0, this.height, -this.distance);
    this.transform.position = Vector3.Lerp(this.transform.position, desiredPosition, Time.deltaTime * this.damping);

    // Calculate and set camera rotation
    Quaternion desiredRotation = Quaternion.LookRotation(this.target.position - this.transform.position, this.target.up);
    this.transform.rotation = Quaternion.Slerp(this.transform.rotation, desiredRotation, Time.deltaTime * this.rotationDamping);
}

Finally, to move the player, we leveraged the transform of the main camera so that with our controls up moves up, down moves down, etc. And it is here we call UpdatePlayerTransform which will get our position snapped to the world object.

void Update () 
{        
    Vector3 movementDirection = Vector3.zero;
    if (Input.GetAxisRaw("Vertical") > 0)
    {
        movementDirection += cameraTransform.up;
    }
    else if (Input.GetAxisRaw("Vertical") < 0)
    {
        movementDirection += -cameraTransform.up;
    }

    if (Input.GetAxisRaw("Horizontal") > 0)
    {
        movementDirection += cameraTransform.right;
    }
    else if (Input.GetAxisRaw("Horizontal") < 0)
    {
        movementDirection += -cameraTransform.right;
    }

    movementDirection.Normalize();

    UpdatePlayerTransform(movementDirection);
}

To implement a more interesting camera but the controls to be about the same as what we have here you could easily implement a camera that isn't rendered or just another dummy object to base movement off of and then use the more interesting camera to render what you want the game to look like. This will allow nice camera transitions as you go around objects without breaking the controls.

\$\endgroup\$
3
\$\begingroup\$

I think this idea will work:

Keep a CPU-side copy of the planet mesh. Having mesh vertices also means you have normal vectors for each point on the planet. Then completely disable gravity for all entities, instead applying a force in exactly opposite direction of a normal vector.

Now, based on which point should that normal vector of the planet be calculated?

The easiest answer (which I'm pretty sure will work okay) is to approximate similarly to Newton's method: When objects first spawn, you know all their initial positions on the planet. Use that initial position to determine each object's up vector. Obviously gravity will be in the opposite direction (towards down). In the next frame, before applying gravity, cast a ray from the object's new position towards it's old down vector. Use that ray's intersection with the planet as the new reference for determining the up vector. The rare case of the ray hitting nothing means something went horribly wrong and you should move your object back to where it was in the previous frame.

Also note that using this method, the further player origin is from the planet, the worse the approximation becomes. Hence it's better to use somewhere around each player's feet as their origin. I'm guessing, but I think using feet as origin also will result in easier handling and navigation of player.


A last note: For better results, you can even do the following: keep track of the player's movement in each frame (e.g. using current_position - last_position). Then clamp that movement_vector so that it's length towards object's up is zero. Let's call this new vector reference_movement. Move the previous reference_point by reference_movement and use this new point as ray tracing origin. After (and if) the ray hits the planet, move reference_point to that hit point. Finally, calculate the new up vector, from this new reference_point.

Some pseudo code to sum it up:

update_up_vector(player:object, planet:mesh)
{
    up_vector        = normalize(planet.up);
    reference_point  = ray_trace (object.old_position, -up_vector, planet);
    movement         = object.position - object.old_position;
    movement_clamped = movement - up_vector * dot (movement, up_vector);

    reference_point  += movement_clamped;
    reference_point  = ray_trace (reference_point, -up_vector, planet);
    player.up        = normal_vector(mesh, reference_point);
}
\$\endgroup\$
2
\$\begingroup\$

This post could be helpful. Its gist is, you don't use the character controllers, but make your own using the physics engine. Then you use the normals detected underneath the player to orient them to the surface of the mesh.

Here's a nice overview of the technique. There are plenty more resources with web search terms like "unity walk on 3d objects mario galaxy".

There is also a demo project from unity 3.x that had a walking engine, with a soldier and a dog and in one of the scenes. It demonstrated walking on 3d objects Galaxy-style. It's called the locomotion system by runevision.

\$\endgroup\$
  • 1
    \$\begingroup\$ At a first glance the demo doesn't work very well and the Unity package has build errors. I'll see if I can make this work but a more complete answer would be appreciated. \$\endgroup\$ – SpartanDonut Dec 15 '14 at 3:25
2
\$\begingroup\$

Rotation

Checkout the answer on this question on answers.unity3d.com (actually asked by myself). Quote:

The first task is to get a vector that defines up. From your drawing, you can do it in one of two ways. You can treat the planet as a sphere and use (object.position - planet.position). The second way is to use Collider.Raycast() and use the 'hit.normal' it returns.

Here is the code that he suggested me:

var up : Vector3 = transform.position - planet.position;
transform.rotation = Quaternion.FromToRotation(transform.up, up) * transform.rotation;

Call that every update, and you should get it working. (note that the code is in UnityScript).
The same code in C#:

Vector3 up = transform.position - planet.position;
transform.rotation = Quaternion.FromToRotation(transform.up, up) * transform.rotation;

Gravity

For the gravity, you can use my "planet physics technique", which I described in my question, but it isn't really optimized. It was just something I got on my mind.
I suggest you creating your own system for the gravity.

Here is a Youtube tutorial by Sebastian Lague. That is a very well-working solution.

EDIT: In unity, go to Edit > Project Settings > Physics and set the all values for Gravity to 0 (Or remove the Rigidbody from all objects), to prevent the built-in gravity (which just pulls the player down) to conflict with the custom solution. (turn it off)

\$\endgroup\$
  • \$\begingroup\$ Gravitating the player to the center of a planet (and rotating them accordingly) works well for near-spherical planets, but I can imagine it going really wrong for less spherical objects, like that rocket-shape Mario is jumping on in the first GIF. \$\endgroup\$ – Anko Dec 23 '14 at 13:34
  • \$\begingroup\$ You could create a cube which is restricted to only move around the X-axis for example, inside of the non-spherical object, and move it when the player moves, so that it is always straight under the player, and then pull the player down to the cube. Try it. \$\endgroup\$ – Daniel Kvist Dec 23 '14 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.