Conveyance/FadeAndDestroy.cs
using Sandbox;

/// <summary>
/// Fades a GameObject's renderers out over time and then destroys it.
/// Drop this on bullets, props, or anything the level spawns that shouldn't live forever.
/// Works with ModelRenderer and SkinnedModelRenderer automatically.
/// </summary>
[Title( "Fade And Destroy" )]
[Category( "Game / Utility" )]
public sealed class FadeAndDestroy : Component
{
	/// <summary>Total lifetime before the fade begins.</summary>
	[Property, Range( 0f, 120f )]
	public float Lifetime { get; set; } = 30f;

	/// <summary>How long the fade-out takes once Lifetime is reached.</summary>
	[Property, Range( 0f, 5f )]
	public float FadeDuration { get; set; } = 1.5f;

	/// <summary>If true, disables Collider components at fade-start so bullets/props stop
	/// interacting with the world while still fading visually.</summary>
	[Property]
	public bool DisableCollidersOnFade { get; set; } = true;

	// ── internal state ─────────────────────────────────────────────────────

	private float _age;
	private bool  _fading;

	// Cached so we're not searching the hierarchy every frame
	private ModelRenderer[]        _renderers;
	private SkinnedModelRenderer[] _skinnedRenderers;

	// ── lifecycle ──────────────────────────────────────────────────────────

	protected override void OnAwake()
	{
		_renderers        = Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ).ToArray();
		_skinnedRenderers = Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ).ToArray();
	}

	protected override void OnUpdate()
	{
		_age += Time.Delta;

		if ( !_fading && _age >= Lifetime )
		{
			StartFade();
		}

		if ( _fading )
		{
			float safeDuration = FadeDuration > 0.001f ? FadeDuration : 0.001f;
			float t            = (_age - Lifetime) / safeDuration;
			float alpha        = 1f - t.Clamp( 0f, 1f );

			SetRendererAlpha( alpha );

			if ( t >= 1f )
			{
				GameObject.Destroy();
			}
		}
	}

	// ── helpers ────────────────────────────────────────────────────────────

	private void StartFade()
	{
		_fading = true;

		if ( DisableCollidersOnFade )
		{
			foreach ( var col in Components.GetAll<Collider>( FindMode.EverythingInSelfAndDescendants ) )
				col.Enabled = false;
		}
	}

	private void SetRendererAlpha( float alpha )
	{
		foreach ( var r in _renderers )
			r.Tint = r.Tint.WithAlpha( alpha );

		foreach ( var r in _skinnedRenderers )
			r.Tint = r.Tint.WithAlpha( alpha );
	}

	// ── public helpers ─────────────────────────────────────────────────────

	/// <summary>Reset the lifetime clock — e.g. if a bullet gets deflected.</summary>
	public void ResetLifetime()
	{
		_age    = 0f;
		_fading = false;
		SetRendererAlpha( 1f );
	}

	/// <summary>Trigger an immediate fade regardless of Lifetime.</summary>
	public void ForceStartFade()
	{
		_age    = Lifetime;
		_fading = false; // OnUpdate calls StartFade cleanly next frame
	}
}