A UI widget that renders a 180-degree compass strip with labeled cardinal ticks. CompassView builds the clipped tick strip and backing card/top bar; CompassWidget is a panel that updates heading from camera or via SetHeading and places the compass at the top-center of the screen.
using System;
using Goo;
using Sandbox;
using Sandbox.UI;
namespace Goo.FpsUI;
// Dock style for the compass: a full-width bar flush to the top edge, or a floating rounded card.
public enum CompassDockMode { TopBar, Card }
// Stateless presenter: a clipped heading strip with cardinal ticks positioned by angular delta.
static class CompassView
{
const float StripW = 360f, Fov = 180f; // 180-degree window across the strip
public const float StripH = 28f; // strip / top-dock bar height (others offset under it)
const float PxPerDeg = StripW / Fov;
static readonly (float Deg, string Label)[] Marks =
{
(0, "N"), (45, ""), (90, "E"), (135, ""), (180, "S"), (225, ""), (270, "W"), (315, ""),
};
// Both docks return the same bounded StripW band, only the backing/corners differ. The caller
// anchors it top-center: TopBar with no margin (flush to the edge), Card with the screen margin.
public static Container Build( CompassModel m, FpsTheme t, CompassDockMode dock = CompassDockMode.Card )
{
var band = Band( m, t );
// TopBar: solid backing, square corners, sits flush against the top edge.
if ( dock == CompassDockMode.TopBar )
return band with { BackgroundColor = t.BackingBg, BorderRadius = 0f };
// Card: lighter backing, rounded, floats below the top edge.
return band with { BackgroundColor = t.PanelBg, BorderRadius = t.Radius };
}
// The 180-degree tick band (transparent, clipped). Marks and the white center reference live here.
static Container Band( CompassModel m, FpsTheme t )
{
var strip = new Container
{
Key = "compass", Position = PositionMode.Relative,
Width = StripW, Height = StripH, Overflow = OverflowMode.Hidden,
};
foreach ( var mark in Marks )
{
float delta = CompassModel.ShortestDelta( m.Heading, mark.Deg );
if ( MathF.Abs( delta ) > Fov / 2f ) continue; // outside the window
float x = StripW / 2f + delta * PxPerDeg;
bool cardinal = mark.Label.Length > 0;
strip.Children.Add( new Container
{
Key = ((int)mark.Deg).ToString(),
Position = PositionMode.Absolute, Top = 0, Left = x - 10f, Width = 20f, Height = StripH,
JustifyContent = Justify.Center, AlignItems = Align.Center,
Children = { cardinal
? new Text( mark.Label ) { FontFamily = t.FontFamily, FontSize = 14f, FontWeight = t.WeightBold, FontColor = t.Ink }
: new Text( "|" ) { FontFamily = t.FontFamily, FontSize = 10f, FontColor = t.Dim } },
} );
}
// center reference marker, always white so it reads against any accent
strip.Children.Add( new Container
{
Key = "center", Position = PositionMode.Absolute, Top = 0, Left = StripW / 2f - 1f,
Width = 2f, Height = StripH, BackgroundColor = Color.White,
} );
return strip;
}
}
// Standalone compass. Call SetHeading(yawDeg) each frame from your player's view angle.
public sealed class CompassWidget : GooPanel<Container>
{
[Property] public bool FollowCamera { get; set; } = true; // follow the active camera's yaw (off = drive it via SetHeading)
[Property] public CompassDockMode Dock { get; set; } = CompassDockMode.TopBar; // top-edge bar or floating card
readonly CompassModel _m = new();
readonly FpsTheme _t = new();
public void SetHeading( float yawDeg ) => _m.SetHeading( yawDeg ); // override the heading manually (call each frame, set FollowCamera = false)
protected override bool Tick( float dt )
{
// Negate yaw: s&box yaw is CCW-positive (left), compass headings are CW-positive (N->E->S->W).
if ( FollowCamera && Scene?.Camera is { } cam ) _m.SetHeading( -cam.WorldRotation.Angles().yaw );
_m.Tick( dt );
return true; // heading typically changes every frame, the strip is cheap (plain rects)
}
protected override Container Build()
{
var root = Parts.Root( "fpsCompass" );
float inset = Dock == CompassDockMode.TopBar ? 0f : _t.Margin; // TopBar sits flush to the edge
root.Children.Add( Parts.Anchor( "a", Parts.Corner.TopCenter, inset, CompassView.Build( _m, _t, Dock ) ) );
return root;
}
}