Valhalla Racing

Lead scripter | Game designer | Scrum master

Genre: Kart racing

Engine: Unity

Language: C#

Development time: 6 Weeks

Team: 3 game designers + 4 3D artists + 2 2D artist

Lead Scripter

  • Script kart physics model
  • Script racing logic

Scrum Master

  • Maintain scrum rituals
  • Create and maintain backlog
  • Report project progress and track velocity

Valhalla Racing is a multi-player kart racing game featuring interactive levels that twist, turn, and produce awesome hazards, in a modernized viking setting.

In Valhalla Racing the players must embrace the chaos that comes with environmental changes. Players can interact with the environment to pave the way for shortcuts, close off roads, and sabotage for their opponents all while avoiding dangerous hazards.

Lead Scripter

As lead scripter in this project I gained a lot of useful experience. Creating and scripting the physics model of the kart taught me much about how game engines handle physics. I also needed to create an easily understandable and adaptable system for hazards and events to affect the kart in different ways that could be used and extended by the other scripters.

e

Scrum Master

As scrum master I learned how to identify and improve different workflow problems within the team, to have retrospectives each week where we discussed the project progression and tried to solve different problems that had occurred. I kept track of these problems and followed up each week to make sure the workflow had improved or if we needed to change something else.

Kart physics model

The basics

The core mechanic of Valhalla Racing is the movement of the karts themselves. We had lots of options but decided against using pre-existing models like Unity’s Wheel Collider or a similar solution and decided to create our own kart physics model but still using the build in the physics engine. This would give us the more flexibility to create the right driving for the game.

The Kart is pushed up from the ground on four points above the wheels,  moves by applying a force forward and is turned by adding torque around its center. This created an easy-to-control kart with many different variables that could be tweaked and modified for a better driving feel.

The kart has no friction to the ground since it’s hovering above it. This caused the kart to start drifting in turns. The kart moved diagonally instead of forward and creating a drift sideways. To counter this we add a force to the kart in the opposite direction of the drift to simulate the friction the wheels would have had.

By connecting the strength of this counterforce to a button we created a player controlled drift that added a lot to the driving experience.

The resulting movement of the kart gave it a fun, arcade like and playful feeling. The drifting added a layer of skill to master making the driving more engaging. 

The kart is being pushed up on the points H1-H4, moved forward by the force V and turned by the torque R.

The kart is drifting in the direction V. To make it move forward, in the direction V’, the force F is applied. F can be manipulated to create player controlled drift.

The plane P is calculated by the ray casts H1-H4. The forward force vector V of the cart is projected onto P to create the new force vector V’.

Tweaking

Some tweaking had to be made to get better kart physics. The force applied to the kart to make it go forward couldn’t be applied in the karts forward vector. If the kart was slightly tipped upwards, for example after a bump or a jump, applying a force in the forward vector of the kart would launch it slightly upward.

To counter this I project the forward vector of the kart onto the plane that the kart is driving on.The driving plane is being calculated by the same ray cast that calculates the force for the wheels.

This solution prevented the kart from moving upwards while bumping into the road or jumping.

 

Show code implementation of HoverController in C#

The Hover controller pushes the Kart Up on four specified points.

using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VikingBoxCarRacing.Interfaces;

public class HoverGravityController : MonoBehaviour
{
    [System.Serializable]
    protected struct HitData
    {
        public Vector3 Normal;
        public Vector3 Position;
        public float SuspensionRate;
        public bool Grounded;

        public HitData(Vector3 normal, Vector3 position, float suspensionRate, bool grounded)
        {
            this.Normal = normal;
            this.Position = position;
            this.SuspensionRate = suspensionRate;
            this.Grounded = grounded;
        }
    }

    public static float globalGravity = -9.81f;
    [Header("Gravity")]
    public float GrabvityScale = 3f;

    [Header("Hover Forces")]
    public float hoverForce = 1000;
    public float PushDownForce = 1000;

    [Header("Hover raycast")]
    public float hoverHeight = 1.5f;
    public LayerMask HitLayer;
    public GameObject[] hoverPoints;

