Friday, June 21, 2019

physics - How could I constrain player movement to the surface of a 3D object using Unity?



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.



Answer



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.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...