Source 2 | Diseño de HUD Personalizado
La entidad custom_hud_layout (C++ CCSCustomHudLayout) permite que un mapa o un plugin del servidor presente a los jugadores un HUD creado en Panorama XML + VCSS.
Lea esto primero: un HUD personalizado no es un documento Panorama común. El XML cargado pasa por un validador de listas blancas dedicado que permite solo cuatro tipos de panel y un conjunto fijo de atributos. No hay style=, ni <script>, ni controladores de eventos en el marcado. Todo el comportamiento dinámico se maneja desde el servidor.
El pipeline completo
custom_hud_layout (servidor, keyvalue layout="...")
│ m_strLayout se replica al cliente
▼
CSGOCustomHuds (ventana de Panorama) → layout/hud/customhuds.xml
└─ <Panel class="WindowRoot" hittest="false">
└─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
└─ CSGOCustomHudLayoutRoot ← creado desde C++, uno por entidad,
└─ TU XML sin id y sin clase
Ruta de retorno (un clic): mensaje de usuario CS_UM_CustomHudClicked (390) → evento JS del servidor OnCustomHudClicked que lleva { player, layout, buttonId }.
Dos tipos de HUD
Cada HUD personalizado es una de dos cosas, y la diferencia es una sola llamada por jugador:
| Superposición — la predeterminada | Cursor — SetInputCaptureEnabled(slot, true) | |
|---|---|---|
| Qué es | píxeles en la pantalla | una pantalla que el jugador puede usar |
| Ratón | ninguno; la mira sigue apuntando | aparece un cursor sobre el HUD |
| Clics | nada se puede hacer clic | los clics de Button llegan al servidor |
:hover | nunca coincide | funciona, y no le cuesta nada al servidor |
| Construcción | temporizadores, marcadores, notificaciones de muerte, banners | tiendas, diálogos de votación, selectores, menús |
La captura está desactivada cada vez que se construye el diseño, por lo que debe volver a activarse después de que la entidad aparezca; consulte entity.md, Captura de entrada. Dentro de un HUD capturado, hittest="false" decide qué paneles atraviesa el cursor; escríbalo en todo y retírelo de los pocos contenedores que deban capturar el ratón.
Lo que un HUD puede contener realmente
El vocabulario del marcado son cuatro etiquetas, pero lo que se puede poner a través de ellas es más amplio de lo que parece:
- Iconos SVG (
.vsvg) que se mantienen nítidos en cualquier escala de interfaz de usuario, teñidos en tiempo de ejecución conwash-color; - sus propias fuentes, colocando archivos
.ttfenpanorama/fonts/del addon; - texto localizado por jugador, a través de
text="#Token"resuelto del lado del cliente en el idioma de cada jugador, utilizando sus propias cadenas o todo el catálogo del juego base; - el arte del juego base, referenciado por la ruta
s2r://sin agregar nada a su descarga; - animaciones y transiciones, incluidos pares de entrada/salida controlados por cambios de clase;
- video, como una capa de
background-imageen unPanelnormal.
Si viene del mundo web
Panorama tiene forma de HTML/CSS, lo cual es una ayuda y una trampa: la sintaxis se lee como familiar, y luego una propiedad no hace nada en silencio. El modelo mental que funciona es una página renderizada por el servidor sin ningún script de cliente, donde las únicas dos actualizaciones que existen son alternar una clase en un panel y establecer una cadena en un panel. Todo lo demás (cada variante, cada estado, cada rama) se crea de antemano y simplemente se revela.
| Usted escribiría | Aquí escribe |
|---|---|
<div> | <Panel> |
<span>, un nodo de texto | <Label text="..."> — el texto vive en un atributo, no entre etiquetas |
<img src> | <Image src>, o un background-image en cualquier panel |
<button onclick> | <Button id> — el clic va al servidor, no a un script |
style="..." | nada; class es el único enlace de estilo |
display: none | visibility: collapse |
visibility: hidden | opacity: 0 — todavía en el diseño, todavía se puede hacer clic |
position: absolute | x / y más ignore-parent-flow: true |
| flexbox, grid | flow-children en el padre, horizontal-align / vertical-align en el hijo |
gap | margin en los hijos |
filter: blur(4px) | blur: gaussian( 4, 4, 1 ) |
rgba(0, 0, 0, 0.5) | #00000080 — los colores son solo hexadecimales, el alfa es el último byte |
linear-gradient(...) en background-image | gradient( ... ) en background-**color** |
@font-face | coloque el .ttf en panorama/fonts/; nada que declarar |
:hover | :hover — una de las pocas cosas que funciona exactamente como se espera |
| un controlador de eventos JS | el evento del servidor OnCustomHudClicked, keyed por el id del botón |
data-* leído por JS | variables de diálogo {s:nombre}, establecidas desde el servidor |
un bucle o <template> | ninguno existe — enumere las filas en el marcado o en la hoja de estilo |
calc(), propiedades personalizadas | ninguna existe — enumere los valores en la hoja de estilo |
Dos hábitos del trabajo web causan la mayor parte del tiempo perdido aquí. Buscar un valor calculado: no hay calc(), ni variables, y el servidor no puede enviar un número, por lo que una barra de progreso es una clase por paso y una coordenada es un literal en una regla. Y buscar un comportamiento de cliente: un menú desplegable que se abre solo, una pestaña que se cambia sola, un formulario que se valida solo — nada de eso puede suceder sin un viaje de ida y vuelta, así que diseñe la interacción en torno a un clic que va al servidor y una clase que regresa.
Lo que sobrevive intacto es la parte que más les importa a los diseñadores: el modelo de caja, la alineación, el color, la tipografía, las sombras, las esquinas redondeadas, las transiciones y las animaciones de fotogramas clave se comportan lo suficientemente cerca de la web como para que el trabajo visual se transfiera directamente. Consulte references/css.md para conocer el conjunto completo de propiedades y la lista completa de lo que está ausente.
Inicio rápido
Un ejemplo práctico: un indicador de punto de captura — una barra de solo lectura anclada a la parte inferior de la pantalla con un anillo de progreso radial, controlado completamente por variables de diálogo y cambios de clase. No necesita captura de entrada, porque nada en él se puede hacer clic.
Los archivos fuente creados viven en panorama/layout/custom_game/ y panorama/styles/custom_game/ en la raíz de contenido del addon; el compilador los convierte en .vxml_c / .vcss_c:
panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css
1. Marcado — panorama/layout/custom_game/capture_point.xml
<root>
<styles>
<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
</styles>
<!--
El panel externo NO debe llevar un id — el cargador lo asigna, y el compilador
lo rechaza. Manténgalo como un envoltorio anónimo de pantalla completa y coloque la raíz real dentro.
-->
<Panel class="cp-screen" hittest="false">
<!-- Comienza oculto: la entidad muestra este diseño a CADA jugador en el momento en que aparece, así que el estado creado tiene que ser el vacío. -->
<Panel id="CaptureRoot" class="cp-root hiden" hittest="false">
<Panel class="cp-ring">
<Panel class="cp-ring-track" hittest="false" />
<!-- El servidor reescribe la clase de cp-fill a cp-fill-0 … cp-fill-100;
la hoja de estilo convierte cada paso en un barrido clip: radial(). -->
<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. Estilos — panorama/styles/custom_game/capture_point.css (no hay estilos en línea en absoluto)
/* Envoltorio de pantalla completa. noclip permite que el brillo del anillo se derrame más allá del borde del panel en lugar de
cortarse; el envoltorio en sí mismo nunca se prueba de impacto. */
.cp-screen
{
width: 100%;
height: 100%;
overflow: noclip;
}
/* La barra en sí, estacionada sobre el borde inferior. Comienza invisible y desplazada, y el
servidor agrega "show" para reproducir la revelación — por eso esto es una alternativa a .hidden,
no una adición: un panel colapsado está fuera del diseño y no tiene nada desde lo que animar. */
.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;
}
/* El estado revelado. Ambas propiedades se enumeran en la transición anterior, por lo que agregar y
eliminar la clase anima en cada dirección. */
.cp-root.show
{
opacity: 1;
transform: translateY(0px);
}
/* Elimina un panel del diseño por completo, para que los vecinos se cierren en lugar de dejar un agujero.
Nada más colapsa un panel — opacity: 0 mantiene el espacio reservado. */
.hidden
{
visibility: collapse;
}
/* Caja cuadrada donde se apilan las dos imágenes del anillo. Ambas la llenan, por lo que se alinean exactamente. */
.cp-ring
{
width: 56px;
height: 56px;
}
/* La pista del anillo sin relleno debajo, atenuada para que el relleno de arriba se lea como progreso. La obra de arte viene
de una textura compilada a través de s2r:// — la forma estándar. */
.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 misma obra de arte en la parte superior, recoloreada por wash-color y revelada un sector a la vez por las
reglas .cp-fill-* a continuación. Teñir una fuente gris supera enviar una textura por equipo. */
.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;
}
/* Una regla por paso que el servidor puede seleccionar, porque puede alternar una clase pero no puede enviar un
número. clip no tiene costo de diseño y anima, por lo que más pasos son económicos. */
.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 );
}
/* Columna de texto a la derecha del anillo, centrada contra él. */
.cp-copy
{
flow-children: down;
vertical-align: center;
margin-left: 12px;
}
/* Nombre del punto — la línea que el jugador lee primero. */
.cp-name
{
font-size: 20px;
font-weight: bold;
color: #ffffffff;
}
/* Línea de apoyo, contenida con alfa en lugar de un segundo color. */
.cp-hint
{
font-size: 14px;
color: #ffffff99;
}
/* Acento del equipo. El servidor establece team-ct o team-t en la raíz, y el selector
descendiente repinta el relleno — por lo que el color del equipo y el progreso permanecen independientes entre sí. */
.cp-root.team-ct .cp-ring-fill
{
wash-color: #56a0ddff;
}
.cp-root.team-t .cp-ring-fill
{
wash-color: #f0a531ff;
}
3. Entidad — en Hammer o mediante CreateEntityByName
| Keyvalue | Valor |
|---|---|
layout | panorama/layout/custom_game/capture_point.vxml |
4. Controlarlo desde el servidor (JS del lado del servidor bajo cs_script) — consulte entity.md
const hud = Instance.FindEntityByName("capture_hud"); // Clase envoltorio JS: CustomHudLayout
function showCapture(slot, name, pct, team) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", true);
hud.SetHasClassForPlayer(slot, "CaptureRoot", "team-" + team, true);
// intercambiar la clase de paso: borrar la anterior, establecer la nueva
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);
}
Debido a que el servidor solo puede alternar clases y establecer cadenas (no puede enviar un número), un valor continuo como un anillo de progreso debe cuantificarse en una clase por paso. Cinco pasos son suficientes para un anillo de captura; una barra más suave solo necesita más reglas.
Rechazos comunes
| Lo que desea | Por qué falla |
|---|---|
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, cualquier otra etiqueta | solo Panel, Label, Image, Button — para video use una capa background-image en lugar de <Movie> |
style="..." en un elemento | ningún tipo de panel tiene un atributo style |
onactivate="..." o cualquier controlador de eventos en el marcado | ninguno; los clics llegan al servidor keyed por id |
<scripts>, <snippets>, <snippet> | rechazado a nivel de tipo de nodo AST |
<include> de otro .xml | solo se puede hacer referencia a un recurso .vcss |
dialogvariable="..." como atributo | el servidor establece variables a través de SetDialogVariableString |
hittest en <Button> | Button acepta solo id y class |
Conjunto completo permitido: xml.md.
Depuración
El validador registra en el canal de registro custom_hud con nivel de advertencia:
| Mensaje | Causa |
|---|---|
Layout xml is an invalid resource name "%s" | layout no se resuelve a un nombre de recurso |
Failed to load layout '%s'. | archivo faltante o no compilado |
Layout contains disallowed node '%s' (type: %d). | <scripts>/<snippets>/<snippet> etc. |
Layout contains disallowed panel type '%s'. | etiqueta fuera de las cuatro permitidas |
Layout contains disallowed attribute %s for panel type '%s'. | atributo no en la lista de esa etiqueta |
Layout contains reference to disallowed resource type '%s'. | <include> apunta a un recurso que no es .vcss |
Layout xml did not pass CustomHud validation "%s" | rechazo final |
Layout is invalid. | un nodo de atributo cuyo padre no es un panel |
Ante una falla de validación, el panel nunca se crea — una pantalla en blanco, sin renderizado parcial. La recarga en caliente de un diseño activo a un estado no válido destruye el HUD existente.
Referencias
| Archivo | Contenido |
|---|---|
| xml.md | Lista blanca completa de etiquetas/atributos, tipos de nodos AST, reglas del validador |
| css.md | Las 140 propiedades CSS de Panorama, unidades, conjuntos de valores de flujo/alineación, propiedades previas y omitidas, funciones de glifo |
| entity.md | Señales de entrada, variables de diálogo, captura de entrada, ciclo de vida del HUD, devolución de llamada de creación, depuración de KVs |
| runtime.md | Inyección de addon, compilación de recursos, carga de activos, compilación de diseño de HUD, introspección de JS |