Editor/game component that simulates decorative machine-gun tracer fire for a trench map. It spawns pooled tracer effects, muzzle flash lights and delayed landing puffs, schedules bursts and shots, and draws editor gizmos and handle objects for start/end lines.
using Sandbox;
using System;
/// <summary>
/// Decorative machine-gun fire for the trench map: bursts of glowing tracers fly from a random
/// point on the Start line to a random point on the End line, with a muzzle flash where they leave
/// and a dust puff where they land, and a distant, softened shot sound for every round. Purely a
/// show: nothing is traced against players or balls, nothing takes damage, nothing is networked.
/// Every machine runs its own with its own random timing.
///
/// The two lines are each two child objects (Start A/B, End A/B) that you move with the normal
/// move gizmo; they are created the first time the component is seen in the editor if missing.
/// The lines and the handles are drawn as gizmos. The component's own Enabled checkbox turns the
/// gun off. Small pool of tracers and flash lights, nothing allocated per shot but the engine's
/// own sound handle and the surface's impact particle.
/// </summary>
[Title( "Trench Fire" ), Category( "Golf" ), Icon( "whatshot" )]
public sealed class TrenchFire : Component
{
/// <summary>One end of the line the tracers leave from.</summary>
[Property, Group( "Lines" ), Title( "Start A" )]
public GameObject StartA { get; set; }
/// <summary>The other end of the line the tracers leave from.</summary>
[Property, Group( "Lines" ), Title( "Start B" )]
public GameObject StartB { get; set; }
/// <summary>One end of the line the tracers land on.</summary>
[Property, Group( "Lines" ), Title( "End A" )]
public GameObject EndA { get; set; }
/// <summary>The other end of the line the tracers land on.</summary>
[Property, Group( "Lines" ), Title( "End B" )]
public GameObject EndB { get; set; }
/// <summary>Random scatter around the landing point, in units.</summary>
[Property, Group( "Lines" ), Range( 0f, 300f )]
public float Spread { get; set; } = 40f;
/// <summary>Bursts per minute.</summary>
[Property, Group( "Timing" ), Range( 0.1f, 60f ), Title( "Rate (bursts per minute)" )]
public float Rate { get; set; } = 6f;
/// <summary>Fewest shots in a burst.</summary>
[Property, Group( "Timing" ), Range( 1, 60 ), Title( "Burst Length Min" )]
public int BurstMin { get; set; } = 8;
/// <summary>Most shots in a burst.</summary>
[Property, Group( "Timing" ), Range( 1, 60 ), Title( "Burst Length Max" )]
public int BurstMax { get; set; } = 15;
/// <summary>Shots per second inside a burst.</summary>
[Property, Group( "Timing" ), Range( 1f, 30f )]
public float ShotsPerSecond { get; set; } = 12f;
/// <summary>How much the pause between bursts varies, 0 for a metronome, 1 for anything from no pause to double.</summary>
[Property, Group( "Timing" ), Range( 0f, 1f )]
public float PauseVariance { get; set; } = 0.5f;
/// <summary>How fast a tracer flies, units per second.</summary>
[Property, Group( "Tracer" ), Range( 500f, 20000f )]
public float TracerSpeed { get; set; } = 6000f;
/// <summary>Colour of the tracer streak, the flash and its light.</summary>
[Property, Group( "Tracer" )]
public Color TracerColour { get; set; } = new Color( 1f, 0.55f, 0.15f );
/// <summary>Length of the streak, in units.</summary>
[Property, Group( "Tracer" ), Range( 10f, 1000f )]
public float TracerLength { get; set; } = 160f;
/// <summary>Width of the streak at its bright end, in units.</summary>
[Property, Group( "Tracer" ), Range( 0.5f, 10f )]
public float TracerWidth { get; set; } = 2.5f;
/// <summary>How much brighter than the colour the streak glows.</summary>
[Property, Group( "Tracer" ), Range( 1f, 8f )]
public float Glow { get; set; } = 3f;
/// <summary>A short flash of light at the start point for every shot.</summary>
[Property, Group( "Effects" )]
public bool MuzzleFlash { get; set; } = true;
/// <summary>The surface's bullet impact particle where a tracer lands.</summary>
[Property, Group( "Effects" )]
public bool DustPuff { get; set; } = true;
/// <summary>The distant shot, played at the start point for every round. Empty uses sounds/trench_fire.</summary>
[Property, Group( "Sound" )]
public SoundEvent ShotSound { get; set; }
/// <summary>Volume of the shots, 0 to 1.</summary>
[Property, Group( "Sound" ), Range( 0f, 1f )]
public float Volume { get; set; } = 0.35f;
const string DefaultSound = "sounds/trench_fire.sound";
const float FlashSeconds = 0.05f;
const float FlashRadius = 160f;
struct Landing
{
public float At; // Time.Now the tracer arrives; 0 for none pending
public Vector3 Position;
public Vector3 Normal;
public Surface Surface;
}
TracerEffect[] _tracers = Array.Empty<TracerEffect>();
Landing[] _landings = Array.Empty<Landing>();
PointLight[] _flashes = Array.Empty<PointLight>();
float[] _flashOff = Array.Empty<float>();
int _nextTracer;
int _nextFlash;
Surface _defaultSurface;
Color _builtColour;
float _builtWidth;
float _nextBurstAt;
float _nextShotAt;
int _shotsLeft;
protected override void OnStart()
{
EnsureHandles();
ShotSound ??= ResourceLibrary.Get<SoundEvent>( DefaultSound );
_defaultSurface = Surface.FindByName( "default" );
BuildPool();
_nextBurstAt = Time.Now + NextPause() * Game.Random.Float( 0f, 1f );
}
protected override void OnDisabled()
{
_shotsLeft = 0;
for ( var i = 0; i < _landings.Length; i++ ) _landings[i].At = 0f;
foreach ( var flash in _flashes ) if ( flash.IsValid() ) flash.Enabled = false;
}
protected override void OnUpdate()
{
if ( Scene.IsEditor ) return;
if ( _tracers.Length == 0 ) return;
var now = Time.Now;
// Flashes go out, landings puff.
for ( var i = 0; i < _flashes.Length; i++ )
{
if ( _flashOff[i] > 0f && now >= _flashOff[i] )
{
_flashOff[i] = 0f;
if ( _flashes[i].IsValid() ) _flashes[i].Enabled = false;
}
}
for ( var i = 0; i < _landings.Length; i++ )
{
if ( _landings[i].At <= 0f || now < _landings[i].At ) continue;
_landings[i].At = 0f;
if ( DustPuff )
BaseCombatWeapon.ImpactPrefab( null, _landings[i].Surface, _landings[i].Position, _landings[i].Normal );
}
// Bursts.
if ( _shotsLeft <= 0 )
{
if ( now < _nextBurstAt ) return;
_shotsLeft = Game.Random.Int( Math.Min( BurstMin, BurstMax ), Math.Max( BurstMin, BurstMax ) );
_nextShotAt = now;
_nextBurstAt = now + NextPause();
}
if ( now < _nextShotAt ) return;
_nextShotAt = now + 1f / Math.Max( 1f, ShotsPerSecond );
_shotsLeft--;
Fire();
}
float NextPause()
{
var mean = 60f / Math.Max( 0.1f, Rate );
return mean * (1f + Game.Random.Float( -PauseVariance, PauseVariance ));
}
void Fire()
{
if ( !StartA.IsValid() || !StartB.IsValid() || !EndA.IsValid() || !EndB.IsValid() ) return;
var start = Vector3.Lerp( StartA.WorldPosition, StartB.WorldPosition, Game.Random.Float( 0f, 1f ) );
var end = Vector3.Lerp( EndA.WorldPosition, EndB.WorldPosition, Game.Random.Float( 0f, 1f ) )
+ Vector3.Random * Spread;
// Where it lands: the ground near the end point, for the puff's surface and facing.
var landing = new Landing { Position = end, Normal = Vector3.Up, Surface = _defaultSurface };
var trace = Scene.Trace.Ray( end + Vector3.Up * 48f, end + Vector3.Down * 96f ).WithoutTags( "player", "golfball" ).Run();
if ( trace.Hit )
{
landing.Position = trace.HitPosition;
landing.Normal = trace.Normal;
landing.Surface = trace.Surface ?? _defaultSurface;
}
var distance = start.Distance( end );
landing.At = Time.Now + distance / Math.Max( 1f, TracerSpeed );
// Tracer: the pool's oldest one restarts from the new start point.
var i = _nextTracer;
_nextTracer = (_nextTracer + 1) % _tracers.Length;
var tracer = _tracers[i];
if ( tracer.IsValid() )
{
if ( _builtColour != TracerColour || _builtWidth != TracerWidth ) BuildLook();
tracer.Enabled = false;
tracer.WorldPosition = start;
tracer.EndPoint = end;
tracer.DistancePerSecond = TracerSpeed;
tracer.Length = TracerLength;
tracer.Enabled = true;
}
_landings[i] = landing;
// Muzzle flash.
if ( MuzzleFlash && _flashes.Length > 0 )
{
var f = _nextFlash;
_nextFlash = (_nextFlash + 1) % _flashes.Length;
var flash = _flashes[f];
if ( flash.IsValid() )
{
flash.WorldPosition = start + (end - start).Normal * 12f;
flash.LightColor = TracerColour * Glow;
flash.Radius = FlashRadius;
flash.Enabled = true;
_flashOff[f] = Time.Now + FlashSeconds;
}
}
// The distant shot.
if ( ShotSound is not null && Volume > 0f )
{
var handle = Sound.Play( ShotSound, start );
if ( handle.IsValid() ) handle.Volume = Volume;
}
}
/// <summary>
/// Enough tracers to cover the longest flight at the burst rate, and a couple of flash lights.
/// Pool objects are hidden children that are never saved or networked.
/// </summary>
void BuildPool()
{
var longest = 0f;
if ( StartA.IsValid() && StartB.IsValid() && EndA.IsValid() && EndB.IsValid() )
{
longest = Math.Max( Math.Max( StartA.WorldPosition.Distance( EndA.WorldPosition ), StartA.WorldPosition.Distance( EndB.WorldPosition ) ),
Math.Max( StartB.WorldPosition.Distance( EndA.WorldPosition ), StartB.WorldPosition.Distance( EndB.WorldPosition ) ) );
}
var flight = (longest + Spread + TracerLength) / Math.Max( 1f, TracerSpeed );
var count = Math.Clamp( (int)MathF.Ceiling( flight * ShotsPerSecond ) + 2, 4, 32 );
_tracers = new TracerEffect[count];
_landings = new Landing[count];
for ( var i = 0; i < count; i++ )
{
var go = new GameObject( GameObject, false, $"tracer {i}" );
go.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;
var tracer = go.AddComponent<TracerEffect>( false );
tracer.StartDistance = 0f;
tracer.Opaque = false;
tracer.CastShadows = false;
tracer.EnableLight = false;
_tracers[i] = tracer;
go.Enabled = true;
}
BuildLook();
_flashes = new PointLight[2];
_flashOff = new float[2];
for ( var i = 0; i < _flashes.Length; i++ )
{
var go = new GameObject( GameObject, false, $"flash {i}" );
go.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;
var light = go.AddComponent<PointLight>( false );
light.Shadows = false;
light.Radius = FlashRadius;
light.Attenuation = 2f;
_flashes[i] = light;
go.Enabled = true;
}
}
void BuildLook()
{
_builtColour = TracerColour;
_builtWidth = TracerWidth;
var head = TracerColour * Glow;
head.a = 1f;
var colour = new Gradient( new Gradient.ColorFrame( 0f, head ), new Gradient.ColorFrame( 0.35f, TracerColour ), new Gradient.ColorFrame( 1f, TracerColour.WithAlpha( 0f ) ) );
var width = new Curve( new Curve.Frame( 0f, TracerWidth ), new Curve.Frame( 1f, 0f ) );
foreach ( var tracer in _tracers )
{
if ( !tracer.IsValid() ) continue;
tracer.LineColor = colour;
tracer.LineWidth = width;
}
}
/// <summary>
/// The four handle objects: found by name under this object, or made with default spots.
/// </summary>
void EnsureHandles()
{
StartA = Handle( StartA, "Start A", new Vector3( 0f, -150f, 40f ) );
StartB = Handle( StartB, "Start B", new Vector3( 0f, 150f, 40f ) );
EndA = Handle( EndA, "End A", new Vector3( 1200f, -250f, 0f ) );
EndB = Handle( EndB, "End B", new Vector3( 1200f, 250f, 0f ) );
}
GameObject Handle( GameObject current, string name, Vector3 localPosition )
{
if ( current.IsValid() ) return current;
foreach ( var child in GameObject.Children )
if ( child.IsValid() && child.Name == name ) return child;
var go = new GameObject( GameObject, true, name );
go.LocalPosition = localPosition;
return go;
}
protected override void DrawGizmos()
{
EnsureHandles();
if ( !StartA.IsValid() || !StartB.IsValid() || !EndA.IsValid() || !EndB.IsValid() ) return;
var alpha = Gizmo.IsSelected ? 1f : (Gizmo.IsHovered ? 0.6f : 0.3f);
var local = WorldTransform;
var sa = local.PointToLocal( StartA.WorldPosition );
var sb = local.PointToLocal( StartB.WorldPosition );
var ea = local.PointToLocal( EndA.WorldPosition );
var eb = local.PointToLocal( EndB.WorldPosition );
Gizmo.Draw.LineThickness = Gizmo.IsSelected ? 3f : 2f;
Gizmo.Draw.Color = TracerColour.WithAlpha( alpha );
Gizmo.Draw.Line( sa, sb );
Gizmo.Draw.LineSphere( new Sphere( sa, 8f ), 8 );
Gizmo.Draw.LineSphere( new Sphere( sb, 8f ), 8 );
Gizmo.Draw.Color = Color.Red.WithAlpha( alpha );
Gizmo.Draw.Line( ea, eb );
Gizmo.Draw.LineSphere( new Sphere( ea, 8f ), 8 );
Gizmo.Draw.LineSphere( new Sphere( eb, 8f ), 8 );
Gizmo.Draw.LineThickness = 1f;
Gizmo.Draw.Color = Color.White.WithAlpha( alpha * 0.7f );
Gizmo.Draw.Arrow( (sa + sb) * 0.5f, (ea + eb) * 0.5f, 24f, 8f );
}
}