Programming the Main Menu

This post explains how the main menu of Veclicous racing works. The main menu scene is just a few props and a UI canvas, but I think the visual effect it quite powerful. Below is a video showing it functioning.

A screen shot of the main menu in the editor is shown below,

The only programming that is contained within the main menu scene is that programming which makes the main menu move and of course the logic to make the buttons function. I will first explain the movement of the main menu when a player ever the player clicks certain button.

The main menu is contained on a single canvas, so if this canvas is moved all of the buttons on the canvas will be subsequently  moved. This is essentially how the main menu of Velicous racing function, when the player presses a certain button, the canvas is moved directly up or down to reveal new buttons

The picture below attempts to illustrate the programming of the main menu.

mainmenu4

A screen shot of the in engine Editor also shows the setup.

mainmenu5

As usual I provide a flow char to help explain the later code.

New-Mind-Map (14)

Pasted Raw Code,

public class Main_Menu_Manager_Script : MonoBehaviour {

 public GameObject MenuHolder;
 public GameObject LoadingScreen;
 public Slider music;
 public Slider effects;
 private GameObject Target;

 void Start()
 {
  music.value = PlayerPrefs.GetFloat("MusicVolume");
  effects.value = PlayerPrefs.GetFloat("SoundEffectVolume");
 }

 void Update()
 {
  if (Target != null)
  {
   float distance = Vector3.Distance(MenuHolder.transform.position, Target.transform.position);
   MenuHolder.transform.position = Vector3.Lerp(MenuHolder.transform.position, Target.transform.position, 10 * Time.fixedDeltaTime);
   if (distance < 10)
   {
    Target = null;
   }
  }
 }

 public void ChangeSoundEffectVolume(float _value)
 {
  PlayerPrefs.SetFloat("SoundEffectVolume", _value);
 }

 public void ChangeSoundMusicVolume(float _value)
 {
  PlayerPrefs.SetFloat("MusicVolume", _value);
 }

 public void Quit()
 {
  Application.Quit();
 }

 public void MoveMenu(GameObject _target)
 {
  Target = _target;
 }

 public void ChangeLevel(int _level)
 {
  LoadingScreen.SetActive(true);
  SceneManager.LoadScene(_level);
 }

 void OnEnable()
 {
  LoadingScreen.SetActive(false);
 }

 public void ClearSave()
 {
  PlayerPrefs.DeleteAll();
 }
}

Programming Custom Car Effects

This post covers the custom effects that take place on the car such as particle effects, emission changes etc…

All custom effects are contained within one script placed on the player’s car object.

Fake Body Rotation

When the player turns the car, the cars body ‘rotates’ from side to side much like a aircraft banking around a corner. In Velicous racing this body roll is completely  cosmetic and only in place for visual feedback.

The body roll method uses the players input and a constant roll value to create a scalar value that ranges from zero to maximum roll. This value is then converted to a quaternion rotation to avoid gimbal lock problems, finally the quaternion value is apply to the transform by multiplying it with the cars current rotation quaternion.

Video showing the bank angle working, notice how the car rolls as it turns(the effect have been amplified for this video to make it more obvious).

Code Snippet,

//'Bank' the body
private void FakeRotation(float _horizontalInput)
{
 float _rotation = MaxBankAngle * _horizontalInput;
 Quaternion _QRotation = Quaternion.Euler(new Vector3(0, 0, -_rotation));
 VehicleGraphics.transform.rotation = transform.rotation * _QRotation;
}

Engine Exhaust

The engine exhaust method is responsible for controlling the direction of the exhaust particle system and controlling its rate of emission.

The method itself works much like the body roll method in that it takes in the players input and outputs quaternion rotation, which is eventually used to control the direction of the exhaust.

The method does however carry out some other checks, the first of which is to check if the player is grounded. This is because the player cannot use the engine when they are grounded, thus the emission rate should be set back to default whenever the car is not grounded no matter the players input.

