FUTURISTIC RACER PROTOTYPE

Description

I began this project as a challenge to see if I am able to make a vehicle similar to those found in F-Zero X and also as an introduction to physics programming. The physics were written without the use of Unity's Rigidbody component. I also did all the programming for the game.

This vertical slice was developed in my spare time over a period of 3 months alongside my studies at Grafisch Lyceum Utrecht.

Work
  • AI racer that follows a spline.
  • Arcade physics from scratch
  • Audio visualization shader for buildings
  • Lap system that keeps track of lap count and time
  • Implemented UI in Unity
Team

Art

Programming

  • Akli Lounès Touati
Code Snippets
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// This script regulates the vehicle's hover height. 
/// </summary>
public class CarHoverHandler : MonoBehaviour
{
    private void Awake()
    {
        _Manager = GetComponent<CarManager>();
    }

    private void FixedUpdate()
    {
        CalculatetHover();
        RotateObjectToNormal();

        transform.Translate(InterpolatedGroundNormal * HoverVelocity * Time.deltaTime, Space.World);

        // Store values to be used in next frame's calculation
        SetPreFaceGroundNormal(FacedGroundNormal);
        SetPreInterpolatedGroundNormal(InterpolatedGroundNormal);
        SetPreHoverVelocoty(HoverVelocity);
    }

    /// <summary>
    /// Looks at vehicles position on a mesh and calculates correcteve forces 
    /// </summary>
    private void CalculateHover()
    {
        Vector3 faceGroundNormal = new Vector3();
        Vector3 interpolatedGroundNormal = new Vector3();

        float force = 0;
        float gravity = 0;

        Ray ray = new Ray(transform.position + (_Manager.RaycastYOffset * transform.up), -transform.up);
        RaycastHit hitInfo;

        // We shoot a ray downward to get mash data of the mesh where we are currently driving.
        if (Physics.Raycast(ray, out hitInfo, _Manager.MaxGroundDistence + _Manager.RaycastYOffset, _Manager.GroundMask))
        {
            // We check if the mesh's ID has changed. If so we copy over the mesh's normals and mesh triangles.
            if (MeshData._ID != hitInfo.collider.gameObject.GetComponent<MeshData>()._ID)
            {
                SetMeshData(hitInfo.collider.gameObject.GetComponent<MeshData>());
                SetNormals(MeshData._Normals);
                SetTriangles(MeshData._Triangles);
            }

            // We calculate an interpolated normal of the hitposition of our raycast.
            Transform hitTransform = hitInfo.collider.transform;
            Vector3 n0 = _Normals[_Triangles[hitInfo.triangleIndex * 3 + 0]];
            Vector3 n1 = _Normals[_Triangles[hitInfo.triangleIndex * 3 + 1]];
            Vector3 n2 = _Normals[_Triangles[hitInfo.triangleIndex * 3 + 2]];
            Vector3 baryCenter = hitInfo.barycentricCoordinate;
            interpolatedGroundNormal = n0 * baryCenter.x + n1 * baryCenter.y + n2 * baryCenter.z;
            interpolatedGroundNormal = interpolatedGroundNormal.normalized;
            interpolatedGroundNormal = hitTransform.TransformDirection(interpolatedGroundNormal);
            faceGroundNormal = hitInfo.normal;

            //We calculate a corrective force based on the vehicle's distance from the ground and flag the vehicle as grounded
            float height = hitInfo.distance - _Manager.RaycastYOffset;
            float forcePercent = _Manager.HoverPID.Seek(_Manager.HoverHeight, height);
            force = _Manager.HoverForce * forcePercent;
            gravity = _Manager.HoverGravity * height;
            SetIsGrounded(true);

            // If the vehicle is intersecting with the road we nullify all hover forces and gravity so the vehicle will not translate through the road.
            if (height <= 0 && Mathf.Sign(height) != Mathf.Sign(PreHeight) && HoverVelocity < 0)
            {
                SetHoverVelocoty(0);
            }

            // We store the vehicles height for next frame's calculation
            SetPreHeight(height);
        }
        // If the raycast does not hit anything the vehicle is airborn.
        else
        {
            interpolatedGroundNormal = PreInterpolatedGroundNormal;
            gravity = _Manager.FallGravity;
            SetIsGrounded(false);
        }

        SetFaceGroundNormal(faceGroundNormal);
        SetInterpolatedGroundNormal(interpolatedGroundNormal);
        SetHoverVelocoty(HoverVelocity + (force * Time.deltaTime));     //adds upwards force to HoverVelocity.  
        SetHoverVelocoty(HoverVelocity - (gravity * Time.deltaTime));   //adds downwards force to HoverVelocity. 
    }

