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
}
}