A UI widget that draws a hitmarker X and presents it with a pop (scale+fade) animation. HitmarkerModel stores state and is driven each frame; Hit(kill:true) triggers a pop tinted by the theme accent for kills.
using Goo;
using Sandbox;
using Sandbox.UI;
using PanelTransform = Goo.PanelTransform;
namespace Goo.FpsUI;
// Stateless presenter: an X that pops (scale + fade) on a hit, accent-tinted on a kill.
static class HitmarkerView
{
const float Box = 28f, Len = 14f, Thick = 2f;
public static Container Build( HitmarkerModel m, FpsTheme t, Color? killColor = null )
{
float s = m.Scale;
Color color = m.Kill ? (killColor ?? t.Accent) : Color.White;
float scale = 0.6f + 0.5f * s;
var root = new Container
{
Key = "hit", Position = PositionMode.Relative, Width = Box, Height = Box,
Opacity = s, Transform = PanelTransform.Scale( scale ),
// Pin the scale pivot to center so the pop grows/shrinks in place (default pivot drifts it up-left).
TransformOriginX = Length.Percent( 50 ), TransformOriginY = Length.Percent( 50 ),
Children =
{
Diagonal( "d1", 45f, color ),
Diagonal( "d2", -45f, color ),
},
};
return root;
static Container Diagonal( string key, float deg, Color color ) => new()
{
Key = key, Position = PositionMode.Absolute,
Top = Box / 2f - Thick / 2f, Left = Box / 2f - Len / 2f,
Width = Len, Height = Thick, BackgroundColor = color,
// Rotate about the arm's own center so both arms cross at the box center (not their shared corner).
TransformOriginX = Length.Percent( 50 ), TransformOriginY = Length.Percent( 50 ),
Transform = PanelTransform.Rotate( deg ),
};
}
}
// Standalone hitmarker. Call Hit() / Hit(kill:true) when your shot connects.
public sealed partial class HitmarkerWidget : GooPanel<Container>
{
readonly HitmarkerModel _m = new();
readonly FpsTheme _t = new();
public void Hit( bool kill = false ) => _m.Pop( kill ); // register a hit (kill = accent tint)
// Demo-only seam: implemented in FpsDemo.cs, compiles out when that file is deleted.
partial void StepDemo( float dt, ref bool active );
protected override bool Tick( float dt )
{
bool demo = false;
StepDemo( dt, ref demo );
bool moving = _m.Tick( dt );
return demo || moving;
}
protected override Container Build()
{
var root = Parts.Root( "fpsHitmarker" );
root.Children.Add( Parts.Anchor( "a", Parts.Corner.Center, 0f, HitmarkerView.Build( _m, _t ) ) );
return root;
}
}