Aller au contenu

FOS UI Culling

Version 1.0.1 · exige FOS Essentials Core 1.0.0 ou plus récent

Désactive les UI du monde quand le joueur local est hors de portée, depuis un unique manager central. Exige FOS Essentials Core 1.0.0.


  1. Créez un GameObject vide et ajoutez FOS Essentials > UI Culling > UI Culling Manager.
  2. Cliquez sur Scan the scene. Chaque canvas World Space reçoit un FOSUICullingTarget, son Canvas et son GraphicRaycaster sont câblés, et la liste du manager est remplie.
  3. Réglez Default distance. Surchargez-la UI par UI dans le composant cible, au besoin.

Relancez le scan après avoir ajouté des UI. Les cibles existantes ne sont jamais écrasées : tout réglage manuel survit.


Composant Rôle
FOSUICullingManager Un par scène. Possède la boucle et pilote tout.
FOSUICullingTarget Un par UI. Ne porte que des réglages — aucun travail par frame.

Option Effet
Default distance Mètres en deçà desquels une UI est affichée, sauf surcharge par la cible.
Show distance (cible) Surcharge par UI. -1 signifie « hériter de la valeur du manager ».
Distance reference (cible) Point depuis lequel la distance est mesurée. Vide signifie le Canvas lui-même.
Hysteresis Mètres supplémentaires avant de masquer à nouveau. Empêche une UI de clignoter quand le joueur se tient sur la limite.
Checks per frame Nombre d’UI testées à chaque image. 4 par défaut.
Line of sight Passe d’occlusion optionnelle. Désactivée par défaut.
Obstacle layers Layers considérés comme bloquants pour la passe d’occlusion.

Deux niveaux, résolus une fois au démarrage du manager :

  1. La Default distance du manager s’applique à toutes les UI.
  2. La Show distance d’une cible la surcharge — sauf si elle vaut -1, ce qui signifie hériter de la valeur du manager.
float distance = target.showDistance;
if (distance < 0f) distance = defaultShowDistance;

Une scène avec Default distance = 15 et une cible réglée sur 8 affiche donc cette UI à 8 mètres et toutes les autres à 15.

L’inspecteur de la cible énonce le résultat — « Effective distance: 15 m (inherited from the manager) » — pour que vous n’ayez jamais à vous rappeler ce que veut dire -1.

Comme la valeur est lue une seule fois au Start, changer une distance en cours d’exécution demande d’appeler RefreshTargets() sur le manager.

Depuis le transform du Canvas, c’est-à-dire, pour un panneau, son pivot, en général son centre. Un panneau large apparaît donc en fonction de son centre plutôt que de son bord le plus proche.

Quand cela compte, glissez n’importe quel Transform dans Distance reference sur la cible — un enfant vide placé au bord, par exemple — et la mesure se fait depuis lui.


Pourquoi un manager central plutôt qu’un script par UI

Section intitulée « Pourquoi un manager central plutôt qu’un script par UI »

Udon est une machine virtuelle interprétée, environ cent fois plus lente que le C#. Le coût dominant est le nombre de comportements qui tournent, pas l’arithmétique qu’ils font. Un comportement comparant trente distances au carré coûte bien moins cher que trente comportements faisant quoi que ce soit.

FOSUICullingTarget ne porte donc que de la configuration. Le manager la lit une fois au Start, la met en cache dans des tableaux plats, puis ne touche plus que directement aux composants Unity. Trente UI coûtent exactement un comportement actif.

Un rayon répond à « quelque chose fait-il obstacle », pas à « suis-je proche ». Il ne peut pas remplacer le test de distance — il en faudrait un d’abord de toute façon. Le raycast a un vrai usage ici, mais en seconde étape : voir Line of sight plus bas.

Pourquoi la distance et pas des colliders de trigger

Section intitulée « Pourquoi la distance et pas des colliders de trigger »

