Premier commit du cinquième front. Trois parties : - `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205), un APK unique pour tous les clients. Menu flottant à sélection au regard, appairage, chargement de scène GLB, POI, cache de contenu, télémétrie. - `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec les pièges rencontrés consignés dans son README. - `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript, three.js), partagé avec les autres fronts. - `docs/` : état des lieux, setup Unity, décisions d'architecture et plan d'exécution en 9 étapes. La scène est décrite par un `scene.json` poussé par `adb push` : l'app le préfère à celui embarqué dans l'APK. Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par Git LFS dès ce premier commit — les y faire entrer après coup demanderait de réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake (`Library/`, `.utmp/`, Burst debug) sont ignorés. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
118 lines
4.3 KiB
C#
118 lines
4.3 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.XR;
|
|
|
|
namespace MyInfoMate.Vr.Kiosk
|
|
{
|
|
/// <summary>
|
|
/// Le mode borne — item <b>E10</b> du lot XR-4.
|
|
///
|
|
/// <b>Ce qui sépare une démo d'une borne</b> : 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 <b>le menu, devant
|
|
/// lui, comme si l'app venait de démarrer</b> — 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 :
|
|
/// <list type="bullet">
|
|
/// <item><b>Le casque est reposé</b> — le signal le plus fiable qui existe : la
|
|
/// visite est finie, tout de suite, sans attendre.</item>
|
|
/// <item><b>Plus rien ne bouge</b> pendant un moment — le repli, pour le casque
|
|
/// posé sur une table sans que personne ne l'ait retiré de la tête.</item>
|
|
/// </list>
|
|
///
|
|
/// <b>Sans SDK Meta.</b> La présence du porteur se lit par <c>CommonUsages.userPresence</c>
|
|
/// d'Unity XR, alimenté par OpenXR : pas besoin d'un <c>OVRManager</c> dans la scène,
|
|
/// donc rien à configurer et rien à oublier de configurer.
|
|
/// </summary>
|
|
public class KioskSession : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[SerializeField] float idleSeconds = 90f;
|
|
|
|
/// <summary>Au-delà, la tête a bougé : quelqu'un est là.</summary>
|
|
const float MovementThresholdDegrees = 3f;
|
|
|
|
Transform _head;
|
|
Quaternion _lastRotation;
|
|
float _idle;
|
|
bool _wasWorn = true;
|
|
|
|
/// <summary>La visite est finie — revenir au menu et repartir de zéro.</summary>
|
|
public event Action Ended;
|
|
|
|
public static KioskSession Attach(GameObject host, Transform head)
|
|
{
|
|
var session = host.AddComponent<KioskSession>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// À 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
static bool IsWorn()
|
|
{
|
|
var device = InputDevices.GetDeviceAtXRNode(XRNode.Head);
|
|
if (!device.isValid) return true;
|
|
|
|
return !device.TryGetFeatureValue(CommonUsages.userPresence, out var present) || present;
|
|
}
|
|
}
|
|
}
|