Street Smart Car Simulator

Last Updated On – 7/DEC/2017
Development Duration – September 2017 to December 2017
Team – Digital Media Team at Tagbin Services Pvt. Ltd.
My Role – Team Lead and Game Developer
Current Status – Implemented at World Road Meet, Expo-Center, India, 2017
Project Short Brief – Street Smart Technologies needed Tagbin to create a 3D Simulation game that was meant to be showcased to Road Transportation Authority of India as an alternative to manual driver testing due to limited spaces in R.T.O. Initially it was meant to be played via normal keyboard but upon further iteration during prototyping it was decided that we shall create a custom car controller mechanics using mechanical parts, arduino and leonardo electronics. After a successful showcase, it was presented at World Road Meet in Expo-Center, Greater Noida, India.
Technologies Used:
1. Unity 3D Game Engine
2. Adobe Photoshop
3. Visual Studio for C-Sharp Coding.
4. Arduino-IDE
5. C-Programming for Ardunio Sketch programming.
6. Local Client-Server network communication scripting in native c-sharp.

Game Walkthrough with custom Car Physics and Adaptable Environment

Scripts

1. Logitech Steering Wheel Controller

We used the Logitech’s Steering Wheel unit and combined it with our custom Arduino and Leonardo units.

using UnityEngine;
using System.Collections;
using System.Text;

public class LogitechSteeringWheel : MonoBehaviour {

    LogitechGSDK.LogiControllerPropertiesData properties;
    private string actualState;
    private string activeForces;
    private string propertiesEdit;
    private string buttonStatus;
    private string forcesLabel;
    string[] activeForceAndEffect;

	// Use this for initialization
	void Start () {
        activeForces = "";
        propertiesEdit = "";
        actualState = "";
        buttonStatus = "";
        forcesLabel = "Press the following keys to activate forces and effects on the steering wheel / gaming controller \n";
        forcesLabel += "Spring force : S\n";
        forcesLabel += "Constant force : C\n";
        forcesLabel += "Damper force : D\n";
        forcesLabel += "Side collision : Left or Right Arrow\n";
        forcesLabel += "Front collision : Up arrow\n";
        forcesLabel += "Dirt road effect : I\n";
        forcesLabel += "Bumpy road effect : B\n";
        forcesLabel += "Slippery road effect : L\n";
        forcesLabel += "Surface effect : U\n";
        forcesLabel += "Car Airborne effect : A\n";
        forcesLabel += "Soft Stop Force : O\n";
        forcesLabel += "Set example controller properties : PageUp\n";
        forcesLabel += "Play Leds : P\n";
        activeForceAndEffect = new string[9];
		Debug.Log(LogitechGSDK.LogiSteeringInitialize(true));
        

	}

    void OnGUI()
    {
        activeForces = GUI.TextArea(new Rect(10, 10, 180, 200), activeForces, 400);
        propertiesEdit = GUI.TextArea(new Rect(200, 10, 200, 200), propertiesEdit, 400);
        actualState = GUI.TextArea(new Rect(410, 10, 300, 200), actualState, 1000);
        buttonStatus = GUI.TextArea(new Rect(720, 10, 300, 200), buttonStatus, 1000);
        GUI.Label(new Rect(10, 400, 800, 400), forcesLabel);
    }

