Zum Inhalt springen

FOS UI Culling

Version 1.0.1 · benötigt FOS Essentials Core 1.0.0 oder neuer

Deaktiviert Welt-UIs, wenn die lokale Person außer Reichweite ist, gesteuert von einem einzigen zentralen Manager. Benötigt FOS Essentials Core 1.0.0.


  1. Erstelle ein leeres GameObject und füge FOS Essentials > UI Culling > UI Culling Manager hinzu.
  2. Klicke auf Scan the scene. Jeder World-Space-Canvas erhält ein FOSUICullingTarget, sein Canvas und sein GraphicRaycaster werden verdrahtet, und die Liste des Managers wird gefüllt.
  3. Stelle Default distance ein. Überschreibe sie bei Bedarf je UI in der Ziel-Komponente.

Führe den Scan nach dem Hinzufügen neuer UIs erneut aus. Bestehende Ziele werden nie überschrieben, jede manuelle Feinabstimmung bleibt erhalten.


Komponente Aufgabe
FOSUICullingManager Einer je Szene. Besitzt die Schleife und steuert alles.
FOSUICullingTarget Eines je UI. Hält nur Einstellungen — keine Arbeit pro Frame.

Option Wirkung
Default distance Meter, unterhalb derer eine UI gezeigt wird, sofern das Ziel es nicht überschreibt.
Show distance (Ziel) Überschreibung je UI. -1 bedeutet „den Wert des Managers erben”.
Distance reference (Ziel) Punkt, von dem aus gemessen wird. Leer bedeutet der Canvas selbst.
Hysteresis Zusätzliche Meter, bevor wieder verborgen wird. Verhindert Flackern, wenn jemand genau auf der Grenze steht.
Checks per frame Wie viele UIs pro Frame geprüft werden. Standard 4.
Line of sight Optionaler Verdeckungsdurchlauf. Standardmäßig aus.
Obstacle layers Layer, die beim Verdeckungsdurchlauf als Hindernis gelten.

Zwei Ebenen, einmalig beim Start des Managers aufgelöst:

  1. Die Default distance des Managers gilt für jede UI.
  2. Die Show distance eines Ziels überschreibt sie — außer sie bleibt auf -1, was den Wert des Managers erben bedeutet.
float distance = target.showDistance;
if (distance < 0f) distance = defaultShowDistance;

Eine Szene mit Default distance = 15 und einem Ziel auf 8 zeigt diese eine UI also bei 8 Metern und alles andere bei 15.

Der Inspektor des Ziels schreibt das Ergebnis aus — „Effective distance: 15 m (inherited from the manager)“ — damit du dir nie merken musst, was -1 bedeutet.

Da der Wert einmalig im Start gelesen wird, erfordert eine Änderung zur Laufzeit einen Aufruf von RefreshTargets() am Manager.

Vom Transform des Canvas, bei einem Panel also von seinem Pivot, meist der Mitte. Ein breites Panel erscheint daher nach seiner Mitte und nicht nach seiner nächsten Kante.

Wenn das zählt, ziehe irgendein Transform in Distance reference am Ziel — etwa ein leeres Kind an der Kante — und die Messung nutzt stattdessen dieses.


Warum ein zentraler Manager statt eines Skripts je UI

Abschnitt betitelt „Warum ein zentraler Manager statt eines Skripts je UI“

Udon ist eine interpretierte VM, rund hundertmal langsamer als C#. Die beherrschenden Kosten sind die Anzahl tickender Verhalten, nicht die Rechnerei darin. Ein Verhalten, das dreißig quadrierte Entfernungen vergleicht, ist weit billiger als dreißig Verhalten, die überhaupt irgendetwas tun.

FOSUICullingTarget hält deshalb nur Konfiguration. Der Manager liest sie einmal im Start, legt sie in flachen Arrays ab und rührt danach nur noch Unity-Komponenten direkt an. Dreißig UIs kosten genau ein aktives Verhalten.

Ein Strahl beantwortet „steht etwas im Weg”, nicht „bin ich nah”. Er kann den Entfernungstest nicht ersetzen — man bräuchte ihn ohnehin zuerst. Raycasting hat hier einen echten Nutzen, aber als zweite Stufe: siehe Line of sight unten.

