//------------------------------------------------------------------------------------------------ // Edy's Vehicle Physics // (c) Angel Garcia "Edy" - Oviedo, Spain // http://www.edy.es //------------------------------------------------------------------------------------------------ using UnityEngine; using System; namespace EVP { // Per-wheel audio-related data public class WheelAudioData { public float lastDownforce = 0.0f; public float lastWheelBumpTime = 0.0f; } [RequireComponent(typeof(VehicleController))] public class VehicleAudio : MonoBehaviour { // Settings are arranged in classes for better organization [Serializable] public class Engine { public AudioSource audioSource; [Space(5)] public float idleRpm = 600.0f; public float idlePitch = 0.25f; public float idleVolume = 0.4f; [Space(5)] public float maxRpm = 6000.0f; public float maxPitch = 1.5f; public float maxVolume = 0.6f; [Space(5)] public float throttleRpmBoost = 500.0f; public float throttleVolumeBoost = 0.4f; [Space(5)] public float wheelsToEngineRatio = 16.0f; public int gears = 5; public float gearDownRpm = 2500.0f; public float gearUpRpm = 5000.0f; [Space(5)] public float gearUpRpmRate = 5.0f; public float gearDownRpmRate = 6.0f; public float volumeChangeRateUp = 48.0f; public float volumeChangeRateDown = 16.0f; } [Serializable] public class EngineExtras { public AudioSource turboAudioSource; public float turboMinRpm = 3500.0f; public float turboMaxRpm = 5500.0f; [Range(0,3)] public float turboMinPitch = 0.8f; [Range(0,3)] public float turboMaxPitch = 1.5f; [Range(0,1)] public float turboMaxVolume = 1.0f; public bool turboRequiresThrottle = true; [Space(5)] public AudioClip turboDumpClip; public float turboDumpMinRpm = 5000.0f; public float turboDumpMinInterval = 2.0f; public float turboDumpMinThrottleTime = 0.3f; public float turboDumpVolume = 0.5f; [Space(5)] public AudioSource transmissionAudioSource; [Range(0.1f,1)] public float transmissionMaxRatio = 0.9f; // Ratio of maximum transmission velocity (maxRpm * gearCount) at which transmission is considered maximum [Range(0,3)] public float transmissionMinPitch = 0.2f; [Range(0,3)] public float transmissionMaxPitch = 1.1f; [Range(0,1)] public float transmissionMinVolume = 0.1f; [Range(0,1)] public float transmissionMaxVolume = 0.2f; } [Serializable] public class Wheels { public AudioSource skidAudioSource; public float skidMinSlip = 2.0f; public float skidMaxSlip = 7.0f; [Range(0,3)] public float skidMinPitch = 0.9f; [Range(0,3)] public float skidMaxPitch = 0.8f; [Range(0,1)] public float skidMaxVolume = 0.75f; [Space(5)] public AudioSource offroadAudioSource; public float offroadMinSpeed = 1.0f; public float offroadMaxSpeed = 20.0f; [Range(0,3)] public float offroadMinPitch = 0.3f; [Range(0,3)] public float offroadMaxPitch = 2.5f; [Range(0,1)] public float offroadMinVolume = 0.3f; [Range(0,1)] public float offroadMaxVolume = 0.6f; [Space(5)] public AudioClip bumpAudioClip; public float bumpMinForceDelta = 2000.0f; // Minimum force change in a fixed time step to be considered a bump public float bumpMaxForceDelta = 18000.0f; // Force change at which the bump is considered to be maximum intensity [Range(0,1)] public float bumpMinVolume = 0.2f; // Volume to be applied at the minimum bump intensity [Range(0,1)] public float bumpMaxVolume = 0.6f; // Volume to be applied at the maximum bump intensity } [Serializable] public class Impacts { [Space(5)] public AudioClip hardImpactAudioClip; public AudioClip softImpactAudioClip; public float minSpeed = 0.1f; // Min contact speed to be considered an impact. public float maxSpeed = 10.0f; // Contact speed at which the impact is considered maximum intensity. [Range(0,3)] public float minPitch = 0.3f; // Pitch to be applied at minimum impact speed [Range(0,3)] public float maxPitch = 0.6f; // Pitch to ba applied at maximum impact speed [Range(0,3)] public float randomPitch = 0.2f; // Random pitch range (+- value) to be added to the pitch [Range(0,1)] public float minVolume = 0.7f; // Volume to be applied at minimum impact speed [Range(0,1)] public float maxVolume = 1.0f; // Volume to be applied at maximum impact speed [Range(0,1)] public float randomVolume = 0.2f; // Random volume range (+- value) to be added to the volume } [Serializable] public class Drags { public AudioSource hardDragAudioSource; public AudioSource softDragAudioSource; public float minSpeed = 2.0f; public float maxSpeed = 20.0f; [Range(0,3)] public float minPitch = 0.6f; [Range(0,3)] public float maxPitch = 0.8f; [Range(0,1)] public float minVolume = 0.8f; [Range(0,1)] public float maxVolume = 1.0f; [Space(5)] public AudioClip scratchAudioClip; public float scratchRandomThreshold = 0.02f; // Percentage of drag events that cause a drag public float scratchMinSpeed = 2.0f; public float scratchMinInterval = 0.2f; [Range(0,3)] public float scratchMinPitch = 0.7f; [Range(0,3)] public float scratchMaxPitch = 1.1f; [Range(0,1)] public float scratchMinVolume = 0.9f; [Range(0,1)] public float scratchMaxVolume = 1.0f; } [Serializable] public class Wind { public AudioSource windAudioSource; public float minSpeed = 3.0f; public float maxSpeed = 30.0f; [Range(0,3)] public float minPitch = 0.5f; [Range(0,3)] public float maxPitch = 1.0f; [Range(0,1)] public float maxVolume = 0.5f; } // Actual public properties [Tooltip("AudioSource to be used with the one-time audio effects (impacts, etc)")] public AudioSource audioClipTemplate; [Space(2)] public Engine engine = new Engine(); [Space(2)] public EngineExtras engineExtras = new EngineExtras(); [Space(2)] public Wheels wheels = new Wheels(); [Space(2)] public Impacts impacts = new Impacts(); [Space(2)] public Drags drags = new Drags(); [Space(2)] public Wind wind = new Wind(); // Additional less-relevant properties. To be configured from scripting if necessary. [NonSerialized] public float skidRatioChangeRate = 40.0f; [NonSerialized] public float offroadSpeedChangeRate = 20.0f; [NonSerialized] public float offroadCutoutSpeed = 0.02f; [NonSerialized] public float dragCutoutSpeed = 0.01f; [NonSerialized] public float turboRatioChangeRate = 8.0f; [NonSerialized] public float wheelsRpmChangeRateLimit = 400.0f; // Private fields VehicleController m_vehicle; float m_engineRpm = 0.0f; float m_engineThrottleRpm = 0.0f; float m_engineRpmDamp; float m_wheelsRpm = 0.0f; int m_gear = 0; int m_lastGear = 0; float m_skidRatio = 0.0f; float m_offroadSpeed = 0.0f; float m_lastScratchTime = 0.0f; float m_turboRatio = 0.0f; float m_lastTurboDumpTime = 0.0f; float m_lastThrottleInput = 0.0f; float m_lastThrottlePressedTime = 0.0f; WheelAudioData[] m_audioData = new WheelAudioData[0]; // Public access to some private fields public int simulatedGear { get { return m_gear; } } public float simulatedEngineRpm { get { return m_engineRpm; } } void OnEnable () { // Configure the vehicle: report impacts, compute extended tire data (for skid) m_vehicle = GetComponent(); m_vehicle.processContacts = true; m_vehicle.onImpact += DoImpactAudio; m_vehicle.computeExtendedTireData = true; // Verify / configure parameters if (engine.gears < 2) engine.gears = 2; m_engineRpmDamp = engine.gearUpRpmRate; m_wheelsRpm = 0.0f; // Verify the audio sources to belong to the actual vehicle (editor only) VerifyAudioSources(); } void OnDisable () { StopAllAudioSources(); } void Update () { if (m_vehicle.paused) { StopAllAudioSources(); return; } DoEngineAudio(); // At this point these values are available: // // - m_gear and m_lastGear (m_gear comes from DoEngineAudio) // - m_vehicle.throttleInput and m_lastThrottleInput // // This allows other parts to react to gear and throttle changes. DoEngineExtraAudio(); DoBodyDragAudio(); DoWindAudio(); DoTireAudio(); m_lastGear = m_gear; m_lastThrottleInput = m_vehicle.throttleInput; } void FixedUpdate () { if (m_vehicle.wheelData.Length != m_audioData.Length) InitializeAudioData(); // TO-DO: Base it on the speed of movement instead of the force. // We should trace the speed of movement in WheelData, in the vehicle's fixed update. // Then move to Update. // Right now it's somewhat designed to use FixedUpdate if (!m_vehicle.paused) DoWheelBumpAudio(); } void InitializeAudioData () { m_audioData = new WheelAudioData[m_vehicle.wheelData.Length]; for (int i=0; i= 0) { // First gear goes from idle to gearUp float firstGear = engine.gearUpRpm - engine.idleRpm; if (transmissionRpm < firstGear) { m_gear = 1; updatedEngineRpm = transmissionRpm + engine.idleRpm; } else { // Second gear and above go from gearDown to gearUp float gearWidth = engine.gearUpRpm - engine.gearDownRpm; m_gear = 2 + (int)((transmissionRpm - firstGear) / gearWidth); if (m_gear > engine.gears) { m_gear = engine.gears; updatedEngineRpm = transmissionRpm - firstGear - (engine.gears - 2) * gearWidth + engine.gearDownRpm; } else { updatedEngineRpm = Mathf.Repeat(transmissionRpm - firstGear, gearWidth) + engine.gearDownRpm; } } } else { // Reverse gear m_gear = -1; updatedEngineRpm = Mathf.Abs(transmissionRpm) + engine.idleRpm; } updatedEngineRpm = Mathf.Clamp(updatedEngineRpm, 10.0f, engine.maxRpm); if (m_gear != m_lastGear) { m_engineRpmDamp = m_gear > m_lastGear ? engine.gearUpRpmRate : engine.gearDownRpmRate; // m_lastGear will be configured outside, so other methods can detect gear changes as well. } m_engineRpm = Mathf.Lerp(m_engineRpm, updatedEngineRpm, m_engineRpmDamp * Time.deltaTime); m_engineThrottleRpm = Mathf.Lerp(m_engineThrottleRpm, m_vehicle.throttleInput * engine.throttleRpmBoost, m_engineRpmDamp * Time.deltaTime); if (engine.audioSource != null) { float engineRatio = Mathf.InverseLerp(engine.idleRpm, engine.maxRpm, m_engineRpm + m_engineThrottleRpm); ProcessContinuousAudioPitch(engine.audioSource, engineRatio, engine.idlePitch, engine.maxPitch); float engineVolume = Mathf.Lerp(engine.idleVolume, engine.maxVolume, engineRatio) + Mathf.Abs(m_vehicle.throttleInput) * engine.throttleVolumeBoost; ProcessVolume(engine.audioSource, engineVolume, engine.volumeChangeRateUp, engine.volumeChangeRateDown); } } void DoEngineExtraAudio () { // Turbo audio float updatedTurboRatio = Mathf.InverseLerp(engineExtras.turboMinRpm, engineExtras.turboMaxRpm, m_engineRpm); if (engineExtras.turboRequiresThrottle) updatedTurboRatio *= Mathf.Clamp01(m_vehicle.throttleInput); m_turboRatio = Mathf.Lerp(m_turboRatio, updatedTurboRatio, turboRatioChangeRate * Time.deltaTime); ProcessContinuousAudio(engineExtras.turboAudioSource, m_turboRatio, engineExtras.turboMinPitch, engineExtras.turboMaxPitch, 0.0f, engineExtras.turboMaxVolume); // Turbo dump audio if (engineExtras.turboDumpClip != null) { if (Time.time-m_lastTurboDumpTime > engineExtras.turboDumpMinInterval && m_engineRpm > engineExtras.turboDumpMinRpm) { bool gearChangedUp = m_gear != m_lastGear && m_lastGear > 0 && m_gear > 0 && m_gear > m_lastGear; bool throttleReleased = m_vehicle.throttleInput < 0.5f && (m_vehicle.throttleInput - m_lastThrottleInput) / Time.deltaTime < -20.0f; float throttlePressedTime = Time.time - m_lastThrottlePressedTime; if (m_vehicle.throttleInput < 0.2f) m_lastThrottlePressedTime = Time.time; if ((gearChangedUp || throttleReleased) && throttlePressedTime > engineExtras.turboDumpMinThrottleTime) { Vector3 pos = engineExtras.turboAudioSource != null? engineExtras.turboAudioSource.transform.position : m_vehicle.cachedTransform.position; PlayOneTime(engineExtras.turboDumpClip, pos, engineExtras.turboDumpVolume); m_lastTurboDumpTime = Time.time; } } } // Transmission audio float transmissionRatio = Mathf.Abs(m_wheelsRpm * engine.wheelsToEngineRatio) / (engine.maxRpm * engine.gears * engineExtras.transmissionMaxRatio); ProcessContinuousAudio(engineExtras.transmissionAudioSource, transmissionRatio, engineExtras.transmissionMinPitch, engineExtras.transmissionMaxPitch, engineExtras.transmissionMinVolume, engineExtras.transmissionMaxVolume); } void DoTireAudio () { float currentSkidRatio = 0.0f; float currentOffroadSpeed = 0.0f; int offroadWheels = 0; // Skid uses the sum of all wheels: a single wheel skidding to the top causes maximum value. // Offroad uses the average value of all wheels over offroad surface. foreach (WheelData wd in m_vehicle.wheelData) { // If no ground material is found, then the surface is considered "hard" (skid audio) if (wd.groundMaterial == null || wd.groundMaterial.surfaceType == GroundMaterial.SurfaceType.Hard) { // Skid value is the sum of the skid ratios based on the actual parameters currentSkidRatio += Mathf.InverseLerp(wheels.skidMinSlip, wheels.skidMaxSlip, wd.combinedTireSlip); } else { // Offroad value is the average velocity of the tire over the surface among all wheels. if (wd.grounded) { currentOffroadSpeed += wd.velocity.magnitude + Mathf.Abs(wd.tireSlip.y); offroadWheels++; } } } // Skid value receives the skid ratio based on wheels.skidMinSlip and wheels.skidMaxSlip m_skidRatio = Mathf.Lerp(m_skidRatio, currentSkidRatio, skidRatioChangeRate * Time.deltaTime); ProcessContinuousAudio(wheels.skidAudioSource, m_skidRatio, wheels.skidMinPitch, wheels.skidMaxPitch, 0.0f, wheels.skidMaxVolume); // Offroad value receives the actual velocity of the wheel over the surface. // It's split in two lineal ranges: // - from cutout to min // - from min to max if (offroadWheels > 1) currentOffroadSpeed /= offroadWheels; m_offroadSpeed = Mathf.Lerp(m_offroadSpeed, currentOffroadSpeed, offroadSpeedChangeRate * Time.deltaTime); ProcessSpeedBasedAudio(wheels.offroadAudioSource, m_offroadSpeed, offroadCutoutSpeed, wheels.offroadMinSpeed, wheels.offroadMaxSpeed, 0.0f, wheels.offroadMinPitch, wheels.offroadMaxPitch, wheels.offroadMinVolume, wheels.offroadMaxVolume); } void DoImpactAudio () { // Body impacts if (impacts.hardImpactAudioClip != null || impacts.softImpactAudioClip != null) { float impactSpeed = m_vehicle.localImpactVelocity.magnitude; if (impactSpeed > impacts.minSpeed) { float impactRatio = Mathf.InverseLerp (impacts.minSpeed, impacts.maxSpeed, impactSpeed); AudioClip clip = null; if (!impacts.softImpactAudioClip) { clip = impacts.hardImpactAudioClip; } else { clip = m_vehicle.isHardImpact? impacts.hardImpactAudioClip : impacts.softImpactAudioClip; } if (clip) PlayOneTime(clip, m_vehicle.cachedTransform.TransformPoint(m_vehicle.localImpactPosition), Mathf.Lerp(impacts.minVolume, impacts.maxVolume, impactRatio) + UnityEngine.Random.Range(-impacts.randomVolume, impacts.randomVolume), Mathf.Lerp(impacts.minPitch, impacts.maxPitch, impactRatio) + UnityEngine.Random.Range(-impacts.randomPitch, impacts.randomPitch)); // Debug.Log("Impact! " + impactRatio); } } } void DoBodyDragAudio () { // Continuous drag audio float dragSpeed = m_vehicle.localDragVelocity.magnitude; float hardDragSpeed = m_vehicle.isHardDrag? dragSpeed : 0.0f; float softDragSpeed = m_vehicle.isHardDrag? 0.0f : dragSpeed; ProcessSpeedBasedAudio(drags.hardDragAudioSource, hardDragSpeed, dragCutoutSpeed, drags.minSpeed, drags.maxSpeed, 0.0f, drags.minPitch, drags.maxPitch, drags.minVolume, drags.maxVolume); ProcessSpeedBasedAudio(drags.softDragAudioSource, softDragSpeed, dragCutoutSpeed, drags.minSpeed, drags.maxSpeed, 0.0f, drags.minPitch, drags.maxPitch, drags.minVolume, drags.maxVolume); // Random body scratch sounds on hard surfaces only if (drags.scratchAudioClip != null) { if (dragSpeed > drags.scratchMinSpeed && m_vehicle.isHardDrag && UnityEngine.Random.value < drags.scratchRandomThreshold && Time.time-m_lastScratchTime > drags.scratchMinInterval) { PlayOneTime(drags.scratchAudioClip, m_vehicle.cachedTransform.TransformPoint(m_vehicle.localDragPosition), UnityEngine.Random.Range(drags.scratchMinVolume, drags.scratchMaxVolume), UnityEngine.Random.Range(drags.scratchMinPitch, drags.scratchMaxPitch)); m_lastScratchTime = Time.time; } } } void DoWheelBumpAudio () { if (wheels.bumpAudioClip == null) return; for (int i=0, c=m_vehicle.wheelData.Length; i wheels.bumpMinForceDelta && (Time.fixedTime - ad.lastWheelBumpTime) > 0.03f) { ProcessWheelBumpAudio(suspensionForceDelta, wd.transform.position); ad.lastWheelBumpTime = Time.fixedTime; } } } void DoWindAudio () { float windRatio = Mathf.InverseLerp(wind.minSpeed, wind.maxSpeed, m_vehicle.cachedRigidbody.velocity.magnitude); ProcessContinuousAudio(wind.windAudioSource, windRatio, wind.minPitch, wind.maxPitch, 0.0f, wind.maxVolume); } void StopAllAudioSources () { StopAudio(engine.audioSource); StopAudio(engineExtras.turboAudioSource); StopAudio(engineExtras.transmissionAudioSource); StopAudio(wheels.skidAudioSource); StopAudio(wheels.offroadAudioSource); StopAudio(drags.hardDragAudioSource); StopAudio(drags.softDragAudioSource); StopAudio(wind.windAudioSource); } //---------------------------------------------------------------------------------------------- void StopAudio (AudioSource audio) { if (audio != null) audio.Stop(); } void ProcessContinuousAudio (AudioSource audio, float ratio, float minPitch, float maxPitch, float minVolume, float maxVolume) { if (audio == null) return; audio.pitch = Mathf.Lerp(minPitch, maxPitch, ratio); audio.volume = Mathf.Lerp(minVolume, maxVolume, ratio); if (!audio.isPlaying && audio.isActiveAndEnabled) audio.Play(); audio.loop = true; } void ProcessContinuousAudioPitch (AudioSource audio, float ratio, float minPitch, float maxPitch) { if (audio == null) return; audio.pitch = Mathf.Lerp(minPitch, maxPitch, ratio); if (!audio.isPlaying && audio.isActiveAndEnabled) audio.Play(); audio.loop = true; } void ProcessVolume (AudioSource audio, float volume, float changeRateUp, float changeRateDown) { float changeRate = volume > audio.volume? changeRateUp : changeRateDown; audio.volume = Mathf.Lerp(audio.volume, volume, Time.deltaTime * changeRate); } void ProcessSpeedBasedAudio (AudioSource audio, float speed, float cutoutSpeed, float minSpeed, float maxSpeed, float cutoutPitch, float minPitch, float maxPitch, float minVolume, float maxVolume) { if (audio == null) return; if (speed < cutoutSpeed) { if (audio.isPlaying) audio.Stop(); } else { if (speed < minSpeed) { float ratio = Mathf.InverseLerp(cutoutSpeed, minSpeed, speed); audio.pitch = Mathf.Lerp(cutoutPitch, minPitch, ratio); audio.volume = Mathf.Lerp(0.0f, minVolume, ratio); } else { float ratio = Mathf.InverseLerp(minSpeed, maxSpeed, speed); audio.pitch = Mathf.Lerp(minPitch, maxPitch, ratio); audio.volume = Mathf.Lerp(minVolume, maxVolume, ratio); } if (!audio.isPlaying && audio.isActiveAndEnabled) audio.Play(); audio.loop = true; } } void ProcessWheelBumpAudio (float suspensionForceDelta, Vector3 position) { float bumpRatio = Mathf.InverseLerp(wheels.bumpMinForceDelta, wheels.bumpMaxForceDelta, suspensionForceDelta); PlayOneTime(wheels.bumpAudioClip, position, Mathf.Lerp(wheels.bumpMinVolume, wheels.bumpMaxVolume, bumpRatio)); } // Playing audio effects. // // We don't use AudioSource.PlayClipAtPoint because we need our sounds to be // parented to the vehicle. void PlayOneTime (AudioClip clip, Vector3 position, float volume) { PlayOneTime(clip, position, volume, 1.0f); } void PlayOneTime (AudioClip clip, Vector3 position, float volume, float pitch) { if (clip == null || pitch < 0.01f || volume < 0.01f) return; GameObject go; AudioSource source; if (audioClipTemplate != null) { go = (GameObject)Instantiate(audioClipTemplate.gameObject, position, Quaternion.identity); source = go.GetComponent(); go.transform.parent = audioClipTemplate.transform.parent; } else { go = new GameObject("One shot audio"); go.transform.parent = m_vehicle.cachedTransform; go.transform.position = position; source = null; } if (source == null) { source = go.AddComponent() as AudioSource; source.spatialBlend = 1.0f; } if (source.isActiveAndEnabled) { source.clip = clip; source.volume = volume; source.pitch = pitch; source.dopplerLevel = 0.0f; // Doppler causes artifacts as for positioning the audio source source.Play(); } Destroy(go, clip.length / pitch); } void VerifyAudioSources () { #if UNITY_EDITOR VerifyAudioSource(ref engine.audioSource); VerifyAudioSource(ref engineExtras.turboAudioSource); VerifyAudioSource(ref engineExtras.transmissionAudioSource); VerifyAudioSource(ref wheels.skidAudioSource); VerifyAudioSource(ref wheels.offroadAudioSource); VerifyAudioSource(ref drags.hardDragAudioSource); VerifyAudioSource(ref drags.softDragAudioSource); VerifyAudioSource(ref wind.windAudioSource); VerifyAudioSource(ref audioClipTemplate); #endif } void VerifyAudioSource (ref AudioSource audioSource) { if (audioSource != null && !audioSource.transform.IsChildOf(m_vehicle.transform)) { Debug.LogWarning(m_vehicle.name + ": VehicleAudio: The audio source [" + audioSource.name + "] is not child of this vehicle. Disabled.", m_vehicle); audioSource = null; } } } }