Source 2 | カスタムHUDレイアウト は何をしますか?
custom_hud_layout エンティティ(C++ CCSCustomHudLayout)を使用すると、マップやサーバーサイドプラグインが、Panorama XML + VCSS で作成されたHUDをプレイヤーに表示できます。
最初に読んでください: カスタムHUDは、通常の Panoramaドキュメントではありません。読み込まれたXMLは、専用のホワイトリストバリデーターによって実行され、4つのパネルタイプと固定の属性セットのみが許可されます。マークアップに style= はなく、<script> もなく、イベントハンドラもありません。すべての動的動作はサーバーから駆動されます。
完全なパイプライン
custom_hud_layout (サーバー, キーバリュー layout="...")
│ m_strLayout がクライアントにレプリケートされる
▼
CSGOCustomHuds (Panorama ウィンドウ) → layout/hud/customhuds.xml
└─ <Panel class="WindowRoot" hittest="false">
└─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
└─ CSGOCustomHudLayoutRoot ← C++ から作成、エンティティごとに1つ、
└─ あなたの XML id も class もなし
戻りパス(クリック時): ユーザーメッセージ CS_UM_CustomHudClicked (390) → サーバーサイド JS イベント OnCustomHudClicked が { player, layout, buttonId } を運びます。
2種類のHUD
すべてのカスタムHUDは2つのうちのいずれかであり、その違いはプレイヤーごとの単一の呼び出しにあります。
| オーバーレイ — デフォルト | カーソル — SetInputCaptureEnabled(slot, true) | |
|---|---|---|
| 説明 | 画面上のピクセル | プレイヤーが使用できる画面 |
| マウス | なし; クロスヘアは照準を続ける | HUD上にカーソルが表示される |
| クリック | 何もクリックできない | Button のクリックがサーバーに届く |
:hover | マッチしない | 動作し、サーバーに負荷はかからない |
| 用途 | タイマー、スコアボード、キルフィード、バナー | ショップ、投票ダイアログ、ピッカー、メニュー |
キャプチャはレイアウトが構築されるたびにオフになるため、エンティティがスポーンした後に再度オンにする必要があります — entity.mdのInput captureを参照してください。キャプチャされたHUD内では、hittest="false" によってカーソルが通過するパネルが決まります。すべてのパネルにこれを記述し、マウスをキャッチする必要がある数少ないコンテナからは外します。
HUDに実際に含めることができるもの
マークアップの語彙は4つのタグですが、それらを通して配置できるものは見た目よりも広範囲です。
- SVGアイコン (
.vsvg): 任意のUIスケールでシャープに表示され、実行時にwash-colorで色付けされます。 - 独自のフォント:
.ttfファイルをアドオンのpanorama/fonts/にドロップします。 - プレイヤーごとのローカライズされたテキスト:
text="#Token"を介して、各プレイヤーの言語でクライアント側で解決され、独自の文字列またはベースゲーム全体のカタログを使用します。 - ベースゲームのアートワーク: ダウンロードに何も追加せずに、
s2r://パスで参照します。 - アニメーションとトランジション: クラストグルによって駆動されるエントリ/終了ペアを含みます。
- ビデオ: 通常の
Panelのbackground-imageレイヤーとして。
Web開発者向け
Panorama は HTML/CSS のような形をしており、これは助けにもなり、落とし穴にもなります。構文は馴染み深く読めますが、プロパティが静かに何も行わないことがあります。ここで有効なメンタルモデルは、クライアントスクリプトが一切ないサーバーレンダリングページであり、存在する更新は パネルのクラスを切り替える と パネルに文字列を設定する の2つだけです。それ以外のすべて — すべてのバリエーション、すべての状態、すべての分岐 — は事前に作成され、単に表示されるだけです。
| あなたが書くもの | ここで書くもの |
|---|---|
<div> | <Panel> |
<span>, テキストノード | <Label text="..."> — テキストはタグの間ではなく属性に存在します |
<img src> | <Image src>、または任意のパネルの background-image |
<button onclick> | <Button id> — クリックはスクリプトではなくサーバーに送られます |
style="..." | なし; class が唯一のスタイリングフックです |
display: none | visibility: collapse |
visibility: hidden | opacity: 0 — レイアウト内に残り、クリック可能です |
position: absolute | x / y に加えて ignore-parent-flow: true |
| flexbox, grid | 親の flow-children、子の horizontal-align / vertical-align |
gap | 子の margin |
filter: blur(4px) | blur: gaussian( 4, 4, 1 ) |
rgba(0, 0, 0, 0.5) | #00000080 — 色は16進数のみ、アルファは最後のバイトです |
background-image 内の linear-gradient(...) | background-**color** 内の gradient( ... ) |
@font-face | .ttf を panorama/fonts/ にドロップ; 宣言は不要 |
:hover | :hover — 期待通りに動作する数少ないものの1つ |
| JS イベントハンドラ | サーバーイベント OnCustomHudClicked、ボタンの id でキー指定 |
JS によって読み取られる data-* | {s:name} ダイアログ変数、サーバーから設定 |
ループまたは <template> | どちらも存在しません — マークアップまたはスタイルシート内で行を列挙します |
calc(), カスタムプロパティ | どちらも存在しません — スタイルシート内で値を列挙します |
Web開発からの2つの習慣が、ここで時間の無駄の大部分を引き起こします。計算値に頼ること: calc() も変数もなく、サーバーは数値を送信できないため、プログレスバーはステップごとのクラスになり、座標はルール内のリテラルになります。そして、クライアントの動作に頼ること: 自分自身で開くドロップダウン、自分自身で切り替わるタブストrip、自分自身で検証するフォーム — これらはすべてラウンドトリップなしでは発生できないため、クリックがサーバーに送られ、クラスが返ってくるというインタラクションを設計してください。
無傷で生き残るのは、デザイナーが最も気にする部分です。ボックスモデル、配置、色、タイポグラフィ、シャドウ、角丸、トランジション、キーフレームアニメーションはすべて、視覚的な作業がそのまま転送されるのに十分なほどWebに近い動作をします。完全なプロパティセットと欠落しているものの完全なリストについては、references/css.md を参照してください。
クイックスタート
実例: キャプチャポイントインジケーター — 画面下部に固定された読み取り専用のストripで、放射状の進行リングを持ち、ダイアログ変数とクラストグルによって完全に駆動されます。クリック可能な要素がないため、入力キャプチャは必要ありません。
作成されたソースは、アドオンのコンテンツルート内の panorama/layout/custom_game/ と panorama/styles/custom_game/ に配置され、コンパイラによって .vxml_c / .vcss_c に変換されます。
panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css
1. マークアップ — panorama/layout/custom_game/capture_point.xml
<root>
<styles>
<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
</styles>
<!--
外側のパネルには id を付けないでください。ローダーが割り当て、コンパイラが拒否します。
匿名のフルスクリーンラッパーのままにし、内部に実際のルートを配置します。
-->
<Panel class="cp-screen" hittest="false">
<!-- 非表示で開始: エンティティはスポーンした瞬間にこのレイアウトを EVERY プレイヤーに表示するため、作成された状態は空の状態でなければなりません。 -->
<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">
<Panel class="cp-ring">
<Panel class="cp-ring-track" hittest="false" />
<!-- サーバーは cp-fill のクラスを cp-fill-0 … cp-fill-100 に書き換えます。
スタイルシートは各ステップを 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. スタイル — panorama/styles/custom_game/capture_point.css(インラインスタイルは一切ありません)
/* フルスクリーンラッパー。noclip により、リングのグローがパネルの端を超えて溢れ出し、
切り取られるのを防ぎます。ラッパー自体はヒットテストされません。 */
.cp-screen
{
width: 100%;
height: 100%;
overflow: noclip;
}
/* ストリップ自体。下端の上に配置されます。非表示でオフセットされた状態で開始し、
サーバーが "show" を追加して表示を再生します。これが .hidden の代替である理由です。
折りたたまれたパネルはレイアウトから外れており、アニメーションの基点がありません。 */
.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;
}
/* 表示された状態。両方のプロパティが上記のトランジションにリストされているため、
クラスの追加と削除によってアニメーションが双方向に実行されます。 */
.cp-root.show
{
opacity: 1;
transform: translateY(0px);
}
/* パネルをレイアウトから完全に削除するため、隣接する要素は穴を残さずに詰められます。
これ以外でパネルを折りたたむものはありません — opacity: 0 はスペースを予約したままにします。 */
.hidden
{
visibility: collapse;
}
/* 2つのリング画像が内部に積み重なる正方形のボックス。両方ともボックスを埋めるため、正確に整列します。 */
.cp-ring
{
width: 56px;
height: 56px;
}
/* 下にある塗りつぶされていないリング。上部の塗りつぶしが進捗として読み取られるように薄暗く表示されます。
アートワークは s2r:// 経由でコンパイルされたテクスチャから取得されます。標準的な形式です。 */
.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;
}
/* 上部の同じアートワーク。wash-color で色が変更され、以下の .cp-fill-* ルールによって
セクターごとに表示されます。1つのグレーのソースを色付けする方が、チームごとにテクスチャを
用意するより効率的です。 */
.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;
}
/* サーバーが選択できる各ステップに対して1つのルール。サーバーはクラスを切り替えることはできますが、
数値を送信することはできません。clip はレイアウトコストがかからずアニメーションするため、
より多くのステップを追加してもコストは低く抑えられます。 */
.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 );
}
/* リングの右側にあるテキスト列。リングに対して中央揃え。 */
.cp-copy
{
flow-children: down;
vertical-align: center;
margin-left: 12px;
}
/* ポイント名 — プレイヤーが最初に読む行。 */
.cp-name
{
font-size: 20px;
font-weight: bold;
color: #ffffffff;
}
/* 補助行。2番目の色ではなくアルファで控えめに表示。 */
.cp-hint
{
font-size: 14px;
color: #ffffff99;
}
/* チームアクセント。サーバーはルートに team-ct または team-t を設定し、子孫セレクターが
塗りつぶしを再描画します。そのため、チームカラーと進捗は互いに独立しています。 */
.cp-root.team-ct .cp-ring-fill
{
wash-color: #56a0ddff;
}
.cp-root.team-t .cp-ring-fill
{
wash-color: #f0a531ff;
}
3. エンティティ — Hammer または CreateEntityByName を使用
| キーバリュー | 値 |
|---|---|
layout | panorama/layout/custom_game/capture_point.vxml |
4. サーバーからの駆動 (cs_script 下のサーバーサイドJS) — entity.md を参照
const hud = Instance.FindEntityByName("capture_hud"); // JS ラッパークラス: CustomHudLayout
function showCapture(slot, name, pct, team) {
hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", true);
hud.SetHasClassForPlayer(slot, "CaptureRoot", "team-" + team, true);
// ステップクラスを交換: 古いクラスをクリアし、新しいクラスを設定
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);
}
サーバーはクラスの切り替えと文字列の設定しかできず、数値を送信できないため、進行リングのような連続値は、ステップごとのクラスに量子化する必要があります。キャプチャリングには5ステップで十分です。より滑らかなバーには、より多くのルールが必要になるだけです。
よくある拒否理由
| やりたいこと | 失敗する理由 |
|---|---|
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, その他のタグ | Panel, Label, Image, Button のみ。ビデオには <Movie> の代わりに background-image レイヤーを使用します |
要素の style="..." | どのパネルタイプにも style 属性はありません |
onactivate="..." または任意のマークアップイベントハンドラ | なし。クリックは id でキー指定されてサーバーに届きます |
<scripts>, <snippets>, <snippet> | ASTノードタイプレベルで拒否されます |
別の .xml の <include> | .vcss リソースのみ参照可能です |
属性としての dialogvariable="..." | サーバーは SetDialogVariableString を介して変数を設定します |
<Button> の hittest | Button は id と class のみを受け入れます |
完全な許可セット: xml.md。
デバッグ
バリデーターは、custom_hud ロギングチャンネルに警告レベルでログを出力します。
| メッセージ | 原因 |
|---|---|
Layout xml is an invalid resource name "%s" | layout がリソース名に解決されない |
Failed to load layout '%s'. | ファイルが見つからないか、コンパイルされていない |
Layout contains disallowed node '%s' (type: %d). | <scripts>/<snippets>/<snippet> など |
Layout contains disallowed panel type '%s'. | 許可された4つ以外のタグ |
Layout contains disallowed attribute %s for panel type '%s'. | そのタグのリストにない属性 |
Layout contains reference to disallowed resource type '%s'. | <include> が .vcss 以外のリソースをターゲットにしている |
Layout xml did not pass CustomHud validation "%s" | 最終的な拒否 |
Layout is invalid. | 親がパネルでない属性ノード |
検証に失敗すると、パネルは作成されません — 空白の画面、部分的なレンダリングはありません。実行中のレイアウトを無効な状態にホットリロードすると、既存のHUDは破棄されます。
参考文献
| ファイル | 内容 |
|---|---|
| xml.md | 完全なタグ/属性ホワイトリスト、ASTノードタイプ、バリデータールール |
| css.md | 全140のPanorama CSSプロパティ、単位、フロー/配置の値セット、事前に... |
...(トリミングされました。翻訳用)