using System.Collections; using UnityEngine; namespace MyInfoMate.Vr.Menu { /// /// Le retour au survol et à la sélection : un son court, et une vibration quand il y /// a une manette pour la sentir. /// /// Ce que ça règle. En visée à la tête, rien ne confirme qu'on a bien accroché /// un panneau avant que l'anneau ne commence à se remplir — et l'anneau est en /// périphérie du regard au moment où on l'attend. Un son de 40 ms dit « tu es /// dessus » avant même que l'œil ne l'ait vu. /// /// Les sons sont synthétisés, pas chargés. Deux raisons : aucun fichier à /// gérer dans un repo qui n'a pas d'assets, et un clic généré est identique sur tous /// les casques. Ils sont volontairement discrets — une borne de musée qui claque à /// chaque regard devient insupportable en une heure. /// public class MenuFeedback : MonoBehaviour { const int SampleRate = 44100; static MenuFeedback _instance; AudioSource _source; AudioClip _hover; AudioClip _select; static MenuFeedback Instance { get { if (_instance != null) return _instance; var root = new GameObject("MenuFeedback"); DontDestroyOnLoad(root); _instance = root.AddComponent(); _instance.Build(); return _instance; } } void Build() { _source = gameObject.AddComponent(); _source.playOnAwake = false; // 2D : le retour n'appartient pas à un endroit de la pièce, il appartient au // geste. Le spatialiser le ferait venir d'un panneau qu'on est en train de // quitter. _source.spatialBlend = 0f; _hover = Tone("hover", 880f, 0.045f, 0.18f); _select = Tone("select", 1320f, 0.09f, 0.32f, 660f); } /// /// Une sinusoïde avec une enveloppe qui décroît. /// non nul fait un glissando : c'est ce qui distingue « choisi » de « survolé » /// sans avoir à monter le volume. /// static AudioClip Tone(string name, float frequency, float seconds, float volume, float startFrequency = 0f) { var samples = Mathf.RoundToInt(SampleRate * seconds); var data = new float[samples]; var phase = 0f; for (var i = 0; i < samples; i++) { var t = i / (float)samples; var f = startFrequency > 0f ? Mathf.Lerp(startFrequency, frequency, t) : frequency; phase += 2f * Mathf.PI * f / SampleRate; // Attaque très courte pour éviter le claquement d'un début abrupt, puis // décroissance exponentielle. var envelope = Mathf.Min(1f, t * 40f) * Mathf.Exp(-5f * t); data[i] = Mathf.Sin(phase) * envelope * volume; } var clip = AudioClip.Create(name, samples, 1, SampleRate, false); clip.SetData(data, 0); return clip; } public static void Hover() => Instance.Play(Instance._hover, 0.12f, 0.05f); public static void Select() => Instance.Play(Instance._select, 0.5f, 0.12f); void Play(AudioClip clip, float vibrationAmplitude, float vibrationSeconds) { if (clip != null) _source.PlayOneShot(clip); var controller = OVRInput.GetActiveController(); if (controller != OVRInput.Controller.RTouch && controller != OVRInput.Controller.LTouch) return; StopAllCoroutines(); StartCoroutine(Vibrate(controller, vibrationAmplitude, vibrationSeconds)); } IEnumerator Vibrate(OVRInput.Controller controller, float amplitude, float seconds) { OVRInput.SetControllerVibration(0.4f, amplitude, controller); yield return new WaitForSeconds(seconds); OVRInput.SetControllerVibration(0f, 0f, controller); } } }