Game/ScorePopup.cs

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.

File Access
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>();
	}
}