using System; using UnityEngine; using UnityEngine.XR; namespace MyInfoMate.Vr.Kiosk { /// /// Le mode borne — item E10 du lot XR-4. /// /// Ce qui sépare une démo d'une borne : une démo, quelqu'un la lance et la /// range ; une borne tourne 8 h par jour, passe de main en main, et personne ne la /// surveille. Le seul état acceptable pour le visiteur suivant est le menu, devant /// lui, comme si l'app venait de démarrer — pas la galerie que le précédent avait /// laissée ouverte, pas un menu dans son dos. /// /// Deux déclencheurs, et le second est le vrai : /// /// Le casque est reposé — le signal le plus fiable qui existe : la /// visite est finie, tout de suite, sans attendre. /// Plus rien ne bouge pendant un moment — le repli, pour le casque /// posé sur une table sans que personne ne l'ait retiré de la tête. /// /// /// Sans SDK Meta. La présence du porteur se lit par CommonUsages.userPresence /// d'Unity XR, alimenté par OpenXR : pas besoin d'un OVRManager dans la scène, /// donc rien à configurer et rien à oublier de configurer. /// public class KioskSession : MonoBehaviour { /// /// 90 s : assez long pour qu'un visiteur qui lit un panneau ne soit pas renvoyé /// au menu, assez court pour que le suivant ne trouve pas l'écran du précédent. /// [SerializeField] float idleSeconds = 90f; /// Au-delà, la tête a bougé : quelqu'un est là. const float MovementThresholdDegrees = 3f; Transform _head; Quaternion _lastRotation; float _idle; bool _wasWorn = true; /// La visite est finie — revenir au menu et repartir de zéro. public event Action Ended; public static KioskSession Attach(GameObject host, Transform head) { var session = host.AddComponent(); session._head = head; session._lastRotation = head != null ? head.rotation : Quaternion.identity; // Une borne ne doit pas s'éteindre toute seule pendant qu'un visiteur lit. Screen.sleepTimeout = SleepTimeout.NeverSleep; return session; } /// /// À appeler sur toute action délibérée. Le mouvement de tête suffit à détecter /// une présence, mais pas à prouver l'attention : quelqu'un qui vise et choisit /// est actif même s'il bouge à peine. /// public void NotifyActivity() => _idle = 0f; void Update() { if (IsWorn()) { if (!_wasWorn) { // Le casque vient d'être remis : c'est un nouveau visiteur, même si // le précédent l'a reposé il y a dix secondes. _wasWorn = true; _idle = 0f; Ended?.Invoke(); } } else if (_wasWorn) { _wasWorn = false; Ended?.Invoke(); return; } if (!_wasWorn) return; if (_head != null) { if (Quaternion.Angle(_head.rotation, _lastRotation) > MovementThresholdDegrees) { _lastRotation = _head.rotation; _idle = 0f; } } _idle += Time.deltaTime; if (_idle < idleSeconds) return; _idle = 0f; Ended?.Invoke(); } /// /// Vrai quand le casque est porté. Un runtime qui ne sait pas répondre — le /// simulateur, l'éditeur — renvoie « porté » : sur un poste de développement, une /// borne qui se réinitialise toutes les 90 s serait intenable. /// static bool IsWorn() { var device = InputDevices.GetDeviceAtXRNode(XRNode.Head); if (!device.isValid) return true; return !device.TryGetFeatureValue(CommonUsages.userPresence, out var present) || present; } } }