Trigger-Volumen sind wirklich günstig: ereignisgesteuert, die Broadphase läuft in nativem Code. Sie verlieren an drei praktischen Punkten:

  • ein Collider muss je UI dimensioniert werden, was die automatische Einrichtung zunichtemacht;
  • OnPlayerTriggerEnter feuert nur am GameObject mit dem Collider, was ein Verhalten je UI erzwingt, um es zu empfangen;
  • die Spielerkapsel muss das Volumen tatsächlich überlappen. Eine UI hoch an der Wand mit kleinem Trigger wird verfehlt, und ein Fehler in der Layer-Matrix scheitert lautlos.

Der Canvas, der Raycaster und die Collider — nicht SetActive(false).

  • Den Canvas zu deaktivieren stoppt das Rendern sofort und hält die Hierarchie am Leben.
  • Die Collider zu deaktivieren ist das, was den Zeiger von VRChat stoppt. Dieser Schritt ist nicht optional: siehe unten.
  • Den GraphicRaycaster zu deaktivieren nimmt die UI aus Unitys UI-Ereignisauflösung.
  • SetActive(false) würde beim Zurückschalten einen vollständigen Layout-Neuaufbau erzwingen und jedes OnEnable erneut ausführen, Udon eingeschlossen, was den Zustand unbeteiligter Skripte zurücksetzen kann.

Warum die Collider wichtiger sind als der Raycaster

Abschnitt betitelt „Warum die Collider wichtiger sind als der Raycaster“

Der Zeiger von VRChat läuft nicht über den GraphicRaycaster. Sein eigener Raycaster macht einen Physik-Raycast und sucht dann ein VRC_UiShape an dem getroffenen Collider:

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

Nur den Canvas zu deaktivieren verbirgt die UI also, lässt aber ihren Collider stehen. Der Zeiger rastet weiterhin darauf ein, und ihre Schaltflächen bleiben anklickbar — eine unsichtbare UI, die noch reagiert. Das ist ein funktionaler Fehler, kein kosmetischer.

Der Scan füllt die Liste Colliders automatisch aus dem GameObject des Canvas. Hat eine UI Collider anderswo, trage sie von Hand nach; der Inspektor des Ziels warnt, wenn er nicht gelistete Collider findet.

Collider leitet nicht von Behaviour ab — er leitet direkt von Component ab, mit eigener enabled-Eigenschaft. Deshalb haben Collider eine eigene Liste, statt unter Extra components zu stehen.

Ist sie aktiv, wird eine bereits in Reichweite befindliche UI verborgen, wenn ein Hindernis die Sicht versperrt. Ein Physics.Linecast je geprüfter UI, vom Kopf aus geworfen — von den Füßen aus würde er den Boden streifen und jede Unebenheit erwischen.

Er läuft nur für UIs, die den Entfernungstest bereits bestanden haben; eine UI in zweihundert Metern kostet also nie einen Strahl. Lass ihn dennoch aus, sofern die Welt ihn nicht wirklich braucht.

Ziele werden reihum geprüft, Checks per frame auf einmal. Bei 4 Prüfungen und 40 UIs beträgt die Latenz im schlimmsten Fall 10 Frames — weniger als eine Fünftelsekunde, unsichtbar für eine UI, die in der Ferne auftaucht.

Der erste Frame ist die Ausnahme: Dort wird alles auf einmal ausgewertet, sonst blieben entfernte UIs beleuchtet, bis der Zeiger eine volle Runde gedreht hat.

Sichtbarkeit wird nur geschrieben, wenn sie sich ändert; eine stabile Szene führt also null Komponentenschreibvorgänge aus.


  • Ein Manager je Szene. Mehrere Manager würden sich um dieselben Canvas streiten; der Inspektor meldet das als Fehler.
  • Verschachtelte Canvas werden übersprungen. Den Wurzel-Canvas zu deaktivieren hält seine Kinder ohnehin vom Zeichnen ab; ein Ziel je Ebene würde nur die Liste zumüllen.
  • Screen-Space-Canvas werden übersprungen, standardmäßig: Sie werden in einer VRChat-Welt niemandem angezeigt.
  • Zur Laufzeit erzeugte UIs werden nicht automatisch erfasst. Fülle targets und rufe RefreshTargets() auf.
  • ShowAll() schaltet zum Debuggen alles wieder ein, ohne den Manager zu entfernen.