The other check that this method carries out is to check whether the player is boosting or not, this is because a secondary particle system is fired whenever the player is boosting to give some extra visual feedback.

Code Snippet with comments to explain,

private void EngineExhaust(float _horizontalInput, float _verticalInput, bool _grounded, bool _boosting)
{
 //Calculate a rotation based on players input
 float _rotation = -MaxExhaustAngle * _horizontalInput;
 //Loop through all of the engine particle system, if there are multiple engines
 for (int i = 0; i < EngineParticleSystems.Length; i++)
 {
  //Check if the player is boosting
  if(_boosting == true)
  {
   //Activate Boost Particle Effect
   BoostParticleSystems[i].Play();
  } else
  { //De-activate Boost Particle Effect
   BoostParticleSystems[i].Stop();
  }
  //Create a quaternion
  Quaternion _QRotation = Quaternion.Euler(new Vector3(0, _rotation, 0));
  //Apply the rotation to all particles in the loop
  EngineParticleSystems[i].transform.rotation = transform.rotation * _QRotation;
  //Create a temporory copy so we can adjust the private value emission
  ParticleSystem temp = EngineParticleSystems[i];
  //Adjust the value
  var tempEm = temp.emission;
  //Check if we are grounded
  if (_grounded == true)
  {
   //We are grounded set a emission rate based on input
   tempEm.rateOverTime = storedEmission + (MaxExhaustEmission * _verticalInput);
  } else
  {
   //Not grounded default emission
   tempEm.rateOverTime = storedEmission;
  }
  //Apply the new emissions
  EngineParticleSystems[i] = temp;
 }
}

Brake Lights

The brake light method is responsible for changing the emission map whenever the player braks,  because some parts car model share emission maps simply increasing the value of emissions would cause other parts of the car to glow and give undesired results. For this reason instead I instead swap the entire material for certain parts of the car, this secondary material has the higher emission value which is required. The swap is contained within a loop just so that multiple break lights can be added.

private void BrakingLights(float _verticalInput)
{
 if(_verticalInput < 0)
 {
  foreach(GameObject light in Lights)
  {
   light.GetComponent<MeshRenderer>().material.SetTexture("_EmissionMap", brakingEmssionMap);
  }
 } else
 if (_verticalInput >= 0)
 {
  foreach (GameObject light in Lights)
  {
   light.GetComponent<MeshRenderer>().material.SetTexture("_EmissionMap", defaultEmssionMap);
  }
 }
}

Sparks on Collide Method

Finally the sparks on collide method is a simply method that is activated each time the players object collides with another object, a flow chart explains the logic.

New-Mind-Map (15)

And the code,

private void OnCollisionEnter(Collision collision)
{
 foreach(ContactPoint contact in collision)
 {
  SparksOnCollide(contact.point, contact.normal);
 }
}
private void SparksOnCollide(Vector3 _position, Vector3 _rotation)
{
 bottomOutSparks.transform.position = _position;
 bottomOutSparks.transform.rotation = Quaternion.Euler(_rotation);
 bottomOutSparks.time = 0;
 bottomOutSparks.Play();
}

Programming Checkpoints

Explained in the Time manager post was the use of checkpoints in the game, as players pass through checks points in time trail mode they are given an amount of extra time and given a reset point in case they crash and need to reset the car.

Before giving the player extra time however, it is vital that the check point ensures the player is travelling in the correct direction. If for example the player travels through the checkpoint backwards they should not be awarded the check point. This check is done through a dot product check.

Once the checkpoint has ensured that the player is traveling in the correct direction, it can access the player’s time manager script and award extra time. As well as accessing the time manager script, the checkpoint script also access the player’s  reset script (used to reset the car after a crash) to save the check point as a reset point.

New-Mind-Map (13)

The raw script is pasted below,

public class CheckPoint_Script : MonoBehaviour
{
 private Vector3 checkPointDirection;

 void Start()
 {
  checkPointDirection = transform.forward;
 }

