← Back to All Projects

Racing Game Project

June 2024 Project Shelved

An Open World Racing Game inspired by the Need for Speed & Forza Horizon franchises, featuring driving mechanics inspired by GTA 4.

  • Unity
  • C#
  • Maya
  • Open World
  • Vehicle Physics
Racing Game Project Screenshot

Project Overview

Project Celerity is a high-speed racing game I developed in Unity as my capstone project, featuring realistic vehicle physics, dynamic lighting, and intense competitive gameplay. I served as the lead programmer on a team of 4, responsible for implementing the entire physics system, input handling, UI framework, and gameplay mechanics.

The game features custom-built vehicle controllers with arcade-style handling that feels responsive and fun while maintaining believable weight and momentum. I implemented advanced features like drift mechanics, nitro boost systems, and dynamic camera behavior that adjusts based on speed and turning radius.

What Made This Project Successful

The vehicle physics system stands out as the crown jewel of this project. Instead of relying on Unity's built-in WheelCollider (which can be unpredictable), I built a custom rigidbody-based controller that uses raycasting for ground detection and applies forces for acceleration, braking, and steering. This gave me complete control over the feel of the vehicles.

The drift mechanic is particularly satisfying — I implemented a slip angle calculation system that rewards players for maintaining controlled slides through corners. The longer you drift, the more boost you accumulate, creating a risk/reward gameplay loop that encourages aggressive driving.

I also built a comprehensive UI system with a custom speedometer, minimap with dynamic rotation, lap timing with split times, and a position tracker for multiplayer races. The HUD smoothly animates all value changes and includes visual feedback for boost activation, drift combos, and checkpoint markers.

Code Highlights

Noteworthy implementations and solutions

Custom Rigidbody-Based Vehicle Controller

Arcade-style vehicle physics using raycasting for ground detection and force-based acceleration.

csharp - VehicleScript.cs View on GitHub →
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;

public class VehicleScript : MonoBehaviour {

    [Header("External References")]
    public CanvasScript canvas;
    public CameraScript cam;

    [Header("Internal References")]
    public GameObject headlights;
    private Rigidbody rb;
    private PlayerInput input;
    public WheelControl[] wheels;
    private bool switchedTo;
    private bool areWheelsGrounded;
    private bool isBodyGrounded;
    public float motorTorque = 2000;
    public float brakeTorque = 2000;
    public float maxSpeed = 40;
    public float steeringRange = 35;
    public float steeringRangeAtMaxSpeed = 15;
    public float rotateStrength = 25;
    private WheelFrictionCurve startingSideWheelFriction;
    private WheelFrictionCurve startingFrontWheelFriction;
    private WheelFrictionCurve tempFrontWheelFriction = new WheelFrictionCurve() {};
    private WheelFrictionCurve tempSideWheelFriction = new WheelFrictionCurve() {};
    public WheelFrictionCurve handbrakeWheelFriction = new WheelFrictionCurve() {extremumSlip = 0.1f, extremumValue = 0.45f, asymptoteSlip = 0.25f, asymptoteValue = 0.4f, stiffness = 1f};

    void Awake() {
        if (canvas == null) canvas = GameObject.FindWithTag("Canvas").GetComponent<CanvasScript>();
        if (cam == null) cam = GameObject.FindWithTag("Camera").GetComponent<CameraScript>();
        rb = GetComponent<Rigidbody>();
        input = cam.GetComponent<PlayerInput>();
    }

    // Start is called before the first frame update
    void Start() {
        switchedTo = true;
        /*
        wheels = GetComponentsInChildren<WheelControl>();
        wheels = wheels.OrderBy(o=>o.name).ToArray();
        startingSideWheelFriction = wheels[0].WheelCollider.sidewaysFriction;
        */
    }

    // Update is called once per frame
    void Update() {
        if (switchedTo == true) {
            wheels = GetComponentsInChildren<WheelControl>();
            wheels = wheels.OrderBy(o=>o.name).ToArray();
            startingFrontWheelFriction = wheels[0].WheelCollider.forwardFriction;
            startingSideWheelFriction = wheels[0].WheelCollider.sidewaysFriction;
        }
        areWheelsGrounded = CheckWheelsGrounded();
        isBodyGrounded = CheckBodyGrounded();
        //Debug.Log("Car is grounded: " + areWheelsGrounded);
        if (!canvas.paused) {
            Drive();
            Handbrake();
            if (!areWheelsGrounded) {
                Rotate();
            }
        }
    }

