Wednesday, October 23, 2013

Fake Gravity: Super Mario Style

Before trying to implement fake gravity we must figure out what we are trying accomplish. Are we creating a realistic space flight game? Do we want to allow the player to walk up walls? Do we want to just create interesting levels which include arbitrary curves that the player can smoothly walk up?

There are two main issues we must figure out. Which way should we apply force to our player and which direction should we orient our character so they stay upright no matter what direction gravity is pulling them towards.

Spherical Gravitation: 
When thinking about fake gravity around a sphere all we really need to keep track of is the direction from the player to the center of the sphere. Finding this vector from the player/objects to the center of the planet will give you the direction that the gravity force should be pushing the object. 

Arbitrary Curve Gravity:
For arbitrary curves, a simple point will not be enough to tell the player which direction gravity should be applied. Instead, we need to base our orientation and the gravitational force on the surface below the object. One way to do this is by finding the normal vector from the ground the player is resting on. 
The normal vector is just the vector perpendicular to a surface. With this vector we can find the direction that we need to apply a force to the player and we can figure out the rotation to orient the player. 

Implementation:
For the implementation I will focus on the arbitrary curves since this can also work for spherical bodies. The code can be pretty easily adjusted to make the player orient towards a point.

First a warning, Do Not Use Unity's Build In Character Controller! Character controller collider's expect that the world's positive Y axis is their up vector direction. You may be able to pull off implementation if you are using a sphere collider, but fake gravity manipulation will not work with capsule colliders since the collider's up vector will always be pointing towards the positive Y direction. 

For my implementation, I am creating a rigidbody player controller with a first person perspective. For a great simple tutorial on how to create a first person controller using rigidbodies, you can check out Eteeski FPS tutorial here

In Unity we have a couple tools to help us. First are Raycasts. Simply, a raycast is a line starting from one point and continues infinitely in one direction. Using a raycast starting at the player and pointing down, we can get information about the surface that the raycast hits.

RaycastHit groundRay;
bool hitGround = Physics.Raycast(transform.position, -1 * transform.up, out groundRay);
In this code, Physics.Raycast is creating a raycast starting at the player's position. It points the raycast towards the object's down vector and stores the data that the ray hit inside of the variable groundRay. Physics.Raycast returns a bool, returning true if the raycast hit a collider, and returning false if the raycast did not hit a collider. More on Raycasts can be found on unity's documentation here and here.

With the groundRay variable, we can easily get the surface normal just by calling groundRay.normal. Now a simple solution will be just to write

                    transform.up = groundRay.normal

This will work, to an extent ... the object will be able to rotate correctly up the walls matching the ground's normal, but you will not be able to rotate the object along its local Y axis. This is due to the up vector of the object, which consists of x,y, and z components copying the components of the ground's normal x,y and z. Although this is a simple solution, it may not work in many situations since the object cannot rotate.

A better way to rotate the object is through

                       Quaternion temp = Quaternion.FromToRotation(transform.up, groundRay.normal);
       temp = temp * transform.rotation;
       transform.rotation = Quaternion.Slerp(transform.rotation,temp,sler);

Quaternions are pretty confusing and abstract compared to the more human readable Euler, but they have a few key advantages, especially in their avoidance of Gimbal Lock. The first line finds the difference of rotation between the players up vector to the groundRay's normal vector. We then multiply this by the players current rotation to keep the same local Y rotation. Finally, we use Quaternion.Slerp to smoothly orient the player with the ground. The variable sler is just a float that represents the speed of the rotation.

Earlier I am using the words local Y to describe the direction we want to be able to rotate our object. This is probably the wrong word in Unity's definition since Unity regards local rotation as the rotation from an object's parent and not what the objects up vector represents. Really we want to rotate the object around its up vector. Because of this, simply changing the y value, even through transform.localRotation does not exactly do what we want.