 void OnTriggerEnter(Collider other)
 {
  //Check the obkects tag
  if(other.transform.tag == "Player")
  {
   //Look for the reset script
   ResetCar resetCar = other.GetComponent&amp;lt;ResetCar&amp;gt;();
   if (resetCar != null)
   {
    //Check the player has not passed through the checkpoint twice
    if (resetCar.LastPassedCheckPoint != this.gameObject)
    {
     //Pass this check point as the last past checkpoint
     resetCar.LastPassedCheckPoint = this.gameObject;
     //Look for a rigid body component
     Rigidbody rigid = other.GetComponent&amp;lt;Rigidbody&amp;gt;();
     if (rigid != null)
     {
      //Check the players velocity against the direction of this checkpoint
      //If the value is above 0, the player is traveling in the correct direction
      if (Vector3.Dot(rigid.velocity, checkPointDirection) &amp;gt; 0)
      {
       //Look for the players time manager script
       Player_TimeManager_Script timemanager = other.GetComponent&amp;lt;Player_TimeManager_Script&amp;gt;();
       if (timemanager != null)
       {
        //Activate the checkpointpassed method
        timemanager.CheckPointPassed();
       }
      }
     }
    }
   }
  }
 }
}

Programming the Time Manager

Velicous racing has a time trail mode(endurance) inside of it, for this game made to work a manger is needed that records the players lap information and time remaining. The lap information will be stored so it can be viewed later, while if the time remaining variable reaches zero the game is over.

The time manager script itself is completely self contained for safety and relatively simple. It contains a timer which is used to record both the players lap time and the time remaining, it also contains some other variables such as how far the player has driven, the number of laps they have done and a check to see if they have passed a checkpoint.

The script is illustrated below,

New-Mind-Map (12)

As shown the script is essentially a timer, all other variables contained within it are arbitrary variables that are used to control whether the game is over or record the players performance.

The timer script also contains two methods, one is activated every time the player passes a checkpoint, this ‘checkpoint’ method adds some time to the players total remaining time and checks to see if the player has passed all of the checkpoints, if he has it is presumed he has completed a lap. Finally the checkpoint method talks to the player’s UI to give some visual feedback when they pass through a checkpoint.

The other method contained on this script is activated when all of the checkpoints in the level have been passed and thus a lap completed. This ‘lap’ completed method checks the players lap time to see if it is a personal best and updates the player preferences accordingly. It also resets the lap timer and adds to the lap count.

The entire script is pasted below for reference.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Player_UI))]
[RequireComponent(typeof(Player_Motor))]
public class Player_TimeManager_Script : MonoBehaviour {

 public static bool GameOver = false;
 private float distanceDriven = 0;
 public float DistanceDriven { get { return distanceDriven; } }
 private float bestDistance = 0;
 public float BestDistanceDriven { get { return bestDistance; } }

 private float timeLeft = 80;
 public float TimeLeft { get { return timeLeft; } }

 private int lapCount = 1;
 public float LapCount { get { return lapCount; } }

 private float lapTimeSeconds = 0;
 private string formattedLapTime = "00 : 00";
 public string FormattedLapTime { get { return formattedLapTime; } }

 private float bestLapSeconds = 0;
 private string formattedBestLapTime = "00 : 00";
 public string FormattedBestLapTime { get { return formattedBestLapTime; } }

 private int numberOfCheckPointsPassed = 0;
 private int numberOfCheckPointsInLevel;

 private Player_UI playerUI;
 private Player_Motor playerMotor;

 void Start()
 {
  GameOver = false;
  playerUI = GetComponent<Player_UI>();
  playerMotor = GetComponent<Player_Motor>();
  numberOfCheckPointsInLevel =
  GameObject.FindGameObjectsWithTag("Checkpoint").Length;

  bestDistance = PlayerPrefs.GetFloat("BestDistanceDriven");
  bestLapSeconds = PlayerPrefs.GetFloat("BestLapSeconds");
 }