    [Header("Self levelling")]
    public bool SelfLeveling = true;
    public float SelfLevelingHeight = 0.25f;
    public float SelfLevelMaxAngle = 90f;

    [Header("Angle")]
    [Tooltip("The maximum angle that registers as a gripable surface")]
    [Range(0,90)]
    public float GripAngle = 60f;

    [Header("Debug")]
    public bool debug = false;

    //HitDatas
    private HitData[] hitData;

    /// <summary>
    /// Returns a composite Normal for each plane that is created between wheels that is touching the ground.
    /// This is used when applying the moving force so that we don't launch the kart into the air and always
    /// apply force along stide the ground.
    /// </summary>
    public Vector3 HoverPlaneNormal
    {
        get
        {

            Vector3 CompositeNormal = new Vector3(0, 0, 0);

            if (hitData.Count(x => x.Grounded) < 3)
            {
                var normals = hitData.Where(x => x.Grounded).Select(x => x.Normal);
                foreach (var normal in normals)
                {
                    CompositeNormal += normal;
                }

                return CompositeNormal.normalized;
            }


            //0
            if (hitData[3].Grounded && hitData[2].Grounded && hitData[1].Grounded)
            {
                var p = new Plane();
                p.Set3Points(hitData[3].Position, hitData[2].Position, hitData[1].Position);
                CompositeNormal += p.normal;
            }

            //1
            if (hitData[3].Grounded && hitData[2].Grounded && hitData[0].Grounded)
            {
                var p = new Plane();
                p.Set3Points(hitData[3].Position, hitData[2].Position, hitData[0].Position);
                CompositeNormal += p.normal;
            }

            //2
            if (hitData[3].Grounded && hitData[1].Grounded && hitData[0].Grounded)
            {
                var p = new Plane();
                p.Set3Points(hitData[3].Position, hitData[1].Position, hitData[0].Position);
                CompositeNormal += p.normal;
            }

            ///3
            if (hitData[2].Grounded && hitData[1].Grounded && hitData[0].Grounded)
            {
                var p = new Plane();
                p.Set3Points(hitData[2].Position, hitData[1].Position, hitData[0].Position);
                CompositeNormal += p.normal;
            }

            return CompositeNormal.normalized;
        }
    }

    public float[] HoverHitHeights
    {
        get
        {
            return hitData.Select(x => (1f - x.SuspensionRate) * hoverHeight).ToArray();
        }
    }
    //private bool grounded = false;
    public bool Grounded
    {
        get
        {
            return hitData.Count(x => x.Grounded) > 0;
        }
    }

    private Vector3 groundPoint;
    public Vector3 GroundPoint
    {
        get
        {
            if (Application.isEditor && !Application.isPlaying)
            {
                return transform.position - Vector3.up * hoverHeight;
            }
            return groundPoint;
        }
    }

    //Components
    private Rigidbody body;

    void Start()
    {
        hitData = new HitData[hoverPoints.Length];

        body = GetComponent<Rigidbody>();
        body.centerOfMass = Vector3.down;
        body.useGravity = false;
        groundPoint = transform.position - Vector3.up * hoverHeight;
    }

