Demos/ComposableHud/ComposableHudUI.cs
using System.Collections.Generic;
using Goo;
using Sandbox;
using Sandbox.Compass;
using Sandbox.KeystrokeViz;
using Sandbox.RadialWheel;
using Sandbox.UI;
namespace Sandbox.ComposableHud;
// GooPanel HUD host built for composition. Owns a collection of HudElements; each frame it ticks every element, ORs their dirty flags, and rebuilds once.
// The three solo demos (compass, keystroke, radial) compose here with zero edits to their view classes via a single ViewElement delegate adapter.
// Lifecycle note: keystroke/radial need Reset() on enable, compass does not, so the host resets per-view. KeystrokeView must be placed at its own Anchor because it reads that value to pick its inner stacking direction (bd memory resolveanchor-positioning-container).
[Title( "Demo: Composable HUD" ), Category( "UI/Demo" ), Icon( "dashboard" )]
public sealed class ComposableHudUI : GooPanel<Container>
{
CompassView? _compass = new();
KeystrokeView? _keystroke = new() { Anchor = Layout.Anchor.BottomLeft };
RadialView? _radial = new();
SquadHealthView? _squad = new();
DamageIndicatorView? _damage = new();
HotbarView? _hotbar = new();
PlayerHealthView? _player = new();
List<HudElement>? _elements;
// Lazy, null-healed element list. s&box builds hotload instances via uninitialized-object + field migration, so constructors and field initializers do NOT run on hotload and a newly added field migrates as null (engine-fact-component-no-ctor-on-hotload). Building lazily and healing null view fields first keeps the host robust across hotloads.
List<HudElement> Elements()
{
_compass ??= new();
_keystroke ??= new() { Anchor = Layout.Anchor.BottomLeft };
_radial ??= new();
_squad ??= new();
_damage ??= new();
_hotbar ??= new();
_player ??= new();
return _elements ??= new()
{
new ViewElement { Anchor = Layout.Anchor.TopCenter, Key = "compass",
OnTick = _compass.Tick, OnBuild = _compass.Build },
new ViewElement { Anchor = _keystroke.Anchor, Padding = _keystroke.AnchorMargin, Key = "keystrokes",
OnTick = _keystroke.Tick, OnBuild = _keystroke.Build },
new ViewElement { Anchor = Layout.Anchor.Center, Key = "radial",
OnTick = _radial.Tick, OnBuild = _radial.Build },
new ViewElement { Anchor = Layout.Anchor.TopLeft, Padding = 16f, Key = "squad",
OnTick = _squad.Tick, OnBuild = _squad.Build },
new ViewElement { Anchor = Layout.Anchor.Center, Key = "dmg",
OnTick = _damage.Tick, OnBuild = _damage.Build },
new ViewElement { Anchor = Layout.Anchor.BottomCenter, Padding = 32f, Key = "hotbar",
OnTick = _hotbar.Tick, OnBuild = _hotbar.Build },
// Sits above the hotbar at bottom-center via larger bottom inset (uniform padding).
new ViewElement { Anchor = Layout.Anchor.BottomCenter, Padding = 110f, Key = "player-hp",
OnTick = _player.Tick, OnBuild = _player.Build },
};
}
protected override void OnEnabled()
{
base.OnEnabled();
Panel.Style.Width = Length.Percent( 100 );
Panel.Style.Height = Length.Percent( 100 );
Elements(); // ensure the view fields exist before resetting them
// Non-uniform lifecycle seam: only the stateful pieces reset on enable.
_keystroke!.Reset();
_radial!.Reset();
_squad!.Reset();
_damage!.Reset();
_hotbar!.Reset();
_player!.Reset();
}
protected override void OnUpdate()
{
bool dirty = false;
foreach ( var e in Elements() )
dirty |= e.Tick( Scene, Time.Delta );
if ( dirty ) Rebuild();
base.OnUpdate();
}
protected override Container Build()
{
var root = Hud.Overlay();
foreach ( var e in Elements() )
root.Children.Add( PlaceAt( e.Anchor, e.Build(), e.Padding, e.Key ) );
return root;
}
// Inline anchored placement, verbatim HudLayout.Anchored. The positioning cell uses FlexDirection.Column so ResolveAnchor's Justify/Align resolve against the vertical main axis (bd memory resolveanchor-positioning-container); the engine-default Row would render top anchors center-left.
static Container PlaceAt( Layout.Anchor anchor, Container content, float padding, string? key )
{
var r = Layout.ResolveAnchor( anchor );
return new Container
{
Key = key,
Position = PositionMode.Absolute,
Top = 0,
Left = 0,
Width = Length.Percent( 100 ),
Height = Length.Percent( 100 ),
Padding = padding,
PointerEvents = PointerEvents.None,
FlexDirection = FlexDirection.Column,
JustifyContent = r.JustifyContent,
AlignItems = r.AlignItems,
Children = { content },
};
}
}