 void Update()
 {
  if(numberOfCheckPointsPassed != 0)
  {
    timeLeft -= Time.fixedDeltaTime;
    lapTimeSeconds += Time.fixedDeltaTime;
    FormatLapTime(lapTimeSeconds);
    if (GameOver == false)
    {
      int speed = Mathf.RoundToInt((playerMotor.ForwardSpeed() / 2));
      distanceDriven += speed * Time.fixedDeltaTime;
    }

    if(distanceDriven > bestDistance)
    {
      bestDistance = distanceDriven;
    }
  }

  if(timeLeft <= 0)
  {
    GameOver = true;
    if (bestLapSeconds != 0 && bestLapSeconds < PlayerPrefs.GetFloat("BestLapSeconds"))     {       PlayerPrefs.SetFloat("BestLapSeconds", bestLapSeconds);     }     if(bestDistance != 0 && bestDistance >
    PlayerPrefs.GetFloat("BestDistanceDriven"))
    {
      PlayerPrefs.SetFloat("BestDistanceDriven", bestDistance);
    }
  }
}

private void FormatLapTime(float _seconds)
{
  string minutes = Mathf.Floor(_seconds / 60).ToString("00");
  string seconds = Mathf.Floor(_seconds % 60).ToString("00");
  formattedLapTime = minutes + " : " + seconds;
}

public void CheckPointPassed()
{
  timeLeft += 10;
  numberOfCheckPointsPassed += 1;
  if(numberOfCheckPointsPassed > numberOfCheckPointsInLevel)
  {
    numberOfCheckPointsPassed = 1;
    LapComplete();
  }
  playerUI.StartCoroutine("FlashCheckPoint");
}

private void LapComplete()
{
  if(lapTimeSeconds < bestLapSeconds || bestLapSeconds == 0)
  {
    bestLapSeconds = lapTimeSeconds;
    string minutes = Mathf.Floor(lapTimeSeconds / 60).ToString("00");
    string seconds = Mathf.Floor(lapTimeSeconds % 60).ToString("00");
    formattedBestLapTime = minutes + " : " + seconds;
  }
  lapTimeSeconds = 0;
  lapCount += 1;
}
}

Programming the Player UI

This Post covers how the UI within Velicous Racing is programmed.

Because this is a single player game, the UI is contained on a UI just one static canvas. This means that I can create a script with a number of variables and then just drag the correct UI pieces to their corresponding variables in the editor. The script will then have all the references it needs to update the UI at run time, it just needs to access the other scripts on the player to get all of the information it needs. The flow chart below attempts to explain this better.

New-Mind-Map (17)

The figure shows the the UI script is essentially two methods which are called every frame, these methods access other scripts on the player to update the UI accordingly, the script is pasted below.

[RequireComponent(typeof(Player_Motor))]
[RequireComponent(typeof(Player_TimeManager_Script))]
public class Player_UI : MonoBehaviour {

 public Text CheckPointFlash;
 public Text TimeLeft;
 public Text LapCount;
 public Text LapTime;
 public Text DrivenDistance;
 public Text BestLapTime;
 public Text BestDistace;
 public Text GameOverDistance;
 public GameObject CrashPanel;
 public GameObject ResetPanel;
 public GameObject GameOverPanel;

 public Text SpeedText;
 public Text EngineOutput;
 public Image SpeedoMeterNeedle;
 public Image SpeedoMeterOuter;
 public Image RedLineFlash;
 public Image Boost_Meter;
 public float MaxNeedleAngle;

 private Player_Motor playerMotor;
 private Player_TimeManager_Script timeManager;
 private float playerMaxSpeed;
 private float playerMaxEngineRpm;
 private float playerFuelAmount; 

 private void Start()
 {
  playerMotor = GetComponent<Player_Motor>();
  timeManager = GetComponent<Player_TimeManager_Script>();
  playerMaxSpeed = playerMotor.TopSpeed;
  playerMaxEngineRpm = playerMotor.MaxEngineRpm;
 }