    void FixedUpdate()
    {

        //  To hover force for each point.
        RaycastHit hit;
        for (int i = 0; i < hoverPoints.Length; i++)
        {
            var hoverPoint = hoverPoints[i];
            if (Physics.Raycast(hoverPoint.transform.position, -Vector3.up, out hit, hoverHeight, HitLayer.value))
            {
                //Hit Ground
                var suspensionRate = (1.0f - (hit.distance / hoverHeight));

                //Check if hit surface normal is steeper than 60 dagrees
                var grounded = Vector3.Angle(hit.normal, Vector3.up) < GripAngle;

                //Slerp:
                var forceVector = Vector3.Slerp(Vector3.zero, hit.normal * hoverForce, suspensionRate);
                //var forceVector = hit.normal * hoverForce * suspensionRate,
                if (grounded)
                {
                    body.AddForceAtPosition(forceVector, hoverPoint.transform.position);
                }
                hitData[i] = new HitData(hit.normal, hit.point, suspensionRate, grounded);
            }
            else
            {
                //Miss ground
                hitData[i] = new HitData(hit.normal, hit.point, 0, false); ;

                //// Self levelling - returns the vehicle to horizontal when not grounded
                if (SelfLeveling && Vector3.Angle(transform.up, Vector3.up) < SelfLevelMaxAngle)
                {
                    //If wheel is above self-leveling limit, push it down to level the kart.
                    if (transform.position.y > hoverPoint.transform.position.y + SelfLevelingHeight)
                    {
                        body.AddForceAtPosition(hoverPoint.transform.up * PushDownForce, hoverPoint.transform.position);
                    }
                    else if (Vector3.Angle(HoverPlaneNormal, Vector3.up) < SelfLevelMaxAngle && Grounded)
                    {
                        body.AddForceAtPosition(hoverPoint.transform.up * -PushDownForce, hoverPoint.transform.position);
                    }
                }
            }

            if (debug)
            {
                Debug.DrawRay(hoverPoint.transform.position, -Vector3.up * hit.distance, Color.red, 0, false);
            }
        }

        //Get Ground position;
        if (Physics.Raycast(transform.position, -Vector3.up, out hit, hoverHeight * 2, HitLayer.value))
        {
            groundPoint = hit.point;
        } else{
            groundPoint = transform.position -Vector3.up * hoverHeight*2;
        }

        if (debug)
        {
            Debug.DrawRay(transform.position, -Vector3.up * hit.distance, Color.blue, 0, false);
        }

        OwnGravity();
    }

    //We use our own gravity to create less floaty kart physics.
    private void OwnGravity()
    {
        Vector3 gravity = globalGravity * GrabvityScale * Vector3.up;
        body.AddForce(gravity, ForceMode.Acceleration);
    }
}

Show code implementation of EngineController in C#

EngineController moves and turns the kart. It also is responsible for the boost and drifting.

using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VikingBoxCarRacing.Interfaces;

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(HoverGravityController))]
public class EngineController : MonoBehaviour {

    [Header("Drag")]
    public float GroundDrag = 3f;
    public float AirDrag = 0.1f;

    [Header("Movement")]
    public float ForwardAcceleration = 8000f;
    public float ReverseAcceleration = 4000f;
    public float MaxSpeed = 50;
    [Tooltip("The speed of wich the kart max speed is reduced when driving on diffrent terrains")]
    public float SlowDownSpeed = 4;
    [Tooltip("The Maximum angle of the driving plane normal to give throttle")]
    [Range(0, 90)]
    public float ThrothleMaxAngle = 45;

    [Header("Turn")]
    [Tooltip("Turn strenght if the kart is standing still")]
    public float MinSpeedTurnStrenght = 200;
    [Tooltip("Turn strenght if the kart is at max speed")]
    public float MaxSpeedTurnStrength = 100;
    [Tooltip("Curve to get the ratio between min and max turn strenght depending on the % of max speed")]
    public AnimationCurve TurnStrenghtCurve;

    [Header("DriftReduction")]
    public float DriftReductionForce = 1000;

    [Header("Drift")]
    [Tooltip("The factor to multiply drift  reduction with while drifting, determines how mush side movement the kart will have.")]
    [Range(0f, 1f)]
    public float DriftFactor = 0.5f;
    public float DriftTurnStrenght = 200f;
    [Range(0f, 1f)]
    public float DriftMinTurn = 0.5f;
    [Range(0f, 1f)]
    public float DriftMedianTurn = 0.75f;
    public float DriftMinSpeed = 5;


    [Header("Drift Boost")]
    public AnimationCurve DriftBoostActivationCurve;
    public float DriftBoostDuration = 1f;
    public float DriftBoostForce = 12000f;
    public float DriftBoostMaxSpeed = 60f;

    //Drift
    private float driftTurnDir;
    private bool driftActive;
    public bool DriftActive
    {
        get { return driftActive; }
    }
    private float driftDuration;


    //Boost
    private bool boost = false;
    private float boostDuration;
    private float boostDurationTotal;
    private float boostMaxSpeed;
    private float boostLeftPercent;
    private float boostForce;

    //Speed;
    private float currentMaxSpeed;
    private float currentSpeedFactor = 1f;
    private float currDirAbs = 0;
    private float currentDir = 0;

