Custom HUD
You can use a custom_hud_layout entity to create a custom HUD for players. The interface is built with Panorama XML and CSS, while the server updates its state through the methods provided by CCSCustomHudLayout.
Custom HUD is still a new and unstable feature, valve may makes breaking change that cause incompatible API in the future. If the actual behavior differs from what is described here, contact the development team so the documentation can be corrected.
Example XML
The examples below use the following XML layout:
<!-- example.xml -->
<root>
<styles>
<include src="s2r://panorama/styles/custom_game/example.vcss_c" />
</styles>
<Panel class="example_class" id="main_panel">
<Label class="example_label" id="text1" text="{s:dynamic}" />
<Button class="example_button" id="action_button">
<Label text="Click me" />
</Button>
</Panel>
</root>The id attribute identifies the HUD element to update. For example, text1 is passed as the panelId in the methods below. {s:dynamic} is a dynamic string that can be updated by the server, where dynamic is the variableName.
Creating a custom_hud_layout Entity
Create the entity with the Entity API, set the path to the Panorama XML layout, notify the engine that the field has changed, and then spawn the entity:
var hud = Core.EntitySystem.CreateEntity<CCSCustomHudLayout>();
hud.StrLayout = "panorama/layout/custom_game/example.xml";
hud.StrLayoutUpdated();
hud.DispatchSpawn();Dynamic Strings
Setting and Reading a Global Value
SetDialogVariableString sets a global value used by all players:
hud.SetDialogVariableString("text1", "dynamic", "Global text");
string? globalValue = hud.GetDialogVariableString("text1", "dynamic");The text1 argument corresponds to the XML element's id, while dynamic is the variable name in {s:dynamic}. GetDialogVariableString returns null when no global value has been set.
Setting, Reading, and Removing a Per-Player Override
Use methods with the ForPlayer suffix to set an override for a specific player:
const int playerId = 0;
hud.SetDialogVariableStringForPlayer(
playerId,
"text1",
"dynamic",
"Text shown only to this player"
);
string? playerValue = hud.GetDialogVariableStringForPlayer(
playerId,
"text1",
"dynamic"
);
hud.RemoveDialogVariableStringForPlayer(playerId, "text1", "dynamic");GetDialogVariableStringForPlayer reads only the player's override and does not fall back to the global value. It therefore returns null when no override has been set, even if a global value exists. After RemoveDialogVariableStringForPlayer is called, the value displayed to that player follows the global setting again.
Dynamic CSS Classes
You can dynamically control whether a HUD element has a particular CSS class. panelId corresponds to the element's id in the XML layout, and className should be the name of a class defined in the Panorama CSS.
EHudPanelClassStatus_t supports the following states:
| State | Meaning |
|---|---|
k_eHudPanelClassStatus_HasClass | The element has the class |
k_eHudPanelClassStatus_DoesNotHaveClass | The element does not have the class |
k_eHudPanelClassStatus_Undefined | No class state is defined; this is also returned when reading a state that does not exist |
Setting and Reading the Global Class State
hud.SetHasClass(
"main_panel",
"highlight",
EHudPanelClassStatus_t.k_eHudPanelClassStatus_HasClass
);
EHudPanelClassStatus_t globalClassStatus = hud.GetHasClass(
"main_panel",
"highlight"
);Setting and Reading a Per-Player Class State
const int playerId = 0;
hud.SetHasClassForPlayer(
playerId,
"main_panel",
"highlight",
EHudPanelClassStatus_t.k_eHudPanelClassStatus_DoesNotHaveClass
);
EHudPanelClassStatus_t playerClassStatus = hud.GetHasClassForPlayer(
playerId,
"main_panel",
"highlight"
);Both GetHasClass and GetHasClassForPlayer return k_eHudPanelClassStatus_Undefined if the specified panelId, className, or corresponding state does not exist.
Input Capture
The input capture state controls whether the Custom HUD receives player input. When enabled, the player can move the mouse cursor freely to click buttons. You can set the state globally for all players or separately for a specific player.
Global Input Capture
hud.SetInputCaptureEnabled(true);
bool globallyEnabled = hud.IsInputCaptureEnabled();Per-Player Input Capture
const int playerId = 0;
hud.SetInputCaptureEnabledForPlayer(playerId, true);
bool enabledForPlayer = hud.IsInputCaptureEnabledForPlayer(playerId);
// Disable input capture for this player when the interaction is complete.
hud.SetInputCaptureEnabledForPlayer(playerId, false);Handling Button Clicks
When input capture is enabled, clicking a button in the layout triggers Core.Event.OnCustomHudClicked. Subscribe to the event when the plugin loads and unsubscribe when it unloads:
public override void Load(bool hotReload)
{
Core.Event.OnCustomHudClicked += OnCustomHudClicked;
}
public override void Unload(bool hotReload)
{
Core.Event.OnCustomHudClicked -= OnCustomHudClicked;
}
private void OnCustomHudClicked(IOnCustomHudClickedEvent @event)
{
if (@event.ButtonId != "action_button")
{
return;
}
Console.WriteLine($"Player {@event.PlayerId} clicked {@event.ButtonId}");
}The event provides the following properties:
| Property | Description |
|---|---|
PlayerId | The ID of the player who clicked the button |
ButtonId | The clicked button's id from the Panorama XML layout |
CustomHudLayout | The CCSCustomHudLayout entity that contains the clicked button |
OnCustomHudClicked receives clicks from every Custom HUD layout. If a plugin creates multiple layouts, compare the layout entity you stored before handling the button ID.
See Core Events for attribute-based listeners and general event subscription behavior.
Async Variants
Each thread-unsafe CCSCustomHudLayout extension method described above has a corresponding Async variant that returns a Task. Use these variants from background tasks or other asynchronous code. They run immediately when called on the game thread and otherwise schedule the operation on the game thread.
await hud.SetDialogVariableStringAsync("text1", "dynamic", "Global text");
await hud.SetHasClassForPlayerAsync(
playerId,
"main_panel",
"highlight",
EHudPanelClassStatus_t.k_eHudPanelClassStatus_HasClass
);
await hud.SetInputCaptureEnabledForPlayerAsync(playerId, true);The async methods use the same parameters and behavior as their synchronous counterparts. This includes SetDialogVariableStringAsync, SetDialogVariableStringForPlayerAsync, RemoveDialogVariableStringForPlayerAsync, SetHasClassAsync, SetHasClassForPlayerAsync, SetInputCaptureEnabledAsync, and SetInputCaptureEnabledForPlayerAsync. Getter methods remain synchronous.
Method Summary
| Method | Description |
|---|---|
SetDialogVariableString | Sets a global dynamic string |
GetDialogVariableString | Reads a global dynamic string, returning null if it has not been set |
SetDialogVariableStringForPlayer | Sets a player's dynamic string override |
GetDialogVariableStringForPlayer | Reads a player's override without falling back to the global value |
RemoveDialogVariableStringForPlayer | Removes a player's override so the displayed value follows the global setting again |
SetHasClass | Sets the global CSS class state |
GetHasClass | Reads the global CSS class state |
SetHasClassForPlayer | Sets a player's CSS class state |
GetHasClassForPlayer | Reads a player's CSS class state |
SetInputCaptureEnabled | Sets the global input capture state |
IsInputCaptureEnabled | Reads the global input capture state |
SetInputCaptureEnabledForPlayer | Sets a player's input capture state |
IsInputCaptureEnabledForPlayer | Reads a player's input capture state |
Synchronous methods that change HUD state are thread-unsafe. From a background task, prefer and await their async variants instead of calling the synchronous methods directly. See Thread Safety for more information.