 private void Update()
 {
  UpdateSpeedo();
  UpdateInfo();
  if (playerMotor.Crashed == true)
  {
   CrashPanel.SetActive(true);
  } else
  {
   CrashPanel.SetActive(false);
  }

  if(Player_TimeManager_Script.GameOver == true)
  {
   GameOverDistance.text =
   Mathf.RoundToInt(timeManager.DistanceDriven).ToString();
   Image image = ResetPanel.GetComponent<Image>();
   image.CrossFadeAlpha(1, 0.25f, false);
   GameOverPanel.SetActive(true);
  }
 }

 public IEnumerator FlashCheckPoint()
 {
  CheckPointFlash.gameObject.SetActive(true);
  yield return new WaitForSeconds(1);
  CheckPointFlash.gameObject.SetActive(false);
 }

 private void UpdateInfo()
 {
  TimeLeft.text = Mathf.RoundToInt(timeManager.TimeLeft).ToString();
  LapCount.text = timeManager.LapCount.ToString();
  LapTime.text = timeManager.FormattedLapTime;
  DrivenDistance.text = Mathf.RoundToInt(timeManager.DistanceDriven).ToString() + " m";
  BestLapTime.text = timeManager.FormattedBestLapTime;
  BestDistace.text = Mathf.RoundToInt(timeManager.BestDistanceDriven).ToString();
 }

 private void UpdateSpeedo()
 {
  //Speedometer
  float playerCurrentSpeed = playerMotor.ForwardSpeed();
  float speedRange = Mathf.InverseLerp(0, playerMaxSpeed, playerCurrentSpeed);
  SpeedText.text = Mathf.RoundToInt((playerCurrentSpeed * 2.2369f) / 2).ToString();
  SpeedoMeterOuter.fillAmount = speedRange;

  //RPM Counter
  float playerCurrentEngineRpm = playerMotor.EngineRpm();
  if (playerCurrentEngineRpm > (playerMaxEngineRpm - 50))
  {
   RedLineFlash.gameObject.SetActive(true);
  }
  else
  {
   RedLineFlash.gameObject.SetActive(false);
  }
  float ang = Mathf.Lerp(0, MaxNeedleAngle, Mathf.InverseLerp(0, playerMaxEngineRpm, playerCurrentEngineRpm));
  SpeedoMeterNeedle.transform.rotation = Quaternion.Euler(0, 0, -ang);

  //Boost meter
  playerFuelAmount = playerMotor.boostFuelAmount;
  float boostRangne = Mathf.InverseLerp(100, 0, playerFuelAmount);
  Boost_Meter.fillAmount = 1 - boostRangne;

  //Engine Output (Force Meter)
  EngineOutput.text = Mathf.RoundToInt(playerMotor.currentEngineForce * 0.001f).ToString();
 }

 public void ChangeLevel(int _level)
 {
  SceneManager.LoadScene(_level);
 }
}

Vehicle Crash Programming

This post explains how the crash system in Velicous Racing is programmed.

As explained in early post, many of the physics within the game are controlled tightly or disabled all together to give more predictable results. However this presents a problem when the player crashes the vehicle, with such tightly controlled physics the crashes are often predicable, un-random and not realistic all together. As shown in the video below,

To combat this a custom a set of methods have been created to emulate more realistic crashes. The methods essentially detect when a collision has taken place, if a collision is detected then many of the players controls are disabled while physics are enabled on the car. Flow chart below,

New-Mind-Map (10)

With the logic created, I program the methods.