    //Components
    private Rigidbody body;
    private HoverGravityController hover;


    /// Collection of Kart direction and velocity
    /// Used to calculate engine output to the kart.
    #region

    /// <summary>
    /// Kart velocity in the world.
    /// </summary>
    public Vector3 KartVelocity
    {
        get
        {
            if (body == null)
            {
                return Vector3.zero;
            }
            return body.velocity;
        }
    }
    /// <summary>
    /// Kart velocity horizontal veclocity.
    /// </summary>
    public Vector3 KartHorizontalVelocity
    {
        get
        {
            var kartVelocity = KartVelocity;
            kartVelocity.y = 0;
            return kartVelocity;
        }
    }

    /// <summary>
    /// Kart velocity relative to itself.
    /// </summary>
    public Vector3 KartRelativeVelocity
    {
        get
        {
            return transform.InverseTransformDirection(KartVelocity);
        }
    }

    /// <summary>
    /// Kart horizontal velocity relative to itself.
    /// </summary>
    public Vector3 KartRelativeHorizontalVelocity
    {
        get
        {
            var relativeVelocity = KartRelativeVelocity;
            relativeVelocity.y = 0;
            return relativeVelocity;
        }
    }

    /// <summary>
    /// Kart direction in the world.
    /// </summary>
    public Vector3 KartDirection
    {
        get
        {
            if (body == null)
            {
                return Vector3.zero;
            }

            return body.velocity.normalized;
        }
    }

    /// <summary>
    /// Kart horizontal direction in the world.
    /// </summary>
    public Vector3 KartHorizontalDirection
    {
        get
        {
            var direction = KartDirection;
            direction.y = 0;
            return direction.normalized;
        }
    }

    /// <summary>
    /// Kart direction relative to itself.
    /// </summary>
    public Vector3 KartRelativeDirection
    {
        get
        {
            return transform.InverseTransformDirection(KartDirection);
        }
    }

    /// <summary>
    /// Kart horizontal direction relative to itself.
    /// </summary>
    public Vector3 KartRelativeHorizontalDirection
    {
        get
        {
            var relativeDirection = KartRelativeDirection;
            relativeDirection.y = 0;
            return relativeDirection.normalized;
        }
    }
    #endregion

    void Start () {
        body = GetComponent<Rigidbody>();
        hover = GetComponent<HoverGravityController>();

        if (body == null || hover == null)
        {
            throw new System.Exception("Required component for engine missing!");
        }

        currentMaxSpeed = MaxSpeed;
    }

    private void FixedUpdate()
    {
        if (hover.Grounded)
        {
            body.drag = GroundDrag;
        }
        else
        {
            body.drag = AirDrag;
        }
        //Boost
        BoostFixedUpdate();
    }

    /// <summary>
    /// Moves and turns the kart.
    /// Is Called from Fixed Update in PlayerController.
    /// </summary>
    public void Move(float turn, float dir, bool drift) {
        var thrust = 0f;
        currentDir = dir;
        currDirAbs = Mathf.Abs(dir);

        // Handle Forward and Reverse forces
        if (dir > 0)
        {
            thrust = dir * ForwardAcceleration;
        } else if(dir < 0)
        {
            thrust = dir * ReverseAcceleration;
        }
        Move(thrust);

        //Calculate  special case turn direction if drifting.
        float turnDir;
        if (driftActive)
        {
            driftDuration += Time.deltaTime;
            var turnAddage = (turn / driftTurnDir);
            var driftTop = 1 - DriftMedianTurn;
            var driftBottom = 1 - DriftMinTurn - driftTop;
            var driftTurnModification = (turnAddage > 0 ? driftTop : driftBottom) * turnAddage;
            var driftTurn = DriftMedianTurn + driftTurnModification;
            turnDir = driftTurn * driftTurnDir;
        } else
        {
            turnDir = turn;
        }

        //Inverse Turn if kart is going backways
        if (KartRelativeHorizontalVelocity.sqrMagnitude > 0.01 && Mathf.Sign(KartRelativeHorizontalVelocity.z) == -1)
        {
            turnDir = -turnDir;
        }
        TurnKart(turnDir, drift);

        //Limit horizontal speed when on the ground.
        if (KartRelativeHorizontalVelocity.magnitude > MaxSpeed * currentSpeedFactor && hover.Grounded)
        {
            var newVelocityVector = currentMaxSpeed * currentSpeedFactor * KartRelativeHorizontalDirection;
            var verticalVelocity = KartRelativeVelocity.y;
            newVelocityVector.y = verticalVelocity;
            body.velocity = transform.TransformVector(newVelocityVector);
        }

        DriftReduction(driftActive);
    }

