"Building an s&box HUD"
You need a real HUD: prompts, hotbar, toasts, title/start menu, pause — without input races, unpausable modals, or frozen panels.
s&box UI is Razor PanelComponents on a ScreenPanel. Without a single modal router and correct BuildHash, panels fight over keys and go stale. SCSS is a flex-default dialect with its own pointer-events rules.
Host
One GameObject hosts ScreenPanel + every PanelComponent. Paired MyPanel.razor.scss — root selector is the class name (MyPanel { … }).
Modal routing — static UIState
public static class UIState
{
public static bool StartMenuOpen = true;
public static bool PauseOpen;
public static bool InventoryOpen;
// …
public static bool AnyModalOpen =>
StartMenuOpen || PauseOpen || InventoryOpen /* + day-summary, etc. */;
}Game clock and player input check AnyModalOpen → opening any panel pauses the world.
Each panel closes itself on e.g. Input.Pressed("attack2") in its own OnUpdate. Toggle-key ownership lives in ONE place per key — two components both reacting to the same press = same-frame open/close races.
Fold every open flag into BuildHash() (ui-frozen-buildhash).
Layout patterns that work
| Region | Role |
|---|---|
| Top-left | Dashboard / resources |
| Top-right | Toast stack (TimeSince + prune in OnUpdate) |
| Bottom-center | Prompt + hotbar |
| Bottom-right | Hints |
| Full-screen | Modal backdrops inside the same panel tree |
Title screen: default StartMenuOpen = true; game-state waits; buttons call StartNew() / ContinueGame(). Continue only when FileSystem.Data.FileExists(saveFile). Destructive New Game → inline confirm, not a second modal.
Admin/debug panel (toggle key): poke singletons directly — time/weather, grant, spawn, teleport. Build it early.
SCSS dialect notes
- Everything is a flex container by default — set
flex-direction: columnon stacks. - HUD root:
position: absolute; pointer-events: none;—pointer-events: allonly on clickable children (also makes the cursor usable over them). - Nesting +
&.modifier,linear-gradient,transition+&:hover,text-shadowover 3D. - Event handlers:
onclick=@Closeoronclick=@(() => Do(x)). Loop-variable capture: copy to a local (var idx = slot++;) before the lambda.
Namespace
@namespace YourGame on every razor — razor-namespace-trap.
Edit .razor with byte-safe tools only — PowerShell 5.1 mojibake destroys non-ASCII (emoji icons, etc.).
A single AnyModalOpen gate gives gameplay one pause signal. One owner per toggle key eliminates open/close races. Hashing UIState flags keeps Razor in sync with the router. Flex + pointer-events defaults match how the engine's UI tree actually hit-tests.