Gravity game was created as a first person shooter project in my scripting for games class. It has two primary mechanics: The gravity book “attack” and the player controller/movement. Both of these are mechanically complex and required me to learn a lot about vector mathematics.

Game Overview

The game’s player controller allows for movement on any orientation of surface, allowing the player to select a nearby surface to jump onto, which becomes the new ground. The player’s gravity is oriented to match this new ground.

By using a raycast to find the normal of the surface below the player I was able to rotate the player to the correct orientation and base all physics off of that new normal.

public class StickToGround : MonoBehaviour
{
    [SerializeField] private Transform feet;
    [SerializeField] private bool useGravity = true;
    [SerializeField] private float jumpRange = 15f;
    private float gravityScale;
    public bool grounded;
    public Location surface;
    public Vector3 movement;
    private Rigidbody rb;
    private ConstantForce cf;
    private RaycastHit hit;
    private bool jumping;
    private Vector3 jumpTarget;
    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        cf = GetComponent<ConstantForce>();
        if (Physics.Raycast(new Ray(transform.position, -transform.up), out hit))
        {
            surface = new Location(hit);
        }
        gravityScale = 100 * rb.mass;
    }
    private void OnCollisionEnter(Collision collision)
    {
        grounded = true;
        if(useGravity) cf.force = Vector3.zero;
    }
    private void FixedUpdate()
    {
        if (!jumping)
        {
            if (Physics.Raycast(new Ray(feet.position, -transform.up), out hit, 1f))
            {
                surface = new Location(hit);
                if (surface.transform.GetComponent<Rigidbody>())
                {
                    movement += surface.transform.GetComponent<Rigidbody>().velocity * Time.fixedDeltaTime +
                        surface.transform.position + Quaternion.Euler(surface.transform.GetComponent<Rigidbody>().angularVelocity * Time.fixedDeltaTime * Mathf.Rad2Deg) * (transform.position - surface.transform.position) - transform.position;
                }
                transform.rotation = Quaternion.FromToRotation(Vector3.up, surface.normal);
            }
            else if (useGravity)
            {
                grounded = false;
                if (surface != null) cf.force = -surface.normal * gravityScale;
                else cf.force = -transform.up * gravityScale;
            }
            //Debug.DrawLine(transform.position, transform.position + movement, Color.green, 30);
            //if (surface != null) Debug.DrawRay(surface.point, surface.normal, Color.black, 30);
            rb.MovePosition(transform.position + movement);
            movement = Vector3.zero;
        }
    }
    public void Jump()
    {
        Ray ray = Camera.main.ScreenPointToRay(new Vector3(Camera.main.pixelWidth / 2, Camera.main.pixelHeight / 2));
        if (Physics.Raycast(ray, out hit, jumpRange))
        {
            surface = new Location(hit);
            StartCoroutine(LerpJump(transform.position, transform.rotation));
        }
    }
    private IEnumerator LerpJump(Vector3 landingStartPos, Quaternion landingStartRot)
    {
        jumping = true;
        float threshold = Vector3.Distance(landingStartPos, surface.point)/40f;
        Quaternion targetRot = Quaternion.FromToRotation(Vector3.up, surface.normal);
        float lerpTime = 0;
        while (Vector3.Distance(transform.position, jumpTarget) >= threshold && transform.up != surface.normal)
        {
            jumpTarget = surface.point + surface.normal * .1f + surface.normal * Vector3.Distance(feet.position, transform.position);
            lerpTime += Time.deltaTime;
            transform.position = Vector3.Lerp(landingStartPos, jumpTarget, lerpTime / Mathf.Sqrt((jumpTarget - landingStartPos).magnitude / 40));
            transform.rotation = Quaternion.Slerp(landingStartRot, targetRot, lerpTime / Mathf.Sqrt((jumpTarget - landingStartPos).magnitude / 50));
            yield return new WaitForFixedUpdate();
        }
        transform.position = jumpTarget;
        lerpTime = 0;
        jumping = false;
    }
}