    private void Move(float thrust)
    {
        //Project force vector onto the hover plane.
        //This prevents from the kart being lanched to the air.
        var planeNormal = hover.HoverPlaneNormal;
        Vector3 forceVector = Vector3.ProjectOnPlane(transform.forward, planeNormal);
        if (Vector3.Angle(planeNormal, Vector3.up) > ThrothleMaxAngle)
        {
            forceVector = Vector3.zero;
        }

        if (Mathf.Abs(thrust) > 0 && hover.Grounded)
        {
            body.AddForce(forceVector * thrust);
        }
    }

    private void TurnKart(float turnDir, bool drift)
    {
        float turnRatio = TurnStrenghtCurve.Evaluate(KartRelativeHorizontalVelocity.magnitude / MaxSpeed);
        float regularTurnStrenght = Mathf.Lerp(MinSpeedTurnStrenght, MaxSpeedTurnStrength, turnRatio);
        var turnStrength = drift ? DriftTurnStrenght : regularTurnStrenght;

        //Only add torque if actually turning.
        if (Mathf.Abs(turnDir) > 0f)
        {
            body.AddRelativeTorque(Vector3.up * turnDir * turnStrength);
        }

    }

    private float CurrentSlipForce;
    private void DriftReduction(bool driftActive)
    {
            var relativeVelocity = transform.InverseTransformDirection(body.velocity);
            var sideWayVelocity = transform.TransformDirection(new Vector3(-relativeVelocity.x, 0, 0));

            //Add less anti-drift force if drift is active.
            var targetSlipForce = DriftReductionForce * (driftActive ? DriftFactor : 1);
            CurrentSlipForce = CurrentSlipForce > targetSlipForce ? Mathf.Lerp(CurrentSlipForce, targetSlipForce, Time.deltaTime * 14) 
              : Mathf.Lerp(CurrentSlipForce, targetSlipForce, Time.deltaTime);
            body.AddForce(sideWayVelocity * CurrentSlipForce);
    }

    /// <summary>
    /// Boost the kart, forcing it to move and adds to  acceleration, maxSpeed during a duration
    /// </summary>
    public void Boost(float accelerationForce, float maxSpeed, float duration)
    {
        boost = true;
        boostMaxSpeed = maxSpeed;
        boostForce = accelerationForce;
        boostDuration = duration;
        boostDurationTotal = duration;
    }

    private void BoostFixedUpdate()
    {
        if ((boostDuration -= Time.deltaTime) > 0 && boost)
        {
            boostLeftPercent = boostDuration / boostDurationTotal;
            currentMaxSpeed = Mathf.Lerp(MaxSpeed, Mathf.Max(MaxSpeed, boostMaxSpeed), boostLeftPercent);
            var boostForceStrenght = Mathf.Lerp(0, boostForce, boostLeftPercent);
            if (hover.Grounded)
            {
                Vector3 forceVector = Vector3.ProjectOnPlane(transform.forward, hover.HoverPlaneNormal);
                body.AddForce(forceVector * boostForceStrenght);
            }
        }
        else if (boost)
        {
            BoostStop();
        }
    }

    private void BoostStop()
    {
        boost = false;
        currentMaxSpeed = MaxSpeed;
        boostLeftPercent = 0f;
    }

    /// <summary>
    /// Resets all movement of the kart. Used when respawning.
    /// </summary>
    public void ResetMovement()
    {
        body.velocity = Vector3.zero;
        body.angularVelocity = Vector3.zero;
        BoostStop();
    }
}

Hazards

