Ir al contenido

FOS UI Culling

Versión 1.0.1 · requiere FOS Essentials Core 1.0.0 o posterior

Desactiva las UI del mundo cuando el jugador local queda fuera de alcance, desde un único gestor central. Requiere FOS Essentials Core 1.0.0.


  1. Crea un GameObject vacío y añade FOS Essentials > UI Culling > UI Culling Manager.
  2. Pulsa Scan the scene. Cada canvas World Space recibe un FOSUICullingTarget, se conectan su Canvas y su GraphicRaycaster, y se rellena la lista del gestor.
  3. Ajusta Default distance. Sobrescríbela por UI en el componente objetivo cuando haga falta.

Vuelve a lanzar el escaneo tras añadir nuevas UI. Los objetivos existentes nunca se sobrescriben: cualquier ajuste manual sobrevive.


Componente Función
FOSUICullingManager Uno por escena. Posee el bucle y lo gobierna todo.
FOSUICullingTarget Uno por UI. Solo guarda ajustes — ningún trabajo por fotograma.

Opción Efecto
Default distance Metros por debajo de los cuales se muestra una UI, salvo que el objetivo lo sobrescriba.
Show distance (objetivo) Sobrescritura por UI. -1 significa «heredar el valor del gestor».
Distance reference (objetivo) Punto desde el que se mide la distancia. Vacío significa el propio Canvas.
Hysteresis Metros adicionales antes de volver a ocultar. Evita que una UI parpadee cuando el jugador está justo en el límite.
Checks per frame Cuántas UI se comprueban en cada fotograma. 4 por defecto.
Line of sight Pasada de oclusión opcional. Desactivada por defecto.
Obstacle layers Capas que cuentan como obstáculo para la pasada de oclusión.

Dos niveles, resueltos una sola vez al arrancar el gestor:

  1. La Default distance del gestor se aplica a todas las UI.
  2. La Show distance de un objetivo la sobrescribe, salvo que valga -1, que significa heredar el valor del gestor.
float distance = target.showDistance;
if (distance < 0f) distance = defaultShowDistance;

Así, una escena con Default distance = 15 y un objetivo puesto a 8 muestra esa UI a 8 metros y todo lo demás a 15.

El inspector del objetivo deletrea el resultado — «Effective distance: 15 m (inherited from the manager)» — para que nunca tengas que recordar qué significa -1.

Como el valor se lee una sola vez en Start, cambiar una distancia en ejecución exige llamar a RefreshTargets() en el gestor.

Desde el transform del Canvas, que en un panel es su pivote, normalmente su centro. Un panel ancho aparece por tanto en función de su centro y no de su borde más cercano.

Cuando eso importa, arrastra cualquier Transform a Distance reference en el objetivo — un hijo vacío colocado en el borde, por ejemplo — y la medición se hará desde ahí.


Por qué un gestor central en lugar de un script por UI

Sección titulada «Por qué un gestor central en lugar de un script por UI»

Udon es una máquina virtual interpretada, unas cien veces más lenta que C#. El coste dominante es el número de comportamientos en marcha, no la aritmética que hacen. Un comportamiento comparando treinta distancias al cuadrado sale mucho más barato que treinta comportamientos haciendo cualquier cosa.

Por eso FOSUICullingTarget solo guarda configuración. El gestor la lee una vez en Start, la cachea en arrays planos, y a partir de ahí solo toca componentes de Unity directamente. Treinta UI cuestan exactamente un comportamiento activo.

Un rayo responde a «¿hay algo en medio?», no a «¿estoy cerca?». No puede sustituir a la prueba de distancia: harían falta las dos, y la distancia primero. El raycast sí tiene un uso real aquí, pero como segunda etapa: mira Line of sight más abajo.

Por qué distancia y no colliders de trigger

Sección titulada «Por qué distancia y no colliders de trigger»

Los volúmenes de trigger son de verdad baratos: van por eventos y la fase amplia se resuelve en código nativo. Pierden en tres puntos prácticos:

  • hay que dimensionar un collider por UI, lo que arruina la configuración automática;
  • OnPlayerTriggerEnter solo se dispara en el GameObject que lleva el collider, lo que obliga a un comportamiento por UI para recibirlo;
  • la cápsula del jugador tiene que solaparse de verdad con el volumen. Una UI alta en una pared con un trigger pequeño se pierde, y un error en la matriz de capas falla en silencio.

El Canvas, el raycaster y los colliders — no SetActive(false).

  • Desactivar el Canvas detiene el renderizado al instante y mantiene viva la jerarquía.
  • Desactivar los colliders es lo que detiene el puntero de VRChat. Este no es opcional: mira abajo.
  • Desactivar el GraphicRaycaster saca la UI de la resolución de eventos de interfaz de Unity.
  • SetActive(false) forzaría una reconstrucción completa del layout al volver y reejecutaría todos los OnEnable, Udon incluido, lo que puede reiniciar el estado de scripts sin relación.

Por qué los colliders importan más que el raycaster

Sección titulada «Por qué los colliders importan más que el raycaster»

El puntero de VRChat no pasa por el GraphicRaycaster. Su propio raycaster hace un raycast físico y luego busca un VRC_UiShape en el collider que haya tocado:

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

Desactivar solo el Canvas oculta la UI pero deja su collider en pie. El puntero sigue enganchándose a él y sus botones siguen siendo pulsables: una UI invisible que aún reacciona. Eso es un fallo funcional, no cosmético.

El escaneo rellena automáticamente la lista Colliders a partir del GameObject del canvas. Si una UI tiene colliders en otro sitio, añádelos a mano; el inspector del objetivo avisa cuando encuentra colliders sin listar.

Collider no deriva de Behaviour: deriva directamente de Component, con su propia propiedad enabled. Por eso los colliders tienen su propia lista en vez de figurar en Extra components.

Al activarla, una UI que ya está dentro de alcance se oculta si un obstáculo tapa la vista. Un Physics.Linecast por cada UI comprobada, lanzado desde la cabeza del jugador: desde los pies rozaría el suelo y engancharía cualquier bulto.

Solo se ejecuta para las UI que ya han pasado la prueba de distancia, así que una UI a doscientos metros nunca cuesta un rayo. Aun así, déjala desactivada salvo que el mundo lo necesite de verdad.

Los objetivos se comprueban por turnos, Checks per frame cada vez. Con 4 comprobaciones y 40 UI, la latencia en el peor caso es de 10 fotogramas: menos de una quinta parte de segundo, invisible para una UI que aparece a lo lejos.

El primer fotograma es la excepción: se evalúa todo de golpe, o las UI lejanas seguirían encendidas hasta que el cursor completara una vuelta entera.

La visibilidad solo se escribe cuando cambia, así que una escena estable no realiza ninguna escritura de componente.


  • Un gestor por escena. Varios gestores se pelearían por los mismos canvas; el inspector lo señala como error.
  • Los canvas anidados se omiten. Desactivar el canvas raíz ya impide que sus hijos se dibujen, así que gestionar un objetivo por nivel solo abarrotaría la lista.
  • Los canvas Screen Space se omiten por defecto: no se le muestran a nadie en un mundo de VRChat.
  • Las UI creadas en ejecución no se recogen automáticamente. Rellena targets y llama a RefreshTargets().
  • ShowAll() lo vuelve a encender todo para depurar, sin quitar el gestor.