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();
}
}
}