Since the kart physics model was implemented using the physics engine, the implementation of the hazards in the game became easier and faster. The hazards like the geyser and the wind on the bridge added forces and torques to the kart, shooting it up in the air, pushing it to the side and turning it left or right.

This created fun and dynamic gameplay since the player needs to counter some of these forces by steering and maneuver the kart.

The quick and easy implementation of the hazards proved that it was a valuable investment in time to implement a robust and flexible physics model for the cart. If we had continued to develop the game, implementation of new hazards like water streams or falling rocks would have been quick and easy to implement and test.

 

Show code implementation of geyser hazard controller in C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GeyserController : MonoBehaviour {

    [Tooltip("Force of geyser hit, Hight number meens the kart is launched higher up in the air")]
    public float GeyserForce = 7000;

    [Tooltip("Lower VelocityLoss results in a lower kart speed on collision with the geyser")]
    [Range(0f,1f)]
    public float VelocityLoss = 0.3f;

    [Tooltip("How high the random number between 0 and 1 must be for the player to activate the geyser when they are activated")]
    [Range(0f, 1f)]
    public float LuckFactor = 0.6f;

    float TimeToActivation;
    bool isActive = false;
    bool canLaunch = false;

    /* [...] - Implementation of other functionality was cut from this example */
    /* [...] - Implementation of activation and deactivation cut from this example */

    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Kart")
        {
            if (isActive)
            {
                LaunchPlayer(other);
            }

            else if (canLaunch)
            {
                float luckyOrNot = Random.Range(0f, 1f);
                print(luckyOrNot);

                if (luckyOrNot >= LuckFactor)
                {
                    LaunchPlayer(other);
                }
            }
        }
    }

    void LaunchPlayer(Collider other)
    {
        var rigidBody = other.GetComponentInParent<Rigidbody>();
        var rbVelocity = rigidBody.velocity;
        var dir = rigidBody.velocity;
        rigidBody.velocity = dir * VelocityLoss;
        other.GetComponentInParent<Rigidbody>().AddForce(transform.up * GeyserForce, ForceMode.Impulse);
    }

    /* [...] - Implementation of other functionality was cut from this example */
}

 

Show code implementation of wind hazard controller in C#
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Collider))]
public class WindHazardController : MonoBehaviour {

    public enum WindDirection{ left, right}

    private class BodyInWind
    {
        public Rigidbody Body;
        public float Time;
    }

    [Header("Force Changed by Time")]
    public AnimationCurve ForceOverTime;
    public AnimationCurve TorqueOverTime;
    public AnimationCurve ForceIncreasion;

    /* [...] - Implementation of other functionality was cut from this example */
    /* [...] - Implementation of activation and deactivation cut from this example */

    /// <summary>
    /// Total time since activation of the wind.
    /// </summary>
    private float activeTime;

    /// <summary>
    /// Karts is added to affectObjects when entering and removed when exiting the trigger volume.
    /// </summary>
    private List<BodyInWind> affectObjects;