    /// <summary>
    /// Rotates the Collider, Model and orientation to InterpolatedGroundNormal.
    /// </summary>
    private void RotateObjectToNormal()
    {
        transform.rotation = Quaternion.FromToRotation(transform.up, InterpolatedGroundNormal) * transform.rotation;
        Manager.SetModelRotation(Quaternion.Slerp(Manager.ModelTransform.rotation, transform.rotation, 4 * Time.fixedDeltaTime));
        Manager.SetUpOrientationRotation(Quaternion.Slerp(Manager.OrientationLookAtTransform.rotation, transform.rotation, 7 * Time.fixedDeltaTime));
    }
    
    private CarManager _Manager;

    private MeshData _MeshData;                     // The MeshData of the mesh that the car is currently driving on
    private Vector3[] _Normals;                     // All vertex normals of the mesh that the car is currently driving on
    private int[] _Triangles;                       // All Tri's of the mesh that the car is currently driving on
    private Vector3 _FaceGroundNormal;              // Face normal of the of the tri the car is currently driving on
    private Vector3 _PreFaceGroundNormal;           // Face normal of the of the tri the car was driving on on the previous fixed update
    private Vector3 _InterpolatedGroundNormal;      // Interpolated normal of the 3 vertecies of the tri the car is currently driving on
    private Vector3 _PreInterpolatedGroundNormal;   // Interpolated normal of the 3 vertecies of the tri the car was driving on on the previous fixed update
    private float _HoverVelocity;                   // the force that is aplied to the car to get it to the ideal hover height
    private float _PreHoverVelocity;                // The hover velocity of the car on the previous fixed time step
    private float _PreHeight;                       // The height of the car above the track on the previous fixed time step
    private bool _IsGrounded;                     

    #region Prop  
    public CarManager Manager { get { return _Manager; } }
    public MeshData MeshData { get { return _MeshData; } }
    public Vector3[] Normals { get { return _Normals; } }
    public int[] Triangles { get { return _Triangles; } }
    public Vector3 FacedGroundNormal { get { return _FaceGroundNormal; } }
    public Vector3 PreFacedGroundNormal { get { return _PreFaceGroundNormal; } }
    public Vector3 InterpolatedGroundNormal { get { return _InterpolatedGroundNormal; } }
    public Vector3 PreInterpolatedGroundNormal { get { return _PreInterpolatedGroundNormal; } }
    public float HoverVelocity { get { return _HoverVelocity; } }
    public float PreHoverVelocity { get { return _PreHoverVelocity; } }
    public float PreHeight { get { return _PreHeight; } }
    public bool Grounded { get { return _IsGrounded; } }

    public void SetMeshData(MeshData newMeshData)
    {
        _MeshData = newMeshData;
    }
    public void SetNormals(Vector3[] newNormalArray)
    {
        _Normals = newNormalArray;
    }
    public void SetTriangles(int[] newTrianglesArray)
    {
        _Triangles = newTrianglesArray;
    }
    public void SetFaceGroundNormal(Vector3 newFaceGroundNormal)
    {
        _FaceGroundNormal = newFaceGroundNormal;
    }
    public void SetPreFaceGroundNormal(Vector3 newPreFaceGroundNormal)
    {
        _PreFaceGroundNormal = newPreFaceGroundNormal;
    }
    public void SetInterpolatedGroundNormal(Vector3 newInterpolatedGroundNormal)
    {
        _InterpolatedGroundNormal = newInterpolatedGroundNormal;
    }
    public void SetPreInterpolatedGroundNormal(Vector3 newPreInterpolatedGroundNormal)
    {
        _PreInterpolatedGroundNormal = newPreInterpolatedGroundNormal;
    }
    public void SetHoverVelocoty(float newVelocity)
    {
        _HoverVelocity = newVelocity;
    }
    public void SetPreHoverVelocoty(float newPreVelocity)
    {
        _PreHoverVelocity = newPreVelocity;
    }
    public void SetPreHeight(float newPreHeight)
    {
        _PreHeight = newPreHeight;
    }
    public void SetIsGrounded(bool newIsGrounded)
    {
        _IsGrounded = newIsGrounded;
    }
    #endregion
}