// CollisionVelocitySound.cs // Plays an impact sound when something bumps into this object, or this object bumps into something. // It works both ways round: on a moving physics object (a ball, a crate with Use Rigidbody on), or on a still object // (a wall, a bell, a gem) that moving physics objects hit. The faster the two meet, the higher the sound plays. // Hitting an object whose name contains MatchName plays AudioObject; anything else plays AlternateAudioObject // (leave it empty for silence). Both objects need a Collider; at least one of them must be a moving physics object. // // Setup: put this script on the object, then pick the object(s) holding the Audio Source. using System.Collections; using System.Collections.Generic; using IHXR.Simulation; public class CollisionVelocitySound : ScriptBase { [Header("Sounds")] [Tooltip("The object with the Audio Source played when this object hits something whose name contains MatchName.")] public SceneObject AudioObject; [Tooltip("Optional: played when it hits anything else. Leave empty to stay silent then.")] public SceneObject AlternateAudioObject; [Tooltip("Part of the other object's name (capitals don't matter). Leave empty to use AudioObject for every hit.")] public string MatchName = "ball"; public int AudioSourceIndex = 0; public bool IncludeAudioChildren = true; [Tooltip("Restart the sound on every hit instead of letting the previous one finish.")] public bool StopBeforePlay = true; [Header("When it plays")] [Tooltip("Hits slower than this (metres per second, how fast the two objects meet) make no sound.")] public float MinSpeedToPlay = 0.1f; [Tooltip("Shortest time between two sounds, in milliseconds (stops a rolling object rattling).")] public float CooldownMilliseconds = 150f; [Tooltip("Also count hits on this object's children.")] public bool IncludeCollisionChildren = true; [Tooltip("Seconds to wait before listening for hits (lets objects settle when the world loads).")] public float StartDelaySeconds = 1f; [Header("Pitch by speed")] [Range(0.1f, 3f)] public float MinPitch = 0.85f; [Range(0.1f, 3f)] public float MaxPitch = 1.35f; [Tooltip("At this speed (or slower) the sound plays at MinPitch.")] public float MinSpeedForPitch = 0f; [Tooltip("At this speed (or faster) the sound plays at MaxPitch.")] public float MaxSpeedForPitch = 12f; [Header("Debug")] public bool LogHits = false; Rigidbody rb; Coroutine startRoutine; float nextAllowedTime; bool warnedNoAudio; // The world reports a hit every moment two objects touch; only the first moment of each contact makes a sound // (a ball resting on this object, or rolling along it, does not keep ringing). Dictionary lastTouched = new Dictionary(); const float NewContactGapSeconds = 0.25f; public override void Start() { rb = gameObject.GetComponent() ?? gameObject.GetComponentInParent(); if (AudioObject == null || AudioObject.IsNull) LogWarning("Pick an AudioObject (an object with an Audio Source)."); startRoutine = StartCoroutine(ListenAfterDelay()); } IEnumerator ListenAfterDelay() { if (StartDelaySeconds > 0f) yield return new WaitForSeconds(StartDelaySeconds); else yield return null; RegisterCollision(OnCollision, IncludeCollisionChildren); startRoutine = null; if (LogHits) Log("Listening for hits."); } public override void OnDestroy() { if (startRoutine != null) StopCoroutine(startRoutine); } void OnCollision(CollisionInfo info) { float now = Time.time; string otherId = info?.ObjectId ?? ""; float lastTime; bool newContact = !lastTouched.TryGetValue(otherId, out lastTime) || now - lastTime > NewContactGapSeconds; lastTouched[otherId] = now; if (!newContact) return; bool matched = NameMatches(info); SceneObject sound = matched ? AudioObject : AlternateAudioObject; if (sound == null || sound.IsNull) { if (matched && !warnedNoAudio) { LogWarning("A hit made no sound: AudioObject is empty."); warnedNoAudio = true; } return; } if (now < nextAllowedTime) return; // How fast the two objects meet: this object's speed against the other one's (either may be standing still). Vector3 mine = rb != null ? rb.linearVelocity : Vector3.zero; Rigidbody otherBody = info != null && info.OtherObject != null && !info.OtherObject.IsNull ? info.OtherObject.GetComponent() : null; Vector3 theirs = otherBody != null ? otherBody.linearVelocity : Vector3.zero; float speed = (mine - theirs).magnitude; if (speed < MinSpeedToPlay) { if (LogHits && speed > 0.01f) Log("Touching '" + (info?.ObjectName ?? "") + "' at " + speed.ToString("0.00") + " m/s: slower than MinSpeedToPlay, no sound."); return; } float t = Mathf.InverseLerp(MinSpeedForPitch, Mathf.Max(MinSpeedForPitch + 0.001f, MaxSpeedForPitch), speed); float pitch = Mathf.Lerp(MinPitch, MaxPitch, Mathf.Clamp01(t)); SetAudioPitch(sound, pitch, null, AudioSourceIndex, IncludeAudioChildren); if (StopBeforePlay) StopAudio(sound, null, AudioSourceIndex, IncludeAudioChildren); PlayAudio(sound, null, AudioSourceIndex, IncludeAudioChildren); nextAllowedTime = now + Mathf.Max(0f, CooldownMilliseconds) * 0.001f; if (LogHits) Log("Hit '" + (info?.ObjectName ?? "") + "' at " + speed.ToString("0.00") + " m/s: pitch " + pitch.ToString("0.00") + (matched ? "" : " (alternate sound)")); } bool NameMatches(CollisionInfo info) { if (string.IsNullOrWhiteSpace(MatchName)) return true; string name = info?.ObjectName ?? ""; return name.ToLowerInvariant().Contains(MatchName.Trim().ToLowerInvariant()); } }