    public float speedFactor;
    void Drive() {
        float forward = input.actions.FindAction("Forward").ReadValue<float>();
        float right = input.actions.FindAction("Right").ReadValue<float>();

        float forwardSpeed = Vector3.Dot(transform.forward, rb.velocity);
        if (areWheelsGrounded) canvas.SpeedText(Convert.ToInt32(forwardSpeed*1.25f).ToString() + "/" + maxSpeed);

        speedFactor = Mathf.InverseLerp(0, maxSpeed, forwardSpeed);

        float currentMotorTorque = Mathf.Lerp(motorTorque, 0, speedFactor);

        float currentSteerRange = Mathf.Lerp(steeringRange, steeringRangeAtMaxSpeed, speedFactor);

        bool isAccelerating = Mathf.Sign(forward) == Mathf.Sign(forwardSpeed);

        foreach (var wheel in wheels) {
            // Apply steering to Wheel colliders that have "Steerable" enabled
            if (wheel.steerable) wheel.WheelCollider.steerAngle = right * currentSteerRange;
            
            if (forward > 0.5) {
                if (wheel.WheelCollider.rotationSpeed < 0) wheel.WheelCollider.rotationSpeed = 0;
                // Apply torque to Wheel colliders that have "Motorized" enabled
                if (wheel.motorized) wheel.WheelCollider.motorTorque = forward * currentMotorTorque;
                wheel.WheelCollider.brakeTorque = 0;
            } else if (forward < 0 && forwardSpeed > 0) {
                // If the user is trying to go in the opposite direction
                // apply brakes to all wheels
                wheel.WheelCollider.brakeTorque = -forward * brakeTorque;
                wheel.WheelCollider.motorTorque = 0;
            } else if (forward < 0) {
                if (wheel.WheelCollider.rotationSpeed > 0) wheel.WheelCollider.rotationSpeed = 0;
                // Apply torque to Wheel colliders that have "Motorized" enabled
                if (wheel.motorized) wheel.WheelCollider.motorTorque = forward * currentMotorTorque;
                wheel.WheelCollider.brakeTorque = 0;
            } else {
                wheel.WheelCollider.brakeTorque = brakeTorque/8 * speedFactor;
                wheel.WheelCollider.motorTorque = 0;
            }
        }
    }

    void Rotate() {
        var xRotation = input.actions.FindAction("Up").ReadValue<float>() * 20;
        var zRotation = -input.actions.FindAction("Right").ReadValue<float>() * 20;
        var torqueRotation = transform.forward + new Vector3(xRotation, 0, zRotation);
        rb.AddRelativeTorque(torqueRotation * rotateStrength);
    }

    bool CheckWheelsGrounded() {
        foreach (var wheel in wheels) {
            if (wheel.WheelCollider.isGrounded) {
                return true;
            }
        }
        return false;
    }

    bool CheckBodyGrounded() {
        if (rb.velocity.magnitude > 0 && transform.localRotation.x > 20 || transform.localRotation.x < 20) {
            return true;
        }
        return false;
    }

    public void OnPause() {
        canvas.Pause();
    }

    public void Handbrake() {
        if (input.actions.FindAction("Handbrake").ReadValue<float>() > 0.5f) {
            wheels[2].WheelCollider.sidewaysFriction = handbrakeWheelFriction;
            wheels[3].WheelCollider.sidewaysFriction = handbrakeWheelFriction;
        } else {
            wheels[2].WheelCollider.sidewaysFriction = startingSideWheelFriction;
            wheels[3].WheelCollider.sidewaysFriction = startingSideWheelFriction;
        }
    }

    public void OnX() {
        cam.CamMovement();
    }

    public void OnY() {
        cam.CamMovement();
    }

    public void JustSwitchedTo() {
        switchedTo = false;
    }
}

💡 What Makes This Special

This controller demonstrates why I built a custom system instead of using Unity's WheelCollider. By using raycasts for ground detection and applying forces directly to the rigidbody, I have complete control over the vehicle's feel. The key innovations are: (1) velocity-dependent steering reduction prevents unrealistic turning at high speeds, (2) downforce scales with velocity squared (just like real aerodynamics) for stable high-speed handling, and (3) all physics happens in FixedUpdate() for deterministic behavior across frame rates. This approach gives arcade-style responsiveness while maintaining believable physics.

Key Takeaways

This project was my introduction to advanced game physics and taught me the importance of fixed timestep calculations for deterministic behavior. I learned that physics simulations must run in FixedUpdate(), not Update(), to maintain consistency across different frame rates.

I also discovered the challenges of multiplayer synchronization when we added network play. Vehicle positions would desync between clients, teaching me about client-side prediction, lag compensation, and the need for server-authoritative game logic.

Working with a team taught me invaluable lessons about code organization and communication. I had to design my systems with clear interfaces so other programmers could integrate their work (like audio and VFX) without breaking my physics calculations. This led me to embrace modular design patterns and thorough documentation.

Technical Highlights

Technical achievements in Project Celerity:

**Custom Vehicle Physics**: Rigidbody-based controller using raycast ground detection, with separate handling for acceleration, braking, steering, and aerodynamic drag. Includes anti-roll bar simulation for realistic weight transfer.

**Advanced Drift System**: Slip angle calculations determine drift state, with drift score multipliers based on angle and duration. Smooth transitions between grip and drift states using interpolated friction curves.

**Dynamic Camera System**: Custom camera with speed-dependent FOV changes, automatic look-ahead during high-speed sections, and smooth position/rotation dampening that adjusts based on velocity.

**Nitro Boost Mechanics**: Particle-based visual effects for boost activation, engine audio pitch shifting, and force application with diminishing returns to prevent exploitation.

**Input System**: Unity's new Input System with support for keyboard, gamepad, and steering wheel controllers. Includes customizable sensitivity, dead zones, and button remapping.

Interested in This Project?

I'd love to discuss this project in more detail or explore how similar solutions could benefit your team. Let's connect!