//Collision and Crash Check
private void OnCollisionEnter(Collision collision)
{
 //Play a sound when we hit something
 soundController.PlayHitSound();
 //Find the velocity difference, between us and the object we crashed
 float velocityDiff = collision.relativeVelocity.magnitude;
 //Covert our up vector to w rodl transform
 Vector3 up = transform.TransformDirection(transform.up);
 //Find direction to crash
 Vector3 toOther = collision.contacts[0].point - crashReferencePoint.transform.position;
 //Check direction and force of crash
 if(Vector3.Dot(up, toOther) &gt; 0.1f &amp;&amp; velocityDiff &gt; 100)
 {
  StartCoroutine("HasCrashed");
 }
}
//Void Update Snippet - Check if car has recovered
if (crashed)
{
 boosting = false;
 if (Physics.Raycast(transform.position, -transform.up, out hit, suspensionRange))
 {
  grounded = true;
 } else
{
 grounded = false;
}

if (transform.hasChanged)
{
 Vector3 rotationDiff = transform.rotation.eulerAngles - savedTransformRotation;
 curDir += rotationDiff.y;
 savedTransformRotation = transform.rotation.eulerAngles;
 transform.hasChanged = false;
}

if (rigid.velocity.magnitude == 0 &amp;&amp; grounded == true)
{
 crashed = false;
}
}

The video below shows the results,

 

 

Programming SpeedControl And Refill Pads

In Velicous Racing there  are two types of ‘pads’ found on the road, these are the speed control pad and Refill Pad. As the name suggest the speed control pad controls the players forward velocity while the refill pad fills the players boost fuel.

Speed Control Pad

In some areas of the enviroment the car has to jump, across the enviroment. Because the player can control what speed they are driving at when they take these jumps it would be nice if they where traveling at the correct speed to land in the correct location. This is what speed control pads do, when the player passes over the pad there velocity is changed to the stored target speed.

The speed control pad itself it simply a trigger collider and a controlling script, as shown below.

slowdown

When the player passes through the trigger collider the players rigid body is sampled so that its velocity can be changed. When the rigid bodies velocity is sampled it is returned in world co-ordinates, for this reason is converted to a local vector, the local vectors z value (forward) is then changed to the respective stored speed so that the player lands in the correct position.

The video below shows the same pad both slowing the player and speeding him up, so hat he lands in roughly the same position every time.

The controlling script is pasted below,

public class SlowDownPlate_Script : MonoBehaviour {

 [SerializeField]
 private float targetSpeed = 150.0f;
 [SerializeField]
 private float smoothvalue = 5f;
 private AudioSource myAudio;

 void Start()
 {
  myAudio = GetComponent<AudioSource>();
 }

 private void OnTriggerStay(Collider other)
 {
  //Check the collider is a player
  if(other.tag == "Player")
  {
    //Play A Sound
    myAudio.Play();
    //Grab the player rigid body reference
    Rigidbody otherBody = other.GetComponent<Rigidbody>();
    if (otherBody != null)
    {
      //Grab players world velocity
      Vector3 velocity = otherBody.velocity;
      //Localise it
      Vector3 localVel = other.transform.InverseTransformDirection(velocity);
      //Lerp the players velocity to the target speed
      localVel.z = Mathf.Lerp(localVel.z, targetSpeed, smoothvalue *
      Time.fixedDeltaTime);
      //Reapply the veclotity as a world velocity
      Vector3 worldVel = otherBody.transform.TransformDirection(localVel);
      otherBody.velocity = worldVel;
    }
   }
 }
}

Boost Refill Pad

The player is able to boost while racing to increase the power of their engine, however this uses ‘boost fuel’ when this is empty the player cannot boost anymore. To refill the player’s boost fuel they must drive over a boost refill pad.

The boost pad works in much the same way as the speed control pad, it is simply a trigger collider and a controlling script.

refilpad

Once again as the player passes through the trigger collider the controlling script is activated. This script accesses the players motor script and adds an amount of fuel for every tick the player is inside of the trigger collider, until the players fuel is completely full.

The script is pasted below,

public class BoostRefill_Script : MonoBehaviour {

 private AudioSource myAudio;
void Start()
 {
  myAudio = GetComponent<AudioSource>();
 }