Changing the y rotation through transform.Rotation changes the rotation based on world space. Changing the y rotation through transform.localRotation changes the rotation based on the objects parent rotation. This is just a pitfall that you may encounter when trying to rotate the object. Instead we need to use the function transform.Rotate(x,y,z) which rotates the object based on it's up vector. A simple way to find how much to change the y rotation will be

         yRotation += Input.GetAxis("Mouse X") * xLookSensitivity;
diffY = yRotation-diffY;
currYRotation = Mathf.SmoothDamp(currYRotation,diffY,ref yRotationVel,                  
                                           lookSmoothDamp);
transform.Rotate(0,currYRotation*Time.deltaTime,0);
diffY = yRotation;

All this does is checks for a change in Input direction from the mouse's x direction and rotates the object based off of this rotation. transform.Rotate will rotate the object based on the object's up vector.

Putting the code together we have

using UnityEngine;
using System.Collections;

public class PlayerMovement : MonoBehaviour {

public float walkAcceleration = 1000;
public GameObject cameraObject;
public float maxWalkSpeed = 20;
Vector3 horizontalMovement;

public float jumpVelocity = 600;
public float maxSlope = 45;

public float walkDecelleration = 1;
public float airAccMod = .5f;
float walkDecX;
float walkDecY;
float walkDecZ;
public bool grounded = true;
public float grav = 750;

MouseLookFpsScript cameraMouseLook;

public float lookSmoothDamp=.1f;

public float xLookSensitivity = 320f;
float currYRotation = 0;
float yRotation;
float yRotationVel;

public float sler = .1f;

float diffY;
public int layerNumber;


void Start () {
cameraMouseLook = (MouseLookFpsScript) cameraObject.GetComponent("MouseLookFpsScript");
}


// Update is called once per frame
void FixedUpdate () {

//Setting limit to Rigidbody velocity
horizontalMovement = new Vector3(rigidbody.velocity.x,rigidbody.velocity.y,      
                                                              rigidbody.velocity.z);
if(horizontalMovement.magnitude > maxWalkSpeed){
horizontalMovement = horizontalMovement.normalized;
horizontalMovement *= maxWalkSpeed;
print ("kittens");
}
rigidbody.velocity = new Vector3(horizontalMovement.x, horizontalMovement.y, 
                                                         horizontalMovement.z);



//Adding friction
if(grounded){
rigidbody.velocity = new Vector3(Mathf.SmoothDamp(rigidbody.velocity.x,0,ref 
                                                                               walkDecX,walkDecelleration),
Mathf.SmoothDamp(rigidbody.velocity.y,0,ref walkDecY, walkDecelleration),
Mathf.SmoothDamp(rigidbody.velocity.z,0,ref walkDecZ, walkDecelleration));
}


//Setting up raycast
RaycastHit groundRay;
Debug.DrawRay (transform.position,transform.up * -50, Color.red);
bool hitGround = Physics.Raycast(transform.position, -1 * transform.up, out groundRay);



//Rotating player based on grounds normal
int gravLayMask = (1 << layerNumber);
if(Physics.Raycast(transform.position, -1 * transform.up, out groundRay,100, 
gravLayMask)){
Quaternion temp = Quaternion.FromToRotation(transform.up, groundRay.normal);
temp = temp * transform.rotation;
transform.rotation = Quaternion.Slerp(transform.rotation,temp,sler);
}



//Rotate the player to look, change "y" 
yRotation += Input.GetAxis("Mouse X") * xLookSensitivity;
diffY = yRotation-diffY;
currYRotation = Mathf.SmoothDamp(currYRotation,diffY,ref yRotationVel, 
                                                                             lookSmoothDamp);
transform.Rotate(0,currYRotation*Time.deltaTime,0);
diffY = yRotation;



//Moving the player
if(grounded){
rigidbody.AddRelativeForce(Input.GetAxis("Horizontal") * walkAcceleration *Time.deltaTime, -1*grav * Time.deltaTime, Input.GetAxis("Vertical")* walkAcceleration*Time.deltaTime);
}else{
rigidbody.AddRelativeForce(Input.GetAxis("Horizontal") * walkAcceleration *Time.deltaTime * airAccMod ,-1 * grav * Time.deltaTime, Input.GetAxis("Vertical")* walkAcceleration*Time.deltaTime * airAccMod);
}

//Jump
if(Input.GetButtonDown("Jump") && grounded){
rigidbody.AddRelativeForce(0,jumpVelocity,0);
}

}

//Checks if the collider is touching ground beneath the player. 
void OnCollisionStay(Collision collision){
foreach(ContactPoint contact in collision.contacts){
if(Vector3.Angle(contact.normal, transform.up)< maxSlope){
grounded = true;
}
}
}


void OnCollisionExit(){
grounded = false;
}

}

