Communitygithub.com

HUD Personalizado de Source 2

Una colección de habilidades para agente que permite trabajar con recursos de Source 2 y desarrollar código relacionado con HUDs personalizados.

¿Qué es HUD Personalizado de Source 2?

Esta habilidad para agente permite trabajar con HUDs personalizados en Source 2 (utilizados en Counter-Strike 2). Explica cómo la entidad `custom_hud_layout` (C++ `CCSCustomHudLayout`) permite a mapas o plugins del servidor presentar a los jugadores un HUD creado en Panorama XML + VCSS. A diferencia de los documentos Panorama estándar, estos HUDs pasan por un validador que solo permite cuatro tipos de panel y un conjunto fijo de atributos, sin `style=`, `<script>` o controladores de eventos en el marcado. Todo el comportamiento dinámico se maneja desde el servidor. La habilidad detalla los dos tipos de HUD (superposición y cursor), el marcado permitido (Panel, Label, Image, Button), y proporciona una guía de estilo con ejemplos prácticos como un indicador de punto de captura, además de referencias a archivos de validación y propiedades CSS. Es útil para desarrolladores que crean HUDs para juegos de Source 2, ofreciendo una comprensión profunda de las restricciones y capacidades del sistema.

Compatible con~Claude Code~Codex CLI✓Cursor
npx skills add Wend4r/s2r-skills

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

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 predeterminadaCursor — SetInputCaptureEnabled(slot, true)
Qué espíxeles en la pantallauna pantalla que el jugador puede usar
Ratónninguno; la mira sigue apuntandoaparece un cursor sobre el HUD
Clicsnada se puede hacer cliclos clics de Button llegan al servidor
:hovernunca coincidefunciona, y no le cuesta nada al servidor
Construccióntemporizadores, marcadores, notificaciones de muerte, bannerstiendas, 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 con wash-color;
  • sus propias fuentes, colocando archivos .ttf en panorama/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-image en un Panel normal.

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íaAquí 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: nonevisibility: collapse
visibility: hiddenopacity: 0 — todavía en el diseño, todavía se puede hacer clic
position: absolutex / y más ignore-parent-flow: true
flexbox, gridflow-children en el padre, horizontal-align / vertical-align en el hijo
gapmargin 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-imagegradient( ... ) en background-**color**
@font-facecoloque 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 JSel evento del servidor OnCustomHudClicked, keyed por el id del botón
data-* leído por JSvariables 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 personalizadasninguna 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

KeyvalueValor
layoutpanorama/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 deseaPor qué falla
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, cualquier otra etiquetasolo Panel, Label, Image, Button — para video use una capa background-image en lugar de <Movie>
style="..." en un elementoningún tipo de panel tiene un atributo style
onactivate="..." o cualquier controlador de eventos en el marcadoninguno; los clics llegan al servidor keyed por id
<scripts>, <snippets>, <snippet>rechazado a nivel de tipo de nodo AST
<include> de otro .xmlsolo se puede hacer referencia a un recurso .vcss
dialogvariable="..." como atributoel 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:

MensajeCausa
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

ArchivoContenido
xml.mdLista blanca completa de etiquetas/atributos, tipos de nodos AST, reglas del validador
css.mdLas 140 propiedades CSS de Panorama, unidades, conjuntos de valores de flujo/alineación, propiedades previas y omitidas, funciones de glifo
entity.mdSeñ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.mdInyección de addon, compilación de recursos, carga de activos, compilación de diseño de HUD, introspección de JS

Skills relacionados