	// Update is called once per frame
	void Update () {
		//All the test functions are called on the first device plugged in(index = 0)
		if(LogitechGSDK.LogiUpdate() && LogitechGSDK.LogiIsConnected(0)){

            //CONTROLLER PROPERTIES
            StringBuilder deviceName = new StringBuilder(256);
            LogitechGSDK.LogiGetFriendlyProductName(0, deviceName, 256);
			propertiesEdit = "Current Controller : "+ deviceName + "\n";
            propertiesEdit += "Current controller properties : \n\n";
            LogitechGSDK.LogiControllerPropertiesData actualProperties = new LogitechGSDK.LogiControllerPropertiesData();
            LogitechGSDK.LogiGetCurrentControllerProperties(0, ref actualProperties);
            propertiesEdit += "forceEnable = " + actualProperties.forceEnable + "\n";
            propertiesEdit += "overallGain = " + actualProperties.overallGain + "\n";
            propertiesEdit += "springGain = " + actualProperties.springGain + "\n";
            propertiesEdit += "damperGain = " + actualProperties.damperGain + "\n";
            propertiesEdit += "defaultSpringEnabled = " + actualProperties.defaultSpringEnabled + "\n";
            propertiesEdit += "combinePedals = " + actualProperties.combinePedals + "\n";
            propertiesEdit += "wheelRange = " + actualProperties.wheelRange + "\n";
            propertiesEdit += "gameSettingsEnabled = " + actualProperties.gameSettingsEnabled + "\n";
            propertiesEdit += "allowGameSettings = " + actualProperties.allowGameSettings + "\n";
                
            //CONTROLLER STATE
            actualState = "Steering wheel current state : \n\n";
            LogitechGSDK.DIJOYSTATE2ENGINES rec;
            rec = LogitechGSDK.LogiGetStateUnity(0);
            actualState += "x-axis position :" + rec.lX + "\n";
            actualState += "y-axis position :" + rec.lY + "\n";
            actualState += "z-axis position :" + rec.lZ + "\n";
            actualState += "x-axis rotation :" + rec.lRx + "\n";
            actualState += "y-axis rotation :" + rec.lRy + "\n";
            actualState += "z-axis rotation :" + rec.lRz + "\n";
            actualState += "extra axes positions 1 :" + rec.rglSlider[0] + "\n";
            actualState += "extra axes positions 2 :" + rec.rglSlider[1] + "\n";
            switch (rec.rgdwPOV[0])
            {
                case (0): actualState += "POV : UP\n"; break;
                case (4500): actualState += "POV : UP-RIGHT\n"; break;
                case (9000): actualState += "POV : RIGHT\n"; break;
                case (13500): actualState += "POV : DOWN-RIGHT\n"; break;
                case (18000): actualState += "POV : DOWN\n"; break;
                case (22500): actualState += "POV : DOWN-LEFT\n"; break;
                case (27000): actualState += "POV : LEFT\n"; break;
                case (31500): actualState += "POV : UP-LEFT\n"; break;
                default: actualState += "POV : CENTER\n"; break;
            }

            //Button status :

            buttonStatus = "Button pressed : \n\n";
            for (int i = 0; i < 128; i++)
            {
                if (rec.rgbButtons[i] == 128)
                {
                    buttonStatus += "Button " + i + " pressed\n";
                }

            }
            
            /* THIS AXIS ARE NEVER REPORTED BY LOGITECH CONTROLLERS 
             * 
             * actualState += "x-axis velocity :" + rec.lVX + "\n";
             * actualState += "y-axis velocity :" + rec.lVY + "\n";
             * actualState += "z-axis velocity :" + rec.lVZ + "\n";
             * actualState += "x-axis angular velocity :" + rec.lVRx + "\n";
             * actualState += "y-axis angular velocity :" + rec.lVRy + "\n";
             * actualState += "z-axis angular velocity :" + rec.lVRz + "\n";
             * actualState += "extra axes velocities 1 :" + rec.rglVSlider[0] + "\n";
             * actualState += "extra axes velocities 2 :" + rec.rglVSlider[1] + "\n";
             * actualState += "x-axis acceleration :" + rec.lAX + "\n";
             * actualState += "y-axis acceleration :" + rec.lAY + "\n";
             * actualState += "z-axis acceleration :" + rec.lAZ + "\n";
             * actualState += "x-axis angular acceleration :" + rec.lARx + "\n";
             * actualState += "y-axis angular acceleration :" + rec.lARy + "\n";
             * actualState += "z-axis angular acceleration :" + rec.lARz + "\n";
             * actualState += "extra axes accelerations 1 :" + rec.rglASlider[0] + "\n";
             * actualState += "extra axes accelerations 2 :" + rec.rglASlider[1] + "\n";
             * actualState += "x-axis force :" + rec.lFX + "\n";
             * actualState += "y-axis force :" + rec.lFY + "\n";
             * actualState += "z-axis force :" + rec.lFZ + "\n";
             * actualState += "x-axis torque :" + rec.lFRx + "\n";
             * actualState += "y-axis torque :" + rec.lFRy + "\n";
             * actualState += "z-axis torque :" + rec.lFRz + "\n";
             * actualState += "extra axes forces 1 :" + rec.rglFSlider[0] + "\n";
             * actualState += "extra axes forces 2 :" + rec.rglFSlider[1] + "\n";
             */

            int shifterTipe = LogitechGSDK.LogiGetShifterMode(0);
            string shifterString = "";
            if (shifterTipe == 1) shifterString = "Gated";
            else if (shifterTipe == 0) shifterString = "Sequential";
            else  shifterString = "Unknown";
            actualState += "\nSHIFTER MODE:" + shifterString;

  


            // FORCES AND EFFECTS 
            activeForces = "Active forces and effects :\n";

            //Spring Force -> S
            if (Input.GetKeyUp(KeyCode.S)){
               if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_SPRING))
               {
                   LogitechGSDK.LogiStopSpringForce(0);
                   activeForceAndEffect[0] = "";
               }
               else
               {
                   LogitechGSDK.LogiPlaySpringForce(0, 50, 50, 50);
                   activeForceAndEffect[0] = "Spring Force\n ";
               }
            }