 void OnTriggerStay(Collider other)
 { 
  if(other.gameObject.tag == "Player")
  {
    if (other.GetComponent<Player_Motor>().boostFuelAmount < 100)
    {
      if(myAudio.isPlaying == false)
      {
        myAudio.Play();
      }
      other.GetComponent<Player_Motor>().boostFuelAmount += 1;
      }
     }
   }
}

Programming Other Vehicle Physics

This post covers miscellaneous methods that are running in the background that complete the vehicle controller, these methods simulate drag among other physics emulations.

Update Forward Velocity Based On Direction

Because the cars in Velicous Racing hover, there is no contact with the ground and this means that as the player turns the car it will simply continue in its original direction. Much like an object floating in space.

To counter this problem a method was created, this method cancels any ‘sideways’ velocity over time. Ultimately this means that when the player turns the car, sideways velocity will be detected and canceled out over time. As usual I provide a flow graph to illustrate,

New-Mind-Map (9)

With the logic set out, I created the final codes.

private float GripBasedOnSpeed(float _speed)
{
 float range = Mathf.InverseLerp(0, topSpeed, _speed);
 float grip = maxGrip * range;
 float returnMaxedGrip = Mathf.Max(minGrip, grip);
 return returnMaxedGrip;
}
private void KeepVelocityAlignedWithVehicle()
{
 Vector3 localV = transform.InverseTransformDirection(rigid.velocity);
 localV.x = Mathf.Lerp(localV.x, 0, currentGrip * Time.fixedDeltaTime);
 Vector3 worldV = transform.TransformDirection(localV);
 rigid.velocity = worldV;
}

Drag

In Unity’s physics engine, by default air drag is a static value. However in real life as an object accelerates to a higher speed its drag increases proportionally. So we must emulate this in Unity. To keep things simply as we are only trying to emulate drag and not simulate it, we can use a formula based on the objects current speed and terminal velocity.

Drag = Current Velocity / Terminal Velocity

The above formula produces a normalized value that ranges from 0 – 1, this can be used to set Unity’s drag value at run time. Which produces much more realistic results compared to a static drag value.

private void UpdateDrag()
{
 rigid.drag = rigid.velocity.magnitude / topSpeed;
}

Vehicle Hover and Steer

This post explains the custom vehicle controller I created for this game. Because this game has hovering cars that move at high speed the default vehicle controller was in no way suitable.

Hover Ability

After some research I could see two feasible methods for getting the desired results in Unity. Both of these use methods use ray cast fired down from the car towards the ground to test for distance and adjust the cars position accordingly.

There are some differences in the approaches I found however, the first method uses four ray cast and Unity’s inbuilt physics engine to apply reactive forces. Thus keeping the car floating above the ground, this is illustrated below.

raycastphysics

This method produces very realistic looking results, however using the physics engine makes it very hard to control the cars hover and makes it very unpredictable. Another undesirable result of this method is that if the player crashes the car at high speed the physics engine struggles to produce realistic looking results.

The second method I found during my research uses just one ray cast and directly sets the position of the car using its transform instead of Unity’s physics engine. Again this method is illustrated below.

raycastphysics2

This method again has its advantages and dis-advantages. The ‘hover’ motion of this solution is much more stable and controllable than previously described solution, but it is less reactive to bumps and therefore bottoms out more often on un-even terrain or hills. This is because the ground is only sampled in one position and if the car rotates the front or back of the car can be lower than the sampled position,

Because both of the solutions have there pro’s and con’s my final solution is a hybrid of the two and again is illustrated below.

raycastphysics3

The later ‘hybrid’ method uses four ray casts, to produce a relative ‘force’ which applied directly through the physics engine, but as an acceleration. Accelerations in Unity ignore all physics and simply moves the transform. This gives the best of both worlds, because it is very controllable while at the same time it has four sample points which helps negates the problems with bumps and uneven terrain.

Finally the cars rigid body rotations are frozon so that physics do not effect them

The flow graph below explains how the logic flows for the cars hover suspension .

New-Mind-Map (5)

Finally, I moved onto create the final code.

