Source 2 | Layout de HUD Personalizado (custom_hud_layout)
A entidade custom_hud_layout (C++ CCSCustomHudLayout) permite que um mapa ou plugin do servidor apresente aos jogadores um HUD criado em Panorama XML + VCSS.
Leia isto primeiro: um HUD personalizado não é um documento Panorama comum. O XML carregado passa por um validador de lista de permissões dedicado que permite apenas quatro tipos de painel e um conjunto fixo de atributos. Não há style=, nem <script>, e nenhum manipulador de eventos na marcação.
Todo o comportamento dinâmico é controlado a partir do servidor.
A pipeline completa
custom_hud_layout (servidor, keyvalue layout="...")
│ m_strLayout é replicado para o cliente
▼
CSGOCustomHuds (janela Panorama) → layout/hud/customhuds.xml
└─ <Panel class="WindowRoot" hittest="false">
└─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
└─ CSGOCustomHudLayoutRoot ← criado a partir de C++, um por entidade,
└─ SEU XML sem id e sem class
Caminho de retorno (um clique): mensagem do usuário CS_UM_CustomHudClicked (390) → evento JS do servidor OnCustomHudClicked carregando { player, layout, buttonId }.
Dois tipos de HUD
Cada HUD personalizado é uma de duas coisas, e a diferença é uma única chamada por jogador:
| Overlay — o padrão | Cursor — SetInputCaptureEnabled(slot, true) | |
|---|---|---|
| O que é | pixels na tela | uma tela que o jogador pode usar |
| Mouse | nenhum; a mira continua apontando | um cursor aparece sobre o HUD |
| Cliques | nada é clicável | cliques em Button alcançam o servidor |
:hover | nunca corresponde | funciona e não custa nada ao servidor |
| Construção | temporizadores, placares, feed de mortes, banners | lojas, diálogos de voto, seletores, menus |
A captura está desligada toda vez que o layout é construído, portanto, precisa ser ativada novamente após a entidade surgir — veja entity.md, Input capture. Dentro de um HUD capturado, hittest="false" decide por quais painéis o cursor passa; escreva-o em tudo e remova-o dos poucos contêineres que devem capturar o mouse.
O que um HUD pode realmente conter
O vocabulário de marcação são quatro tags, mas o que você pode colocar por meio delas é mais amplo do que parece:
- Ícones SVG (
.vsvg) que permanecem nítidos em qualquer escala de interface, tingidos em tempo de execução comwash-color; - suas próprias fontes, colocando arquivos
.ttfna pastapanorama/fonts/do addon; - texto localizado por jogador, via
text="#Token"resolvido no lado do cliente no idioma de cada jogador, usando suas próprias strings ou todo o catálogo do jogo base; - a arte do jogo base, referenciada pelo caminho
s2r://sem adicionar nada ao seu download; - animações e transições, incluindo pares de entrada/saída acionados por alternâncias de classe;
- vídeo, como uma camada
background-imageem umPanelcomum.
Se você está vindo da web
Panorama tem a forma de HTML/CSS, o que é uma ajuda e uma armadilha: a sintaxe parece familiar, e então uma propriedade silenciosamente não faz nada. O modelo mental que funciona é uma página renderizada no servidor sem nenhum script do cliente, onde as únicas duas atualizações que existem são alternar uma classe em um painel e definir uma string em um painel. Todo o resto — cada variante, cada estado, cada ramo — é criado antecipadamente e meramente revelado.
| Você escreveria | Aqui você escreve |
|---|---|
<div> | <Panel> |
<span>, um nó de texto | <Label text="..."> — o texto vive em um atributo, não entre tags |
<img src> | <Image src>, ou um background-image em qualquer painel |
<button onclick> | <Button id> — o clique vai para o servidor, não para um script |
style="..." | nada; class é o único gancho de estilização |
display: none | visibility: collapse |
visibility: hidden | opacity: 0 — ainda no layout, ainda clicável |
position: absolute | x / y mais ignore-parent-flow: true |
| flexbox, grid | flow-children no pai, horizontal-align / vertical-align no filho |
gap | margin nos filhos |
filter: blur(4px) | blur: gaussian( 4, 4, 1 ) |
rgba(0, 0, 0, 0.5) | #00000080 — cores são apenas hexadecimais, alfa é o último byte |
linear-gradient(...) em background-image | gradient( ... ) em background-**color** |
@font-face | coloque o .ttf em panorama/fonts/; nada a declarar |
:hover | :hover — uma das poucas coisas que funciona exatamente como esperado |
| um manipulador de evento JS | o evento do servidor OnCustomHudClicked, indexado pelo id do botão |
data-* lido por JS | variáveis de diálogo {s:nome}, definidas a partir do servidor |
um loop ou <template> | nenhum existe — enumere as linhas na marcação ou na folha de estilo |
calc(), propriedades personalizadas | nenhuma existe — enumere os valores na folha de estilo |
Dois hábitos do trabalho na web causam a maior parte do tempo perdido aqui. Recorrer a um valor calculado: não há calc(), nem variáveis, e o servidor não pode enviar um número, então uma barra de progresso é uma classe por etapa e uma coordenada é um literal em uma regra. E recorrer a comportamento do cliente: um menu suspenso que abre sozinho, uma guia que alterna sozinha, um formulário que se valida sozinho — nada disso pode acontecer sem uma ida e volta ao servidor, então projete a interação em torno de um clique indo para o servidor e uma classe voltando.
O que sobrevive intacto é a parte que os designers mais se importam: o modelo de caixa, alinhamento, cor, tipografia, sombras, cantos arredondados, transições e animações keyframe se comportam próximos o suficiente da web para que o trabalho visual seja transferido diretamente. Veja references/css.md para o conjunto completo de propriedades e a lista completa do que está ausente.
Início rápido
Um exemplo prático: um indicador de ponto de captura — uma faixa somente leitura ancorada na parte inferior da tela com um anel de progresso radial, controlado inteiramente por variáveis de diálogo e alternância de classes. Não precisa de captura de entrada, porque nada nele é clicável.
Os arquivos de origem ficam em panorama/layout/custom_game/ e panorama/styles/custom_game/ na raiz de conteúdo do addon; o compilador os transforma em .vxml_c / .vcss_c:
panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css
1. Marcação — panorama/layout/custom_game/capture_point.xml
<root>
<styles>
<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
</styles>
<!--
O painel externo NÃO deve ter um id — o carregador o atribui e o compilador
rejeita um. Mantenha-o como um wrapper anônimo de tela cheia e coloque a raiz real dentro.
-->
<Panel class="cp-screen" hittest="false">
<!-- Começa oculto: a entidade mostra este layout para TODOS os jogadores no momento em que surge, então o estado criado tem que ser o vazio. -->
<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">
<Panel class="cp-ring">
<Panel class="cp-ring-track" hittest="false" />
<!-- O servidor reescreve a classe de cp-fill para cp-fill-0 … cp-fill-100;
a folha de estilo transforma cada etapa em um sweep 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 (não há estilos inline)
/* Wrapper de tela cheia. noclip permite que o brilho do anel se espalhe para além da borda do painel em vez de
ser cortado; o wrapper em si nunca é testado para hit. */
.cp-screen
{
width: 100%;
height: 100%;
overflow: noclip;
}
/* A faixa em si, estacionada acima da borda inferior. Começa invisível e deslocada, e o
servidor adiciona "show" para reproduzir a revelação — é por isso que esta é uma alternativa para .hidden,
não uma adição: um painel recolhido está fora do layout e não tem nada para 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;
}
/* O estado revelado. Ambas as propriedades estão listadas na transição acima, então adicionar e
remover a classe anima em cada direção. */
.cp-root.show
{
opacity: 1;
transform: translateY(0px);
}
/* Remove um painel do layout completamente, para que os vizinhos se aproximem em vez de deixar um buraco.
Nada mais recolhe um painel — opacity: 0 mantém o espaço reservado. */
.hidden
{
visibility: collapse;
}
/* Caixa quadrada onde as duas imagens do anel são empilhadas. Ambas a preenchem, então se alinham exatamente. */
.cp-ring
{
width: 56px;
height: 56px;
}
/* O anel não preenchido abaixo, escurecido para que o preenchimento no topo seja lido como progresso. A arte vem
de uma textura compilada via s2r:// — a forma padrão. */
.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;
}
/* A mesma arte no topo, recolorida por wash-color e revelada um setor de cada vez pelas
regras .cp-fill-* abaixo. Tingir uma fonte cinza é melhor do que enviar uma textura por equipe. */
.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;
}
/* Uma regra por etapa que o servidor pode selecionar, porque ele pode alternar uma classe, mas não pode enviar um
número. clip não tem custo de layout e anima, então mais etapas são baratas. */
.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 );
}
/* Coluna de texto à direita do anel, centralizada em relação a ele. */
.cp-copy
{
flow-children: down;
vertical-align: center;
margin-left: 12px;
}
/* Nome do ponto — a linha que o jogador lê primeiro. */
.cp-name
{
font-size: 20px;
font-weight: bold;
color: #ffffffff;
}
/* Linha de suporte, suavizada com alfa em vez de uma segunda cor. */
.cp-hint
{
font-size: 14px;
color: #ffffff99;
}
/* Destaque da equipe. O servidor define team-ct ou team-t na raiz, e o seletor descendente
repinta o preenchimento — então a cor da equipe e o progresso permanecem independentes um do outro. */
.cp-root.team-ct .cp-ring-fill
{
wash-color: #56a0ddff;
}
.cp-root.team-t .cp-ring-fill
{
wash-color: #f0a531ff;
}
3. Entidade — no Hammer ou via CreateEntityByName
| Keyvalue | Valor |
|---|---|
layout | panorama/layout/custom_game/capture_point.vxml |
4. Controlando a partir do servidor (JS do lado do servidor em cs_script) — veja 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);
// troca a classe da etapa: limpa a antiga, define a nova
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", "Segure o ponto");
}
function hideCapture(slot) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", false);
}
Como o servidor só pode alternar classes e definir strings — não pode enviar um número — um valor contínuo como um anel de progresso precisa ser quantizado em uma classe por etapa. Cinco etapas são suficientes para um anel de captura; uma barra mais suave precisa apenas de mais regras.
Rejeições comuns
| O que você quer | Por que falha |
|---|---|
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, qualquer outra tag | apenas Panel, Label, Image, Button — para vídeo use uma camada background-image em vez de <Movie> |
style="..." em um elemento | nenhum tipo de painel tem um atributo style |
onactivate="..." ou qualquer manipulador de evento na marcação | nenhum; cliques alcançam o servidor indexados por id |
<scripts>, <snippets>, <snippet> | rejeitado no nível do tipo de nó AST |
<include> de outro .xml | apenas um recurso .vcss pode ser referenciado |
dialogvariable="..." como um atributo | o servidor define variáveis via SetDialogVariableString |
hittest em <Button> | Button aceita apenas id e class |
Conjunto completo permitido: xml.md.
Depuração
O validador registra no canal de log custom_hud no nível de aviso:
| Mensagem | Causa |
|---|---|
Layout xml is an invalid resource name "%s" | layout não resolve para um nome de recurso |
Failed to load layout '%s'. | arquivo ausente ou não compilado |
Layout contains disallowed node '%s' (type: %d). | <scripts>/<snippets>/<snippet> etc. |
Layout contains disallowed panel type '%s'. | tag fora das quatro permitidas |
Layout contains disallowed attribute %s for panel type '%s'. | atributo não na lista dessa tag |
Layout contains reference to disallowed resource type '%s'. | <include> tem como alvo um recurso que não é .vcss |
Layout xml did not pass CustomHud validation "%s" | rejeição final |
Layout is invalid. | um nó de atributo cujo pai não é um painel |
Em uma falha de validação, o painel nunca é criado — uma tela em branco, sem renderização parcial. Recarregar um layout ativo em um estado inválido destrói o HUD existente.
Referências
| Arquivo | Conteúdo |
|---|---|
| xml.md | Lista de permissões completa de tags/atributos, tipos de nós AST, regras do validador |
| css.md | Todas as 140 propriedades CSS do Panorama, unidades, conjuntos de valores de fluxo/alinhamento, equivalências de web para Panorama |
| entity.md | Atributos de entidade, captura de entrada, variáveis de diálogo, alternância de classes, rede, total de bytes, multiquadro |
| js.md | Classe CustomHudLayout para scripts de servidor, chamadas de método, configuração de retorno de chamada, slots, eventos |
| overlay_multipanel.md | Várias instâncias de HUD em uma única entidade, casos de uso, limites |
| gradients.md | Função gradient(), parâmetros, paradas de cor, dicas de design, exemplos de VCSS |
| implementation.md | CCSCustomHudLayout, CSGOCustomHudLayoutRoot, layout, opacidade temporária, canal custom_hud |