The script I use to move the camera up and down

using UnityEngine;
using System.Collections;

public class MouseLookFpsScript : MonoBehaviour {

public float yLookSensitivity = 5f;
float xRotation;
float currXRotation;
float xRotationVel;
public float lookSmoothDamp = 0.1f;


void Update () {
xRotation -= Input.GetAxis("Mouse Y") * yLookSensitivity;
xRotation = Mathf.Clamp(xRotation,-90,90);
currXRotation = Mathf.SmoothDamp(currXRotation,xRotation,ref xRotationVel,lookSmoothDamp);
transform.localRotation = Quaternion.Euler(currXRotation,0,transform.localEulerAngles.z);
}
}

In Unity, I create a capsule object and add a rigidbody component and the top script. After, I attach a camera to the capsule object and add the second script. Just make sure the camera object rotation is zeroed out after attaching it to the player object. 

Quick word on Layer Masks:
The code in player movement that orients the player based on the normal looks for a specific layer that I call gravLayMask. Using this I can still create hills or objects without the player orienting themselves to that object. You can take out that if statement that includes the gravLayMask and the code should allow the player to orient to any surface. LayerMasks work pretty interestingly in Unity. In Physics.Raycast() the last variable you can give it is a binary number (an int that unity thinks of as a binary number). Each spot in the binary number represents a layer in Unity. When giving this number Unity will ignore any location spot that has a 0 and will only look for any layers that have the 1. In Unity, if you go to the inspector for your ground object and click the box next to Layer and then click "add new" you will see a list of layers. 


Here you can add a new layer, I named my layer gravGround. The tags are irrelevant for this. Now remember which layer number that you added for the ground, you will need this number. For this example, my layer is number 8. Since we only want the Physics.Raycast() to look for collisions that have the layer "gravGround", we need the layerMask that we give the Physics.Raycast() to be 0b100000000. This will make unity not look for layer 0, not look for layer 1, not look for layer 2 etc, and will make Unity look for layer 8, which is our gravGround layer. In my code, I just take the layer number from the user (in the public int variable layerNumber)  and bit shift 1 by that amount to get 0b100000000. 

Finally we have the following!


and



Most of the other code comes from a reworked version of Eteeski FPS tutorial that I mentioned above. More detailed information about created a movable first person character can be found from his videos. 

Phew, and this is just the beginning. Obviously much more work and scripts need to be created for other cases such as jumping between planets and other objects using this gravity. I may make another post in the near future that deals with other nonplayer rigidbodies or jumping between different gravitational bodies. These may require a combination of orienting the player towards a certain point (such as creating gravity just for a sphere) and then when the player lands to check the normal for orientation.

Further details about gravity manipulation can be found on the Gamasutra article: Games Demystified Super Mario Galaxy, and this Unity forum thread about Faux Gravity.

I would love to see what people come up with, or hear any comments you have about this article (good or bad, you can tell me if i'm doing something extremely silly, I will not take it personally). I would love your feedback about any of this moving forward or to hear how you are accomplishing anything interesting in Unity. I probably will not have time to bug fix anyone's specific code or problems but I would be happy to give suggestions on possible solutions. Future articles may be more design focused, we will see.

Thanks for reading!

No comments:

Post a Comment