    void FixedUpdate()
    {
        if (active)
        {
            activeTime += Time.deltaTime;
            var windVector = GetWindDirection() * WindStrenght * ForceOverTime.Evaluate(activeTime);
            float windDir = CurrWindDir == WindDirection.right ? 1f : -1f;
            Vector3 WindTorque = Vector3.up * windDir * WindTorqueStrenght * TorqueOverTime.Evaluate(activeTime);
            foreach (var ob in affectObjects)
            {
                ob.Time += Time.deltaTime;
                if (ob.Body != null)
                {
                    var timeFactor = ForceIncreasion.Evaluate(ob.Time);
                    ob.Body.AddForce(windVector * timeFactor, ForceMode.Acceleration);
                    ob.Body.AddTorque(WindTorque * timeFactor, ForceMode.Acceleration);
                }
            }
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        var body = other.GetComponent<Rigidbody>();
        if (body == null)
        {
            body = other.GetComponentInParent<Rigidbody>();
        }

        if (body != null)
        {
            if (!affectObjects.Any(x => x.Body == body))
            {
                affectObjects.Add(new BodyInWind() { Body = body});
            }
        }
    }

    private void OnTriggerExit(Collider other)
    {
        var body = other.GetComponent<Rigidbody>();
        if (body == null)
        {
            body = other.GetComponentInParent<Rigidbody>();
        }

        if (body != null)
        {
            if (affectObjects.Any(x => x.Body == body))
            {
                affectObjects.Remove(affectObjects.First(x => x.Body == body));
            }
        }
    }

   /* [...] - Implementation of other functionality was cut from this example */
}

Camera

We wanted to have a dramatic camera that enhanced the racing feeling. We achieved this by focusing on the camera behavior when the kart turns or drifts.

The camera works by each frame calculates a target location and rotation. The camera then is moved towards those targeted values.

The target position is calculated by taking the opposite direction the kart is moving in. The target rotation is calculated by taking the same direction the kart is facing.

 

The camera creates tension and excitement when drifting through tight turns.

 

The kart is currently turning in the direction R and the player is sliding. The kart is moving in the direction of the vector V and is facing in the direction of F.

To calculate the target camera position the vector V is flipped 180° to create the vector V’. The target rotation F’ is the same as the forward vector of the kart, F.

Show implementation of the CameraController in C#
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VikingBoxCarRacing.Scripts.Uitls;

[RequireComponent(typeof(Camera))]
public class GameplayCamera : MonoBehaviour {

    [Header("Kart")]
    public GameObject KartToFollow;

    [Header("Camera Position")]
    public float CameraDistance = 2.5f;
    public float CameraHeight = 1.8f;

    [Header("Camera speed")]
    public float CameraSpeed = 28;
    public float CameraRotationSpeed = 8;

    [Tooltip("The offset from the kart where the camera will look at.")]
    public Vector3 LookAtOffset;
    [Tooltip("The threshhold to determain if the kart is moving.")]
    public float MovingThreshold = 0.2f;

    private HoverGravityController hover;
    private EngineController engine;


    void Start ()
    {
        if(KartToFollow == null)
        {
            throw new Exception("Camera does not have kart to follow.");
        }

        hover = KartToFollow.GetComponent<HoverGravityController>();
        engine = KartToFollow.GetComponent<EngineController>();
        
        if (hover == null || engine == null)
        {
            throw new Exception("Kart does not have either a HoverController or a Engine,");
        }
    }

    private void FixedUpdate()
    {
        //Lerp the camera position with speed depending on the Karts speed.
        var targetPos = GetCameraTargetPosition();
        transform.position = Vector3.Lerp(transform.position, targetPos, CameraSpeed * Time.deltaTime);

        //Lerp the rotation of the camera to look at the kart.
        var targetRotation = GetCameraRotation(targetPos);
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, CameraRotationSpeed * Time.deltaTime);
    }


    private Vector3 GetCameraTargetPosition()
    {
        Vector3 kartVelocity = engine.KartHorizontalVelocity;
        Vector3 kartDir = engine.KartDirection;
        //We strap away the vertical part of the forward vector to avoid vertical movement of the camera.
        kartDir.y = 0;
        kartDir.Normalize();

        var relativeVelocity = engine.KartRelativeHorizontalVelocity;

        Vector3 cameraDir = -kartDir;
        //Special case if the kart is moving backward, do not flipp the direction then.
        if (Mathf.Sign(relativeVelocity.z) == -1)
        {
            cameraDir = kartDir;
        }

        //calculate the position of the camera from the grounded point of the kart to avoid the camera bouncing.
        return hover.GroundPoint + cameraDir * CameraDistance + Vector3.up * CameraHeight;

    }

    public Quaternion GetCameraRotation(Vector3 cameraTargetPosition)
    {
        //Check if kart is standing still and that we are moving forward.
        if (engine.KartHorizontalVelocity.sqrMagnitude > 0.2 && Mathf.Sign(engine.KartRelativeHorizontalVelocity.z) == 1)
        {
            return Quaternion.LookRotation(KartToFollow.transform.forward, Vector3.up);
        }

        //Default case if kart is moving backwards or not moving at all.
        var lookAtPosition = hover.GroundPoint + KartToFollow.transform.TransformVector(LookAtOffset);
        return Quaternion.LookRotation(lookAtPosition - cameraTargetPosition, Vector3.up); ;
    }

}