            //Constant Force -> C
            if (Input.GetKeyUp(KeyCode.C))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_CONSTANT))
                {
                    LogitechGSDK.LogiStopConstantForce(0);
                    activeForceAndEffect[1] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlayConstantForce(0, 50);
                    activeForceAndEffect[1] = "Constant Force\n ";
                }
            }

            //Damper Force -> D
            if (Input.GetKeyUp(KeyCode.D))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_DAMPER))
                {
                    LogitechGSDK.LogiStopDamperForce(0);
                    activeForceAndEffect[2] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlayDamperForce(0, 50);
                    activeForceAndEffect[2] = "Damper Force\n ";
                }
            }

            //Side Collision Force -> left or right arrow
            if (Input.GetKeyUp(KeyCode.LeftArrow) || Input.GetKey(KeyCode.RightArrow))
            {
                LogitechGSDK.LogiPlaySideCollisionForce(0, 60);
            }

            //Front Collision Force -> up arrow
            if (Input.GetKeyUp(KeyCode.UpArrow))
            {
                LogitechGSDK.LogiPlayFrontalCollisionForce(0, 60);
            }

            //Dirt Road Effect-> I
            if (Input.GetKeyUp(KeyCode.I))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_DIRT_ROAD))
                {
                    LogitechGSDK.LogiStopDirtRoadEffect(0);
                    activeForceAndEffect[3] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlayDirtRoadEffect(0, 50);
                    activeForceAndEffect[3] = "Dirt Road Effect\n ";
                }

            }
            
            //Bumpy Road Effect-> B
            if (Input.GetKeyUp(KeyCode.B))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_BUMPY_ROAD))
                {
                    LogitechGSDK.LogiStopBumpyRoadEffect(0);
                    activeForceAndEffect[4] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlayBumpyRoadEffect(0, 50);
                    activeForceAndEffect[4] = "Bumpy Road Effect\n";
                }

            }

            //Slippery Road Effect-> L
            if (Input.GetKeyUp(KeyCode.L))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_SLIPPERY_ROAD))
                {
                    LogitechGSDK.LogiStopSlipperyRoadEffect(0);
                    activeForceAndEffect[5] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlaySlipperyRoadEffect(0, 50);
                    activeForceAndEffect[5] = "Slippery Road Effect\n ";
                }
            }

            //Surface Effect-> U
            if (Input.GetKeyUp(KeyCode.U))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_SURFACE_EFFECT))
                {
                    LogitechGSDK.LogiStopSurfaceEffect(0);
                    activeForceAndEffect[6] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlaySurfaceEffect(0, LogitechGSDK.LOGI_PERIODICTYPE_SQUARE, 50, 1000);
                    activeForceAndEffect[6] = "Surface Effect\n";
                }
            }

            //Car Airborne -> A
            if (Input.GetKeyUp(KeyCode.A))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_CAR_AIRBORNE))
                {
                    LogitechGSDK.LogiStopCarAirborne(0);
                    activeForceAndEffect[7] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlayCarAirborne(0);
                    activeForceAndEffect[7] = "Car Airborne\n ";
                }
            }

            //Soft Stop Force -> O
            if (Input.GetKeyUp(KeyCode.O))
            {
                if (LogitechGSDK.LogiIsPlaying(0, LogitechGSDK.LOGI_FORCE_SOFTSTOP))
                {
                    LogitechGSDK.LogiStopSoftstopForce(0);
                    activeForceAndEffect[8] = "";
                }
                else
                {
                    LogitechGSDK.LogiPlaySoftstopForce(0, 20);
                    activeForceAndEffect[8] = "Soft Stop Force\n";
                }
            }

            //Set preferred controller properties -> PageUp
            if (Input.GetKeyUp(KeyCode.PageUp))
            {
                //Setting example values
                properties.wheelRange = 90;
                properties.forceEnable = true;
                properties.overallGain = 80;
                properties.springGain = 80;
                properties.damperGain = 80;
                properties.allowGameSettings = true;
                properties.combinePedals = false;
                properties.defaultSpringEnabled = true;
                properties.defaultSpringGain = 80;
                LogitechGSDK.LogiSetPreferredControllerProperties(properties);

            }

            //Play leds -> P
            if (Input.GetKeyUp(KeyCode.P))
            {
                LogitechGSDK.LogiPlayLeds(0, 20, 20, 20);
            }

            for (int i = 0; i < 9; i++)
            {
                activeForces += activeForceAndEffect[i];
            }

		}
		else if(!LogitechGSDK.LogiIsConnected(0))
		{
			 actualState = "PLEASE PLUG IN A STEERING WHEEL OR A FORCE FEEDBACK CONTROLLER";
		}
		else{
			actualState = "THIS WINDOW NEEDS TO BE IN FOREGROUND IN ORDER FOR THE SDK TO WORK PROPERLY";
		}
	}
}

