UI component for a floating score popup. It creates a text label that rises, scales (pops) and fades over a Lifetime, then destroys its GameObject. Spawn(scene, worldPos, text, color, scale) constructs the GameObject and TextRenderer.
using Sandbox;
namespace Breakout;
/// <summary>
/// A little floating score number ("+100") that pops up when a brick breaks, rises, and fades out.
/// Call Spawn() to create one; the component animates itself and deletes itself when done.
/// </summary>
[Title( "Score Popup" ), Category( "Breakout" )]
public sealed class ScorePopup : Component
{
[Property] public float Lifetime { get; set; } = 0.65f;
[Property] public float RiseSpeed { get; set; } = 95f;
private TextRenderer label;
private TimeSince sinceSpawn;
private Color baseColor = Color.White;
private float baseScale = 0.25f;
protected override void OnEnabled()
{
label = Components.Get<TextRenderer>( FindMode.EverythingInSelfAndDescendants );
if ( label is not null )
{
baseColor = label.Color;
baseScale = label.Scale;
}
sinceSpawn = 0f;
}
protected override void OnUpdate()
{
// t runs from 0 up to 2 across the popup's life. It rises (quicker at first), pops the
// text a bit bigger for a moment, then fades to nothing. At t = 2 it's done and removes itself.
float t = Lifetime > 0f ? (sinceSpawn / Lifetime).Clamp( 0f, 2f ) : 2f;
if ( t >= 2f )
{
GameObject.Destroy();
return;
}
var pos = WorldPosition;
pos.z += RiseSpeed * (2f - t) * Time.Delta;
WorldPosition = pos;
if ( label is null )
return;
float pop = t < 0.2f ? 0.6f + 2f * t : 1f - 0.12f * (t - 0.2f);
label.Scale = baseScale * pop.Clamp( 0.4f, 1.3f ) * 1.5f;
label.Color = baseColor.WithAlpha( baseColor.a * (1f - t * t) );
}
/// <summary>
/// Creates a self-animating "+score" popup at the given world position.
/// </summary>
public static void Spawn( Scene scene, Vector3 worldPos, string text, Color color, float scale )
{
if ( scene is null || string.IsNullOrEmpty( text ) )
return;
var go = new GameObject( true, "score_popup" );
go.WorldPosition = worldPos + Vector3.Right * 50;
go.WorldRotation = Rotation.From( 0f, 90f, 0f );
var label = go.Components.Create<TextRenderer>();
label.Text = text;
label.Color = color;
label.FontSize = 88;
label.FontWeight = 800;
label.Scale = scale;
go.Components.Create<ScorePopup>();
}
}