Source 2 | Disposition HUD personnalisée (Custom Hud Layout)
L'entité custom_hud_layout (C++ CCSCustomHudLayout) permet à une carte ou à un plugin serveur
d'afficher aux joueurs un HUD créé en Panorama XML + VCSS.
À lire en premier : un HUD personnalisé n'est pas un document Panorama ordinaire. Le XML chargé est
traité par un validateur dédié basé sur une liste blanche qui n'autorise que quatre types de panneaux et un ensemble
d'attributs fixes. Il n'y a pas de style=, pas de <script>, et pas de gestionnaires d'événements dans le balisage.
Tout comportement dynamique est géré depuis le serveur.
Le pipeline complet
custom_hud_layout (serveur, clé-valeur layout="...")
│ m_strLayout est répliqué vers le client
▼
CSGOCustomHuds (fenêtre Panorama) → layout/hud/customhuds.xml
└─ <Panel class="WindowRoot" hittest="false">
└─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
└─ CSGOCustomHudLayoutRoot ← créé depuis le C++, un par entité,
└─ VOTRE XML sans id et sans class
Chemin de retour (un clic) : message utilisateur CS_UM_CustomHudClicked (390) → événement JS côté serveur
OnCustomHudClicked transportant { player, layout, buttonId }.
Deux types de HUD
Chaque HUD personnalisé est l'une des deux choses suivantes, et la différence réside dans un seul appel par joueur :
| Superposition (Overlay) — la valeur par défaut | Curseur (Cursor) — SetInputCaptureEnabled(slot, true) | |
|---|---|---|
| Ce que c'est | des pixels sur l'écran | un écran que le joueur peut utiliser |
| Souris | aucune ; le viseur continue de viser | un curseur apparaît sur le HUD |
| Clics | rien n'est cliquable | Les clics sur Button atteignent le serveur |
:hover | ne s'applique jamais | fonctionne, et ne coûte rien au serveur |
| Construction | minuteurs, tableaux de scores, kill feeds, bannières | magasins, boîtes de vote, sélecteurs, menus |
La capture est désactivée à chaque construction de la disposition, elle doit donc être réactivée après la
dissimulation de l'entité — voir entity.md, Capture d'entrée. Dans un
HUD capturé, hittest="false" décide par quels panneaux le curseur passe ; appliquez-le sur
tout et retirez-le des quelques conteneurs qui doivent attraper la souris.
Ce qu'un HUD peut réellement contenir
Le vocabulaire de balisage est de quatre balises, mais ce que vous pouvez y mettre à travers elles est plus large qu'il n'y paraît :
- Icônes SVG (
.vsvg) qui restent nettes à n'importe quelle échelle d'interface, colorées dynamiquement avecwash-color; - vos propres polices, en déposant des fichiers
.ttfdans le dossierpanorama/fonts/du addon ; - texte localisé par joueur, via
text="#Token"résolu côté client dans la langue de chaque joueur, en utilisant vos propres chaînes ou tout le catalogue du jeu de base ; - les œuvres du jeu de base, référencées par un chemin
s2r://sans rien ajouter à votre téléchargement ; - animations et transitions, y compris les paires d'entrée/sortie pilotées par des bascules de classe ;
- vidéo, comme une couche
background-imagesur unPanelordinaire.
Si vous venez du web
Panorama a la forme HTML/CSS, ce qui est à la fois une aide et un piège : la syntaxe semble familière, puis une propriété ne fait silencieusement rien. Le modèle mental qui fonctionne est une page rendue par le serveur sans aucun script client, où les deux seules mises à jour qui existent sont basculer une classe sur un panneau et définir une chaîne sur un panneau. Tout le reste — chaque variante, chaque état, chaque branche — est créé à l'avance et simplement révélé.
| Vous écririez | Ici vous écrivez |
|---|---|
<div> | <Panel> |
<span>, un nœud de texte | <Label text="..."> — le texte vit dans un attribut, pas entre les balises |
<img src> | <Image src>, ou un background-image sur n'importe quel panneau |
<button onclick> | <Button id> — le clic va au serveur, pas à un script |
style="..." | rien ; class est le seul crochet de style |
display: none | visibility: collapse |
visibility: hidden | opacity: 0 — toujours dans la disposition, toujours cliquable |
position: absolute | x / y plus ignore-parent-flow: true |
| flexbox, grille | flow-children sur le parent, horizontal-align / vertical-align sur l'enfant |
gap | margin sur les enfants |
filter: blur(4px) | blur: gaussian( 4, 4, 1 ) |
rgba(0, 0, 0, 0.5) | #00000080 — les couleurs sont en hexadécimal uniquement, l'alpha est le dernier octet |
linear-gradient(...) dans background-image | gradient( ... ) dans background-**color** |
@font-face | déposez le .ttf dans panorama/fonts/ ; rien à déclarer |
:hover | :hover — l'une des rares choses qui fonctionne exactement comme prévu |
| un gestionnaire d'événement JS | l'événement serveur OnCustomHudClicked, indexé par l'id du bouton |
data-* lu par JS | variables de dialogue {s:name}, définies depuis le serveur |
une boucle ou <template> | ni l'un ni l'autre n'existe — énumérez les lignes dans le balisage ou la feuille de style |
calc(), propriétés personnalisées | ni l'un ni l'autre n'existe — énumérez les valeurs dans la feuille de style |
Deux habitudes du travail sur le web causent la plupart du temps perdu ici. Recourir à une valeur calculée :
il n'y a pas de calc(), pas de variables, et le serveur ne peut pas envoyer un nombre, donc une barre de progression est une
classe par étape et une coordonnée est une valeur littérale dans une règle. Et recourir à un comportement client : une
liste déroulante qui s'ouvre toute seule, un onglet qui se change tout seul, un formulaire qui se valide tout seul — rien de tout
cela ne peut se produire sans un aller-retour, donc concevez l'interaction autour d'un clic allant au serveur et d'une classe
revenant.
Ce qui survit intact est la partie que les concepteurs apprécient le plus : le modèle de boîte, l'alignement, la couleur, la typographie, les ombres, les coins arrondis, les transitions et les animations d'images clés se comportent tous suffisamment proche du web pour que le travail visuel soit transféré directement. Voir references/css.md pour l'ensemble complet des propriétés et la liste complète de ce qui est absent.
Démarrage rapide
Un exemple concret : un indicateur de point de capture — une bande en lecture seule ancrée en bas de l'écran avec un anneau de progression radial, piloté entièrement par des variables de dialogue et des bascules de classe. Il n'a pas besoin de capture d'entrée, car rien dedans n'est cliquable.
Les sources créées se trouvent sous panorama/layout/custom_game/ et panorama/styles/custom_game/
dans la racine de contenu du addon ; le compilateur les transforme en .vxml_c / .vcss_c :
panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css
1. Balisage — panorama/layout/custom_game/capture_point.xml
<root>
<styles>
<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
</styles>
<!--
Le panneau extérieur NE DOIT PAS porter d'id — le chargeur l'attribue, et le compilateur
le rejette. Gardez-le comme un wrapper anonyme plein écran et mettez la vraie racine à l'intérieur.
-->
<Panel class="cp-screen" hittest="false">
<!-- Commence caché : l'entité montre cette disposition à CHAQUE joueur dès qu'elle apparaît, donc l'état créé doit être l'état vide. -->
<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">
<Panel class="cp-ring">
<Panel class="cp-ring-track" hittest="false" />
<!-- Le serveur réécrit la classe de cp-fill en cp-fill-0 … cp-fill-100 ;
la feuille de style transforme chaque étape en un clip : radial() sweep. -->
<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 (il n'y a aucun style en ligne)
/* Wrapper plein écran. noclip permet à la lueur de l'anneau de dépasser du bord du panneau au lieu d'être
coupée ; le wrapper lui-même n'est jamais testé pour les clics. */
.cp-screen
{
width: 100%;
height: 100%;noverflow: noclip;
}
/* La bande elle-même, positionnée au-dessus du bord inférieur. Elle commence invisible et décalée, et le
serveur ajoute "show" pour jouer la révélation — c'est pourquoi c'est une alternative à .hidden,
pas un ajout : un panneau réduit est hors disposition et n'a rien à animer à partir de. */
.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;
}
/* L'état révélé. Les deux propriétés sont listées dans la transition ci-dessus, donc l'ajout et le
retrait de la classe animent dans chaque direction. */
.cp-root.show
{
opacity: 1;
transform: translateY(0px);
}
/* Supprime un panneau complètement de la disposition, de sorte que les voisins se resserrent plutôt que de laisser un trou.
Rien d'autre ne réduit un panneau — opacity: 0 garde l'espace réservé. */
.hidden
{
visibility: collapse;
}
/* Boîte carrée dans laquelle les deux images de l'anneau s'empilent. Toutes deux la remplissent, donc elles s'alignent exactement. */
.cp-ring
{
width: 56px;
height: 56px;
}
/* L'anneau non rempli en dessous, atténué pour que le remplissage au dessus se lise comme une progression. L'œuvre
provient d'une texture compilée via s2r:// — le formulaire standard. */
.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;
}
/* La même œuvre au dessus, recoloriée par wash-color et révélée secteur par secteur par les
règles .cp-fill-* ci-dessous. Teinter une source grise est mieux que de livrer une texture par équipe. */
.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;
}
/* Une règle par étape que le serveur peut sélectionner, car il peut basculer une classe mais ne peut pas envoyer un
nombre. clip est gratuit en termes de disposition et s'anime, donc plus d'étapes sont peu coûteuses. */
.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 );
}
/* Colonne de texte à droite de l'anneau, centrée par rapport à lui. */
.cp-copy
{
flow-children: down;
vertical-align: center;
margin-left: 12px;
}
/* Nom du point — la ligne que le joueur lit en premier. */
.cp-name
{
font-size: 20px;
font-weight: bold;
color: #ffffffff;
}
/* Ligne de soutien, atténuée avec alpha plutôt qu'avec une seconde couleur. */
.cp-hint
{
font-size: 14px;
color: #ffffff99;
}
/* Accent d'équipe. Le serveur définit team-ct ou team-t sur la racine, et le sélecteur de descendant
repeint le remplissage — ainsi la couleur d'équipe et la progression restent indépendantes l'une de l'autre. */
.cp-root.team-ct .cp-ring-fill
{
wash-color: #56a0ddff;
}
.cp-root.team-t .cp-ring-fill
{
wash-color: #f0a531ff;
}
3. Entité — dans Hammer ou via CreateEntityByName
| Clé-valeur | Valeur |
|---|---|
layout | panorama/layout/custom_game/capture_point.vxml |
4. Piloter depuis le serveur (JS côté serveur sous cs_script) — voir entity.md
const hud = Instance.FindEntityByName("capture_hud"); // Classe wrapper JS : CustomHudLayout
function showCapture(slot, name, pct, team) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", true);
hud.SetHasClassForPlayer(slot, "CaptureRoot", "team-" + team, true);
// échange la classe d'étape : efface l'ancienne, définit la nouvelle
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", "Hold the point");
}
function hideCapture(slot) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", false);
}
Parce que le serveur ne peut que basculer des classes et définir des chaînes — il ne peut pas envoyer un nombre — une valeur continue comme un anneau de progression doit être quantifiée en une classe par étape. Cinq étapes sont suffisantes pour un anneau de capture ; une barre plus lisse a simplement besoin de plus de règles.
Rejets courants
| Ce que vous voulez | Pourquoi ça échoue |
|---|---|
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, toute autre balise | seuls Panel, Label, Image, Button — pour la vidéo, utilisez une couche background-image au lieu de <Movie> |
style="..." sur un élément | aucun type de panneau n'a d'attribut style |
onactivate="..." ou tout gestionnaire d'événement dans le balisage | aucun ; les clics atteignent le serveur indexés par id |
<scripts>, <snippets>, <snippet> | rejeté au niveau du type de nœud AST |
<include> d'un autre .xml | seule une ressource .vcss peut être référencée |
dialogvariable="..." en tant qu'attribut | le serveur définit les variables via SetDialogVariableString |
hittest sur <Button> | Button n'accepte que id et class |
Ensemble complet autorisé : xml.md.
Débogage
Le validateur enregistre dans le canal de journalisation custom_hud au niveau d'avertissement :
| Message | Cause |
|---|---|
Layout xml is an invalid resource name "%s" | layout ne correspond pas à un nom de ressource |
Failed to load layout '%s'. | fichier manquant ou non compilé |
Layout contains disallowed node '%s' (type: %d). | <scripts>/<snippets>/<snippet> etc. |
Layout contains disallowed panel type '%s'. | balise en dehors des quatre autorisées |
Layout contains disallowed attribute %s for panel type '%s'. | attribut absent de la liste de cette balise |
Layout contains reference to disallowed resource type '%s'. | <include> cible une ressource non-.vcss |
Layout xml did not pass CustomHud validation "%s" | rejet final |
Layout is invalid. | un nœud d'attribut dont le parent n'est pas un panneau |
En cas d'échec de validation, le panneau n'est jamais créé — un écran vide, pas de rendu partiel. Le rechargement à chaud d'une disposition active dans un état invalide détruit le HUD existant.
Références
| Fichier | Contenu |
|---|---|
| xml.md | Liste blanche complète des balises/attributs, types de nœuds AST, règles du validateur |
| css.md | Les 140 propriétés CSS Panorama, unités, ensembles de valeurs flow/align, liste complète des propriétés et de ce qui est absent |
| entity.md | API côté serveur : SetHasClassForPlayer, SetDialogVariableStringForPlayer, capture d'entrée, événement OnCustomHudClicked |