//Hover Physics
private void HoverSuspension()
{
 for (int i = 0; i &amp;amp;amp;lt; RayCastPoints.Length; i++)
 {
  if (Physics.Raycast(RayCastPoints[i].position, -transform.up, out hit, suspensionRange))
  {
   if (hit.collider.tag == "Driveable" || hit.collider.tag == "Road")
   {
    distances[i] = hit.distance;
    curNormals[i] = hit.normal;
    vectors[i] = hit.point;
    groundedArray[i] = true;
  }
 }
 else
 {
 distances[i] = 999f; // Debug Value to bypass later code
 groundedArray[i] = false;
 }
 }
 //If any ray has hit, then we are grounded.
 if(groundedArray[0] == true || groundedArray[1] == true || groundedArray[2] == true || groundedArray[3] == true)
 {
 grounded = true;
 } else
 //No rays hit, we are flying!
 {
 grounded = false;
 return;
 }

 float minDistance = Mathf.Min(distances[0], distances[1], distances[2], distances[3]);
 int index = System.Array.IndexOf(distances, minDistance);
 Vector3 minPoint = vectors[index];

 //Find velocity at hit point
 Vector3 _velocityAtTouch = rigid.GetPointVelocity(minPoint);
 //Create compression varible
 float _compression = minDistance / hoverHeight;
 _compression = -_compression + 1;
 //Create counterForce vector
 Vector3 _counterForce = (transform.up * _compression) * suspensionForce;
 //Create a stabalising drag force
 Vector3 t = transform.InverseTransformDirection(_velocityAtTouch);
 //Remove X and Z
 t.z = t.x = 0;
 //Create a Y drag force
 Vector3 _shockDrag = rigid.transform.TransformDirection(t) * -suspensionDamp;
 //Apply The Force, Use the physics engine, but apply it as an acceleration
 rigid.AddForce(_counterForce + _shockDrag, ForceMode.Acceleration);
 }

Terrain and Steering Rotation

Now the car can hover, I move onto programming its ability to align its self with terrain below. Because my hover solution adds a single force to the cars transform it can not deal with rotations, for this reason I need a second method which can.

After some research I once again found a solution, available to view here.

Unity Hover Car Terrain Align Solution

The solution is posted by ‘aldonaletto‘ uses only one ray trace to sample the normal angle of the terrain below relative to the car, this value is then used to adjust the cars rotation accordingly.

Because my car has four race traces aldonaletto’s solution can be adapted and made more accurate for my needs. Once again a flow chart shows the logic,

New-Mind-Map (6)

The flow chart above describes how the car will align itself with the terrain below, but does not allow the player to steer the car. How this is done is shown below,

New-Mind-Map (7)

Final code for the vehicles ability to align its self with the terrain and steer is below,

//Steerting and floor aignment
private void TerrainAlignSteer(float _horizontalInput)
{
//Average the normals
Vector3 averageCurNormal = (curNormals[0] + curNormals[1] + curNormals[2] + curNormals[3]) / 4;
//Check how fast we are going relative to top speed
float speedRange = Mathf.InverseLerp(0, topSpeed, currentSpeed);
//Use speedrange value to adjust the cars turn rate
float adjustedTurnFoce = Mathf.Max(minTurnForce, maxTurnForce * 1 - speedRange);
float turn = (adjustedTurnFoce * _horizontalInput * Time.fixedDeltaTime);
curDir = (curDir + turn) % 360; // rotate angle modulo 360 according to input
//Smooth the terrain normal
curNormal = Vector3.Lerp(curNormal, averageCurNormal, 5 * Time.fixedDeltaTime);
//Find the difference between up and required normal
Quaternion grndTilt = Quaternion.FromToRotation(Vector3.up, curNormal);
//Multiply the input with terrain align and apply
Quaternion newRot = grndTilt * Quaternion.Euler(0, curDir, 0);
transform.rotation = newRot;
}

Create a free website or blog at WordPress.com.

Up ↑