Source 2 | Benutzerdefiniertes HUD-Layout
Die Entität custom_hud_layout (C++ CCSCustomHudLayout) ermöglicht es einer Map oder einem serverseitigen Plugin,
Spielern ein HUD anzuzeigen, das in Panorama XML + VCSS erstellt wurde.
Lesen Sie dies zuerst: Ein benutzerdefiniertes HUD ist kein gewöhnliches Panorama-Dokument. Das geladene XML wird
durch einen speziellen Whitelist-Validator geprüft, der nur vier Panel-Typen und einen festen Attributsatz zulässt.
Es gibt kein style=, kein <script> und keine Event-Handler im Markup. Das gesamte dynamische Verhalten wird vom Server gesteuert.
Die vollständige Pipeline
custom_hud_layout (Server, keyvalue layout="...")
│ m_strLayout wird zum Client repliziert
▼
CSGOCustomHuds (Panorama-Fenster) → layout/hud/customhuds.xml
└─ <Panel class="WindowRoot" hittest="false">
└─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
└─ CSGOCustomHudLayoutRoot ← erstellt aus C++, eine pro Entität,
└─ IHR XML ohne id und ohne class
Rückweg (ein Klick): Benutzernachricht CS_UM_CustomHudClicked (390) → serverseitiges JS-Ereignis
OnCustomHudClicked mit { player, layout, buttonId }.
Zwei Arten von HUDs
Jedes benutzerdefinierte HUD ist eines von zwei Dingen, und der Unterschied ist ein einziger Aufruf pro Spieler:
| Overlay – der Standard | Cursor – SetInputCaptureEnabled(slot, true) | |
|---|---|---|
| Was es ist | Pixel auf dem Bildschirm | ein Bildschirm, den der Spieler nutzen kann |
| Maus | keine; das Fadenkreuz zielt weiterhin | ein Cursor erscheint über dem HUD |
| Klicks | nichts ist anklickbar | Klicks auf Button erreichen den Server |
:hover | trifft nie zu | funktioniert, und kostet den Server nichts |
| Anwendung | Timer, Anzeigetafeln, Kill Feeds, Banner | Shops, Abstimmungsdialoge, Auswahlen, Menüs |
Capture ist jedes Mal ausgeschaltet, wenn das Layout erstellt wird, also muss es nach dem Erscheinen der
Entität wieder eingeschaltet werden – siehe entity.md, Input capture. Innerhalb eines
captured HUDs entscheidet hittest="false", welche Panels der Cursor durchdringt; schreiben Sie es auf
alles und entfernen Sie es von den wenigen Containern, die die Maus abfangen müssen.
Was ein HUD tatsächlich enthalten kann
Das Markup-Vokabular umfasst vier Tags, aber was Sie durch sie hindurch einsetzen können, ist umfangreicher als es aussieht:
- SVG-Symbole (
.vsvg), die bei jeder UI-Skalierung scharf bleiben und zur Laufzeit mitwash-colorgetönt werden; - eigene Schriftarten, indem Sie
.ttf-Dateien in daspanorama/fonts/-Verzeichnis des Addons legen; - spielerspezifischer lokalisierter Text über
text="#Token", clientseitig in der jeweiligen Sprache des Spielers aufgelöst, unter Verwendung eigener Zeichenfolgen oder des gesamten Basisspiel-Katalogs; - die Grafik des Basisspiels, referenziert über den
s2r://-Pfad, ohne dass etwas zum Download hinzugefügt werden muss; - Animationen und Übergänge, einschließlich Ein-/Aus-Paaren, die durch Klassen-Toggles gesteuert werden;
- Video als
background-image-Ebene auf einem gewöhnlichenPanel.
Wenn Sie aus dem Web kommen
Panorama hat die Form von HTML/CSS, was sowohl hilfreich als auch eine Falle ist: Die Syntax wirkt vertraut, und dann tut eine Eigenschaft stillschweigend nichts. Das mentale Modell, das funktioniert, ist eine serverseitig gerenderte Seite ohne jegliches Client-Script, bei der die einzigen beiden Aktualisierungen, die es gibt, eine Klasse auf einem Panel umschalten und eine Zeichenkette auf einem Panel setzen sind. Alles andere – jede Variante, jeder Zustand, jede Verzweigung – wird vorab erstellt und nur angezeigt.
| Sie würden schreiben | Hier schreiben Sie |
|---|---|
<div> | <Panel> |
<span>, ein Textknoten | <Label text="..."> – Text lebt in einem Attribut, nicht zwischen Tags |
<img src> | <Image src>, oder ein background-image auf einem beliebigen Panel |
<button onclick> | <Button id> – der Klick geht an den Server, nicht an ein Script |
style="..." | nichts; class ist der einzige Styling-Hook |
display: none | visibility: collapse |
visibility: hidden | opacity: 0 – noch im Layout, noch klickbar |
position: absolute | x / y plus ignore-parent-flow: true |
| flexbox, grid | flow-children auf dem Eltern-Element, horizontal-align / vertical-align auf dem Kind-Element |
gap | margin auf den Kindern |
filter: blur(4px) | blur: gaussian( 4, 4, 1 ) |
rgba(0, 0, 0, 0.5) | #00000080 – Farben sind nur hexadezimal, Alpha ist das letzte Byte |
linear-gradient(...) in background-image | gradient( ... ) in background-**color** |
@font-face | legen Sie die .ttf in panorama/fonts/ ab; nichts zu deklarieren |
:hover | :hover – eines der wenigen Dinge, das genau wie erwartet funktioniert |
| ein JS-Event-Handler | das Server-Ereignis OnCustomHudClicked, gekoppelt an die id des Buttons |
data-* gelesen von JS | {s:name} Dialogvariablen, vom Server gesetzt |
eine Schleife oder <template> | beides existiert nicht – zählen Sie die Zeilen im Markup oder im Stylesheet auf |
calc(), benutzerdefinierte Eigenschaften | beides existiert nicht – zählen Sie die Werte im Stylesheet auf |
Zwei Gewohnheiten aus der Webentwicklung verursachen hier die meiste Zeitverschwendung. Nach einem berechneten Wert greifen:
es gibt kein calc(), keine Variablen, und der Server kann keine Zahl senden, also ist ein Fortschrittsbalken eine
Klasse pro Schritt und eine Koordinate ein Literal in einer Regel. Und nach Client-Verhalten greifen: eine
Dropdown-Liste, die sich selbst öffnet, eine Registerkartenleiste, die sich selbst umschaltet, ein Formular, das sich selbst validiert – nichts
davon kann ohne einen Roundtrip passieren. Gestalten Sie die Interaktion also so, dass ein Klick zum Server geht
und eine Klasse zurückkommt.
Was intakt bleibt, ist der Teil, der den Designern am wichtigsten ist: das Boxmodell, Ausrichtung, Farbe, Typografie, Schatten, abgerundete Ecken, Übergänge und Keyframe-Animationen verhalten sich dem Web so ähnlich, dass visuelle Arbeit direkt übertragbar ist. Siehe references/css.md für die vollständige Eigenschaftsliste und die vollständige Liste der nicht vorhandenen Funktionen.
Schnellstart
Ein gearbeitetes Beispiel: ein Capturpunkt-Indikator – ein schreibgeschützter Streifen, der am unteren Rand des Bildschirms mit einem radialen Fortschrittsring verankert ist, vollständig gesteuert durch Dialogvariablen und Klassen-Toggles. Er benötigt keine Input-Erfassung, da nichts darin anklickbar ist.
Die erstellten Quellen befinden sich unter panorama/layout/custom_game/ und panorama/styles/custom_game/
im Content-Root des Addons; der Compiler wandelt sie in .vxml_c / .vcss_c um:
panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css
1. Markup – panorama/layout/custom_game/capture_point.xml
<root>
<styles>
<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
</styles>
<!--
Das äußere Panel darf KEINE id tragen – der Zuweiser weist sie zu, und der Compiler
lehnt eine ab. Belassen Sie es als anonymen Vollbild-Wrapper und setzen Sie die eigentliche Wurzel hinein.
-->
<Panel class="cp-screen" hittest="false">
<!-- Startet versteckt: Die Entität zeigt dieses Layout JEDEM Spieler in dem Moment, in dem es erscheint, der erstellte Zustand muss also der leere sein. -->
<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">
<Panel class="cp-ring">
<Panel class="cp-ring-track" hittest="false" />
<!-- Der Server schreibt die Klasse von cp-fill in cp-fill-0 … cp-fill-100 um;
das Stylesheet wandelt jeden Schritt in einen clip: radial()-Bereich um. -->
<Panel id="CaptureFill" class="cp-ring-fill cp-fill-0" hittest="false" />
<Label id="CapturePct" class="cp-pct" text="{s:pct}" />
</Panel>
<Panel class="cp-copy">
<Label id="CaptureName" class="cp-name" text="{s:point_name}" />
<Label id="CaptureHint" class="cp-hint" text="{s:hint}" />
</Panel>
</Panel>
</Panel>
</root>
2. Styles – panorama/styles/custom_game/capture_point.css (es gibt überhaupt keine Inline-Styles)
/* Vollbild-Wrapper. noclip lässt das Leuchten des Rings über den Panelrand hinausragen, anstatt
abgeschnitten zu werden; der Wrapper selbst wird niemals einem Treffertest unterzogen. */
.cp-screen
{
width: 100%;
height: 100%;
overflow: noclip;
}
/* Der Streifen selbst, oberhalb der unteren Kante positioniert. Er beginnt unsichtbar und versetzt, und der
Server fügt "show" hinzu, um die Enthüllung abzuspielen – weshalb dies eine Alternative zu .hidden ist,
keine Ergänzung: Ein collapsed Panel ist aus dem Layout herausgenommen und hat nichts, von dem aus es animieren könnte. */
.cp-root
{
flow-children: right;
horizontal-align: center;
vertical-align: bottom;
margin-bottom: 96px;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.15s ease-out 0.0s, transform 0.15s ease-out 0.0s;
}
/* Der enthüllte Zustand. Beide Eigenschaften sind im obigen Übergang aufgeführt, so dass das Hinzufügen und
Entfernen der Klasse in jede Richtung animiert. */
.cp-root.show
{
opacity: 1;
transform: translateY(0px);
}
/* Entfernt ein Panel vollständig aus dem Layout, sodass sich benachbarte Elemente schließen, anstatt ein Loch zu hinterlassen.
Nichts anderes collapsed ein Panel – opacity: 0 reserviert den Platz. */
.hidden
{
visibility: collapse;
}
/* Rechteckige Box, in der die beiden Ringbilder übereinander gestapelt werden. Beide füllen sie aus, sodass sie genau übereinander liegen. */
.cp-ring
{
width: 56px;
height: 56px;
}
/* Der ungefüllte Ring darunter, abgedunkelt, damit die Füllung oben als Fortschritt lesbar ist. Bildmaterial kommt
aus einer kompilierten Textur über s2r:// – die Standardform. */
.cp-ring-track
{
width: 100%;
height: 100%;
background-image: url( "s2r://panorama/images/custom_game/ring_png.vtex" );
background-size: contain;
background-repeat: no-repeat;
opacity: 0.25;
}
/* Dasselbe Bildmaterial oben, durch wash-color neu eingefärbt und nach und nach durch die
.cp-fill-*-Regeln unten freigelegt. Eine graue Quelle zu tönen ist besser, als eine Textur pro Team auszuliefern. */
.cp-ring-fill
{
width: 100%;
height: 100%;
background-image: url( "s2r://panorama/images/custom_game/ring_png.vtex" );
background-size: contain;
background-repeat: no-repeat;
wash-color: #4caf6dff;
}
/* Eine Regel pro Schritt, den der Server auswählen kann, da er eine Klasse umschalten, aber keine Zahl senden kann.
clip ist layoutkostenfrei und animiert, daher sind mehr Schritte kostengünstig. */
.cp-fill-0
{
clip: radial( 50% 50%, 0deg, 0deg );
}
.cp-fill-25
{
clip: radial( 50% 50%, 0deg, 90deg );
}
.cp-fill-50
{
clip: radial( 50% 50%, 0deg, 180deg );
}
.cp-fill-75
{
clip: radial( 50% 50%, 0deg, 270deg );
}
.cp-fill-100
{
clip: radial( 50% 50%, 0deg, 360deg );
}
/* Textspalte rechts vom Ring, zentriert dazu. */
.cp-copy
{
flow-children: down;
vertical-align: center;
margin-left: 12px;
}
/* Punktname – die Zeile, die der Spieler zuerst liest. */
.cp-name
{
font-size: 20px;
font-weight: bold;
color: #ffffffff;
}
/* Ergänzungszeile, mit Alpha zurückgehalten anstatt mit einer zweiten Farbe. */
.cp-hint
{
font-size: 14px;
color: #ffffff99;
}
/* Team-Akzent. Der Server setzt team-ct oder team-t auf die Wurzel, und der Nachkommen-Selektor
übermalt die Füllung – Teamfarbe und Fortschritt bleiben also unabhängig voneinander. */
.cp-root.team-ct .cp-ring-fill
{
wash-color: #56a0ddff;
}
.cp-root.team-t .cp-ring-fill
{
wash-color: #f0a531ff;
}
3. Entität – in Hammer oder über CreateEntityByName
| Schlüsselwert | Wert |
|---|---|
layout | panorama/layout/custom_game/capture_point.vxml |
4. Vom Server aus steuern (serverseitiges JS unter cs_script) – siehe entity.md
const hud = Instance.FindEntityByName("capture_hud"); // JS-Wrapper-Klasse: CustomHudLayout
function showCapture(slot, name, pct, team) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", true);
hud.SetHasClassForPlayer(slot, "CaptureRoot", "team-" + team, true);
// die Schrittklasse wechseln: die alte löschen, die neue setzen
hud.SetHasClassForPlayer(slot, "CaptureFill", "cp-fill-" + roundTo25(pct), true);
hud.SetDialogVariableStringForPlayer(slot, "CapturePct", "pct", pct + "%");
hud.SetDialogVariableStringForPlayer(slot, "CaptureName", "point_name", name);
hud.SetDialogVariableStringForPlayer(slot, "CaptureHint", "hint", "Halte den Punkt");
}
function hideCapture(slot) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", false);
}
Da der Server nur Klassen umschalten und Zeichenketten setzen kann – er kann keine Zahl senden – muss ein kontinuierlicher Wert wie ein Fortschrittsring in eine Klasse pro Schritt quantisiert werden. Fünf Schritte sind für einen Captur-Ring ausreichend; ein gleichmäßigerer Balken braucht nur mehr Regeln.
Häufige Ablehnungen
| Was Sie wollen | Warum es fehlschlägt |
|---|---|
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, jedes andere Tag | nur Panel, Label, Image, Button – für Video verwenden Sie eine background-image-Ebene anstelle von <Movie> |
style="..." auf einem Element | kein Panel-Typ hat ein style-Attribut |
onactivate="..." oder ein anderer Markup-Event-Handler | keine; Klicks erreichen den Server, gekoppelt an id |
<scripts>, <snippets>, <snippet> | auf AST-Knotentyp-Ebene zurückgewiesen |
<include> einer anderen .xml | nur eine .vcss-Ressource kann referenziert werden |
dialogvariable="..." als Attribut | der Server setzt Variablen über SetDialogVariableString |
hittest auf <Button> | Button akzeptiert nur id und class |
Vollständiger erlaubter Satz: xml.md.
Fehlersuche
Der Validator protokolliert im custom_hud-Logging-Kanal auf Warnstufe:
| Nachricht | Ursache |
|---|---|
Layout xml is an invalid resource name "%s" | layout löst sich nicht in einen Ressourcennamen auf |
Failed to load layout '%s'. | Datei fehlt oder nicht kompiliert |
Layout contains disallowed node '%s' (type: %d). | <scripts>/<snippets>/<snippet> usw. |
Layout contains disallowed panel type '%s'. | Tag außerhalb der vier erlaubten |
Layout contains disallowed attribute %s for panel type '%s'. | Attribut nicht in der Liste dieses Tags |
Layout contains reference to disallowed resource type '%s'. | <include> zielt auf eine Nicht-.vcss-Ressource ab |
Layout xml did not pass CustomHud validation "%s" | endgültige Zurückweisung |
Layout is invalid. | ein Attributknoten, dessen Eltern-Element kein Panel ist |
Bei einem Validierungsfehler wird das Panel nie erstellt – ein leerer Bildschirm, kein teilweises Rendern. Das heiße Neuladen eines Live-Layouts in einen ungültigen Zustand zerstört das vorhandene HUD.
Referenzen
| Datei | Inhalt |
|---|---|
| xml.md | Vollständige Tag-/Attribut-Whitelist, AST-Knotentypen, Validatorregeln |
| css.md | Alle 140 Panorama CSS-Eigenschaften, Einheiten, Flow-/Align-Wertsätze, Vorbereitung … |
… [für die Übersetzung gekürzt]