Les volumes de trigger sont réellement bon marché — événementiels, avec la phase large gérée en code natif. Ils perdent sur trois points pratiques :

  • un collider doit être dimensionné pour chaque UI, ce qui ruine la configuration automatique ;
  • OnPlayerTriggerEnter ne se déclenche que sur le GameObject qui porte le collider, ce qui impose un comportement par UI pour le recevoir ;
  • la capsule du joueur doit réellement chevaucher le volume. Une UI haut sur un mur avec un petit trigger est manquée, et une erreur de matrice de layers échoue en silence.

Le Canvas, le raycaster et les colliders — et non SetActive(false).

  • Désactiver le Canvas arrête le rendu immédiatement tout en gardant la hiérarchie vivante.
  • Désactiver les colliders est ce qui arrête le pointeur de VRChat. Celui-là n’est pas optionnel : voir plus bas.
  • Désactiver le GraphicRaycaster sort l’UI de la résolution des événements d’interface d’Unity.
  • SetActive(false) forcerait une reconstruction complète de la mise en page au retour et relancerait tous les OnEnable, Udon compris, ce qui peut réinitialiser l’état de scripts sans rapport.

Pourquoi les colliders comptent plus que le raycaster

Section intitulée « Pourquoi les colliders comptent plus que le raycaster »

Le pointeur de VRChat ne passe pas par le GraphicRaycaster. Son propre raycaster effectue un raycast physique, puis cherche un VRC_UiShape sur le collider touché :

int hitCount = Physics.RaycastNonAlloc(...); // touche des COLLIDERS
VRC_UiShape shape = hitObject.GetComponent<VRC_UiShape>();

Désactiver le seul Canvas masque donc l’UI en laissant son collider en place. Le pointeur continue de s’y accrocher, et ses boutons restent cliquables — une UI invisible qui réagit encore. C’est un bug fonctionnel, pas cosmétique.

Le scan remplit automatiquement la liste Colliders depuis le GameObject du canvas. Si une UI a des colliders ailleurs, ajoutez-les à la main ; l’inspecteur de la cible avertit quand il trouve des colliders non listés.

Collider ne dérive pas de Behaviour — il dérive directement de Component, avec sa propre propriété enabled. C’est pourquoi les colliders ont leur propre liste plutôt que de figurer dans Extra components.

Une fois activée, une UI déjà à portée est masquée si un obstacle bouche la vue. Un Physics.Linecast par UI vérifiée, lancé depuis la tête du joueur — depuis les pieds, il raserait le sol et accrocherait la moindre bosse.

Elle ne s’exécute que pour les UI ayant déjà passé le test de distance : une UI à deux cents mètres ne coûte donc jamais un rayon. Laissez-la tout de même désactivée sauf si le monde en a réellement besoin.

Les cibles sont vérifiées à tour de rôle, Checks per frame à la fois. Avec 4 vérifications et 40 UI, la latence dans le pire des cas est de 10 images — moins d’un cinquième de seconde, invisible pour une UI qui apparaît au loin.

La première image fait exception : tout est évalué d’un coup, sans quoi les UI lointaines resteraient allumées jusqu’à ce que le curseur ait bouclé un tour complet.

La visibilité n’est écrite que lorsqu’elle change : une scène stable n’effectue donc aucune écriture de composant.


  • Un manager par scène. Plusieurs managers se disputeraient les mêmes canvas ; l’inspecteur le signale comme une erreur.
  • Les canvas imbriqués sont ignorés. Désactiver le canvas racine empêche déjà ses enfants de s’afficher : gérer une cible par niveau ne ferait qu’encombrer la liste.
  • Les canvas Screen Space sont ignorés par défaut : ils ne sont affichés à personne dans un monde VRChat.
  • Les UI créées à l’exécution ne sont pas prises en compte automatiquement. Remplissez targets et appelez RefreshTargets().
  • ShowAll() rallume tout pour du débogage, sans retirer le manager.