using UnityEngine; namespace MyInfoMate.Vr.Scene { /// /// Une phrase affichée dans le casque, lisible, à hauteur d'yeux. /// /// C'est un livrable, pas un raffinement. Une scène qu'on ne sait pas /// charger — décor en splats, manifeste d'une version inconnue, asset absent — /// doit le dire. Sur site, personne ne lit une console : un écran noir devient /// « le casque est cassé », et un message devient un appel au support qui sait /// quoi corriger. /// public class SceneMessage : MonoBehaviour { // 3,50 m : à 2 m un texte de cette taille occupe trop de champ pour être lu // confortablement, et l'oeil doit converger de près. Plus loin, il se lit // d'un seul regard. const float DistanceMeters = 3.5f; const float HeightMeters = 1.5f; /// Au-delà de cet écart, le visiteur ne regarde plus le message. const float ReleaseAngleDegrees = 45f; const float RecentringSpeed = 3f; Transform _head; bool _recentring; public static SceneMessage Show(string message, Transform visitorHead) { var instance = new GameObject("SceneMessage").AddComponent(); instance.Build(message, visitorHead); return instance; } void Build(string message, Transform visitorHead) { _head = visitorHead; var label = Ui.Label.Create( transform, "Text", Ui.Label.Anchor.MiddleCenter, 0.08f, Color.white); label.Text = Wrap(message, 40); if (_head != null) transform.position = TargetPosition(); FaceVisitor(); } /// /// Le message reste posé tant que le visiteur le regarde, et ne se /// recentre que s'il lui tourne le dos. Le suivre à chaque image le rend /// illisible — on ne peut pas fixer un texte qui fuit ; ne jamais le suivre /// le laisserait dans le dos du visiteur, donc jamais lu. /// void LateUpdate() { if (_head == null) return; if (LookingAway()) _recentring = true; if (_recentring) { var target = TargetPosition(); transform.position = Vector3.Lerp( transform.position, target, Time.deltaTime * RecentringSpeed); if ((transform.position - target).sqrMagnitude < 0.0004f) _recentring = false; } FaceVisitor(); } bool LookingAway() { var toMessage = transform.position - _head.position; toMessage.y = 0f; if (toMessage.sqrMagnitude < 0.01f) return true; return Vector3.Angle(HeadForward(), toMessage.normalized) > ReleaseAngleDegrees; } Vector3 TargetPosition() { return new Vector3(_head.position.x, HeightMeters, _head.position.z) + HeadForward() * DistanceMeters; } /// Le texte fait face au visiteur, sans jamais s'incliner. void FaceVisitor() { if (_head == null) return; var away = transform.position - _head.position; away.y = 0f; if (away.sqrMagnitude < 0.01f) return; transform.rotation = Quaternion.LookRotation(away.normalized); } Vector3 HeadForward() { var forward = _head.forward; forward.y = 0f; return forward.sqrMagnitude < 0.01f ? Vector3.forward : forward.normalized; } static string Wrap(string message, int columns) { var words = message.Split(' '); var line = 0; var wrapped = new System.Text.StringBuilder(); foreach (var word in words) { if (line + word.Length > columns) { wrapped.Append('\n'); line = 0; } else if (line > 0) { wrapped.Append(' '); line++; } wrapped.Append(word); line += word.Length; } return wrapped.ToString(); } } }