I didn’t want to make a conventional first person shooter, so I had an idea that fit with the theme of my game. The gravity book uses raycasts to select points on the surfaces of objects, one point for left mouse button and one for right mouse button. Once both points are selected, those objects are drawn to each other via a force applied at that point (with some other rules applied to make sure the motion acts like how I wanted it to). A a ridgidbody can be draw to a stationary object. The selected points are stored in a container class I created called Location.

private void ObjectGrav(int num)
    {
        if(Cursor.lockState == CursorLockMode.Locked && Physics.Raycast(Camera.main.ScreenPointToRay(new Vector3(Camera.main.pixelWidth / 2, Camera.main.pixelHeight / 2)), out hit, gravDistMax))
        {
            if (gravObjs[num])
            {
                gravPages[num].SetActive(false);
                Destroy(gravTargeters[num]);
                foreach (GameObject g in gravObjs)
                    if (g && g.GetComponent<Rigidbody>())
                    {
                        g.GetComponent<Rigidbody>().velocity = Vector3.zero;
                        g.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
                    }
                gravObjs[num] = null;
                gravPoints[num] = null;
            }
            currentAmmo--;
            AudioSource.PlayClipAtPoint(gravSound, hit.point, .6f);
            gravPoints[num] = new Location(hit.transform, hit.point, hit.normal);
            gravTargeters[num] = Instantiate(targeter, hit.collider.gameObject.transform, false);
            gravTargeters[num].transform.position = hit.point;
            gravTargeters[num].transform.localScale = new Vector3(.2f / hit.transform.lossyScale.x, .2f / hit.transform.lossyScale.y, .2f / hit.transform.lossyScale.z);
            gravPages[num].SetActive(true);
            gravObjs[num] = hit.collider.gameObject;
        }
        else
        {
            if(!distMarker2) Destroy(distMarker2 = Instantiate(distMarker, cameraTransform.position + Camera.main.ScreenPointToRay(new Vector3(Camera.main.pixelWidth / 2, Camera.main.pixelHeight / 2)).direction * gravDistMax, cameraTransform.rotation, cameraTransform), 1f);
            if (gravObjs[num])
            {
                gravPages[num].SetActive(false);
                Destroy(gravTargeters[num]);
                foreach (GameObject g in gravObjs)
                    if (g && g.GetComponent<Rigidbody>())
                    {
                        g.GetComponent<Rigidbody>().velocity = Vector3.zero;
                        g.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
                    }
                gravObjs[num] = null;
                gravPoints[num] = null;
            }
        }

Stores the targeted objects transform, and initial information about a point in relation to that object. It accepts multiple types of inputs including a RaycastHit, ContactPoint, and raw transform data. It can return a point in relation to to that object based on its initial and current position and rotation.

public class Location //Used to keep track of a point on a moving/rotating surface over time
{
    public Transform transform;
    private Quaternion rotFromUp;
    private float distFromCenter;
    private Quaternion surfaceNormalFromUp;
    public Vector3 point { get { return rotFromUp * -transform.up * distFromCenter + transform.position; } }
    public Vector3 normal { get { return surfaceNormalFromUp * transform.up; } }
    public Location(RaycastHit hit)
    {
        transform = hit.transform;
        rotFromUp = Quaternion.FromToRotation(transform.up, transform.position - hit.point);
        surfaceNormalFromUp = Quaternion.FromToRotation(transform.up, hit.normal);
        distFromCenter = Vector3.Distance(transform.position, hit.point);
    }
    public Location(ContactPoint cp)
    {
        transform = cp.otherCollider.transform;
        rotFromUp = Quaternion.FromToRotation(transform.up, transform.position - cp.point);
        surfaceNormalFromUp = Quaternion.FromToRotation(transform.up, cp.normal);
        distFromCenter = Vector3.Distance(transform.position, cp.point);
    }
    public Location(Transform objTransform, Vector3 surfacePoint, Vector3 normal)
    {
        transform = objTransform;
        rotFromUp = Quaternion.FromToRotation(transform.up, transform.position - surfacePoint);
        surfaceNormalFromUp = Quaternion.FromToRotation(transform.up, normal);
        distFromCenter = Vector3.Distance(transform.position, surfacePoint);
    }
}