2. Car Audio Controller

As the car physics was dynamic and environment was adaptable, we had to customize the car’s audio outputs to simulate real-world environment.

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

namespace Tagbin.Game.DrivingSimulator.Controllers {
    [RequireComponent(typeof(CarController))]
    public class CarAudio : MonoBehaviour {
        // This script reads some of the car's current properties and plays sounds accordingly.
        // The engine sound can be a simple single clip which is looped and pitched, or it
        // can be a crossfaded blend of four clips which represent the timbre of the engine
        // at different RPM and Throttle state.

        // the engine clips should all be a steady pitch, not rising or falling.

        // when using four channel engine crossfading, the four clips should be:
        // lowAccelClip : The engine at low revs, with throttle open (i.e. begining acceleration at very low speed)
        // highAccelClip : Thenengine at high revs, with throttle open (i.e. accelerating, but almost at max speed)
        // lowDecelClip : The engine at low revs, with throttle at minimum (i.e. idling or engine-braking at very low speed)
        // highDecelClip : Thenengine at high revs, with throttle at minimum (i.e. engine-braking at very high speed)

        // For proper crossfading, the clips pitches should all match, with an octave offset between low and high.


        public enum EngineAudioOptions // Options for the engine audio
        {
            Simple, // Simple style audio
            FourChannel // four Channel audio
        }

        public EngineAudioOptions engineSoundStyle = EngineAudioOptions.FourChannel;// Set the default audio options to be four channel
        public AudioClip lowAccelClip;                                              // Audio clip for low acceleration
        public AudioClip lowDecelClip;                                              // Audio clip for low deceleration
        public AudioClip highAccelClip;                                             // Audio clip for high acceleration
        public AudioClip highDecelClip;                                             // Audio clip for high deceleration
        public float pitchMultiplier = 1f;                                          // Used for altering the pitch of audio clips
        public float lowPitchMin = 1f;                                              // The lowest possible pitch for the low sounds
        public float lowPitchMax = 6f;                                              // The highest possible pitch for the low sounds
        public float highPitchMultiplier = 0.25f;                                   // Used for altering the pitch of high sounds
        public float maxRolloffDistance = 500;                                      // The maximum distance where rollof starts to take place
        public float dopplerLevel = 1;                                              // The mount of doppler effect used in the audio
        public bool useDoppler = true;                                              // Toggle for using doppler

        private AudioSource m_LowAccel; // Source for the low acceleration sounds
        private AudioSource m_LowDecel; // Source for the low deceleration sounds
        private AudioSource m_HighAccel; // Source for the high acceleration sounds
        private AudioSource m_HighDecel; // Source for the high deceleration sounds
        private bool m_StartedSound; // flag for knowing if we have started sounds
        private CarController m_CarController; // Reference to car we are controlling


        private void StartSound() {
            // get the carcontroller ( this will not be null as we have require component)
            m_CarController = GetComponent<CarController>();

            // setup the simple audio source
            m_HighAccel = SetUpEngineAudioSource(highAccelClip);

            // if we have four channel audio setup the four audio sources
            if(engineSoundStyle == EngineAudioOptions.FourChannel) {
                m_LowAccel = SetUpEngineAudioSource(lowAccelClip);
                m_LowDecel = SetUpEngineAudioSource(lowDecelClip);
                m_HighDecel = SetUpEngineAudioSource(highDecelClip);
            }

            // flag that we have started the sounds playing
            m_StartedSound = true;
        }


        private void StopSound() {
            //Destroy all audio sources on this object:
            foreach(var source in GetComponents<AudioSource>()) {
                Destroy(source);
            }

            m_StartedSound = false;
        }