Racing systems

 

Checkpoint system

We implemented a checkpoint system to track the player’s progression along the track. In order to complete a lap, the players needed to pass through a series of checkpoints in order. This forced players to go through the whole racetrack and prevented them from cheating by taking illegal shortcuts.

 

Placement system

We calculated the player’s placement in the race based on the checkpoint system. The player with the highest lap-number, with the highest checkpoint number and closest to the next checkpoint, was considered to be in lead over the other players.

Show code implementation of checkpoint controller in C#
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Collider))]
public class CheckPointController : RespawnController {

    [Header("Checkpoint")]
    public int CheckPointNumber;

    private Collider checkPointColider;

    private void OnTriggerEnter(Collider other)
    {
        var contender = other.gameObject.GetComponentInParent<RaceContenderController>();
        if(contender == null)
        {
            contender = other.gameObject.GetComponent<RaceContenderController>();
        }

        if(contender != null)
        {
            if(contender.NextCheckPoint == CheckPointNumber)
            {
                contender.SetNextCheckPoint(CheckPointNumber);
            }
        }
    }

    public float GetDistance(GameObject go)
    {
        return GetDistance(go.transform.position);
    }

    public float GetDistance(Vector3 pos)
    {
        if (checkPointColider == null)
        {
            checkPointColider = GetComponent<Collider>();
        }

        return Vector3.Distance(checkPointColider.ClosestPointOnBounds(pos), pos);
    }
}

Show code implementation of race contender controller in C#
public class RaceContenderController : MonoBehaviour {

    /* [...] - Implementation of other functionality was cut from this example */

    
    /* These private members have public getters cut from this code example */
    private int lapNumber;
    private int nextCheckPoint;
    private int lastCheckPoint = -1;
    private int finishedPlacement = -1;

    public void SetNextCheckPoint(int CheckPointNumber)
    {
        lastCheckPoint = nextCheckPoint;
        bool lapComplete;
        nextCheckPoint = LapCecker.Instance.GetNextCheckPointNumber(CheckPointNumber, out lapComplete);
        if (lapComplete)
        {
            lapNumber++;
            //Check if finished 
            if (lapNumber >= LapCecker.Instance.NumberOfLaps)
            {
                finishedPlacement = LapCecker.Instance.GetPlacement(this);
            }
        }
    }

    /* [...] - Implementation of other functionality was cut from this example */

    //Compare Race contenders, lower means higher placement in race.
    public int CompareTo(object obj)
    {
        if (obj == null) return -1;

        RaceContenderController otherContender = obj as RaceContenderController;
        if (otherContender == null)
        {
            throw new ArgumentException("Object is not a Race contenter");
        }

        //Compare race Finished placement.
        if (otherContender.FinishedPlacement != -1)
        {
            if(this.FinishedPlacement == -1)
            {
                return 1;
            } else if( otherContender.FinishedPlacement < FinishedPlacement)
            {
                return 1;
            } else if (otherContender.FinishedPlacement > FinishedPlacement)
            {
                return -1;
            }        
        }
        //Finished players is always before non-finished player.
        else if (FinishedPlacement != -1 && otherContender.FinishedPlacement == -1)
        {            
            return -1;
        }

        //Compare lap number
        if (otherContender.LapNumber > LapNumber)
        {
            return 1;
        }
        else if (otherContender.LapNumber < LapNumber)
        {
            return -1;
        }

        //Compare Checkpointnumber
        if (otherContender.NextCheckPoint > NextCheckPoint)
        {
            return 1;
        }
        else if (otherContender.NextCheckPoint < NextCheckPoint)
        {
            return -1;
        }

        var checkPoints = LapCecker.Instance.GetCheckPoints(NextCheckPoint);
        var myDistance = checkPoints.Select(x => x.GetDistance(gameObject)).Min();
        var otherDistance = checkPoints.Select(x => x.GetDistance(otherContender.gameObject)).Min();

        //Compare distance to next checkpoint
        if (otherDistance < myDistance)
        {
            return 1;
        }
        else if (otherDistance > myDistance)
        {
            return -1;
        }

        return 0;
    }
}