        // Update is called once per frame
        private void Update() {
            // get the distance to main camera
            float camDist = (Camera.main.transform.position - transform.position).sqrMagnitude;

            // stop sound if the object is beyond the maximum roll off distance
            if(m_StartedSound && camDist > maxRolloffDistance * maxRolloffDistance) {
                StopSound();
            }

            // start the sound if not playing and it is nearer than the maximum distance
            if(!m_StartedSound && camDist < maxRolloffDistance * maxRolloffDistance) {
                StartSound();
            }

            if(m_StartedSound) {
                // The pitch is interpolated between the min and max values, according to the car's revs.
                float pitch = ULerp(lowPitchMin, lowPitchMax, m_CarController.Revs);

                // clamp to minimum pitch (note, not clamped to max for high revs while burning out)
                pitch = Mathf.Min(lowPitchMax, pitch);

                if(engineSoundStyle == EngineAudioOptions.Simple) {
                    // for 1 channel engine sound, it's oh so simple:
                    m_HighAccel.pitch = pitch * pitchMultiplier * highPitchMultiplier;
                    m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                    m_HighAccel.volume = 1;
                } else {
                    // for 4 channel engine sound, it's a little more complex:

                    // adjust the pitches based on the multipliers
                    m_LowAccel.pitch = pitch * pitchMultiplier;
                    m_LowDecel.pitch = pitch * pitchMultiplier;
                    m_HighAccel.pitch = pitch * highPitchMultiplier * pitchMultiplier;
                    m_HighDecel.pitch = pitch * highPitchMultiplier * pitchMultiplier;

                    // get values for fading the sounds based on the acceleration
                    float accFade = Mathf.Abs(m_CarController.AccelInput);
                    float decFade = 1 - accFade;

                    // get the high fade value based on the cars revs
                    float highFade = Mathf.InverseLerp(0.2f, 0.8f, m_CarController.Revs);
                    float lowFade = 1 - highFade;

                    // adjust the values to be more realistic
                    highFade = 1 - ((1 - highFade) * (1 - highFade));
                    lowFade = 1 - ((1 - lowFade) * (1 - lowFade));
                    accFade = 1 - ((1 - accFade) * (1 - accFade));
                    decFade = 1 - ((1 - decFade) * (1 - decFade));

                    // adjust the source volumes based on the fade values
                    m_LowAccel.volume = lowFade * accFade;
                    m_LowDecel.volume = lowFade * decFade;
                    m_HighAccel.volume = highFade * accFade;
                    m_HighDecel.volume = highFade * decFade;

                    // adjust the doppler levels
                    m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                    m_LowAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                    m_HighDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                    m_LowDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                }
            }
        }


        // sets up and adds new audio source to the gane object
        private AudioSource SetUpEngineAudioSource(AudioClip clip) {
            // create the new audio source component on the game object and set up its properties
            AudioSource source = gameObject.AddComponent<AudioSource>();
            source.clip = clip;
            source.volume = 0;
            source.loop = true;

            // start the clip from a random point
            source.time = Random.Range(0f, clip.length);
            source.Play();
            source.minDistance = 5;
            source.maxDistance = maxRolloffDistance;
            source.dopplerLevel = 0;
            return source;
        }


        // unclamped versions of Lerp and Inverse Lerp, to allow value to exceed the from-to range
        private static float ULerp(float from, float to, float value) {
            return (1.0f - value) * from + value * to;
        }
    }
}

3. Car Core Controller

The controller which dictated the core game logic of the car physics was Car Controller. It had the capacity to dynamically author car phyisics based on Front-Wheel Drive, Rear-Wheel Drive or Four-Wheel Drive selection.

4. Multi-Camera Projection Controller

As the whole setup was to be assembled at the event area, we needed a way to calibrate, align and manipulate the Camera Projection system as the wall sizes may vary upon the environment. This controller was created to manage eight main cameras of the project system, viz. Left Mirror, Right Mirror, Back Mirror, Left Side Window, Right Side Window, Front Windshield, Rear Windshield and the Dashboard screen. As Unity can handle only 8 multiple camera projections simultaneously hence no partition was placed between side windows.

5. Traffic Light System

Each objective was confined to a specific layout according to the R.T.O guidelines. There were many traffic lights which were sensor-based and automated based on the objective and level-difficulty selected.

Level Track System

According to R.T.O guidelines a driver was tested on six types of level tracks, viz., Zig-Zag Track, Round-About Track, Slob Track, H Track, Engligh-Eight Track and Reverse-S Track.