Effects/ParticleLineEmitter.cs
/// <summary>
/// Emits particles along a line between two points.
/// Points can be defined as world positions or tracked GameObjects.
/// </summary>
[Title( "Line Emitter" )]
[Category( "Particles" )]
[Icon( "show_chart" )]
public sealed class ParticleLineEmitter : ParticleEmitter
{
public enum LineVelocityMode
{
/// <summary>No extra velocity applied.</summary>
None,
/// <summary>Velocity shoots outward from the line (radial).</summary>
Radial,
/// <summary>Velocity along the line direction (start → end).</summary>
Along,
/// <summary>Velocity along the line normal (perpendicular in the XY plane).</summary>
Normal,
}
[Header( "End Points" )]
/// <summary>If set, overrides StartPosition each frame.</summary>
[Property] public GameObject StartObject { get; set; }
/// <summary>If set, overrides EndPosition each frame.</summary>
[Property] public GameObject EndObject { get; set; }
[Property] public Vector3 StartPosition { get; set; } = Vector3.Zero;
[Property] public Vector3 EndPosition { get; set; } = new Vector3( 100, 0, 0 );
[Header( "Emission" )]
/// <summary>Spread radius around each emitted point perpendicular to the line.</summary>
[Property, Range( 0, 200 )] public float Radius { get; set; } = 0f;
[Header( "Velocity" )]
[Property] public LineVelocityMode VelocityMode { get; set; } = LineVelocityMode.None;
[Property, Range( -2000, 2000 )] public float Velocity { get; set; } = 100f;
protected override void DrawGizmos()
{
if ( !Gizmo.IsSelected ) return;
var start = StartObject.IsValid() ? StartObject.WorldPosition : WorldTransform.PointToWorld( StartPosition );
var end = EndObject.IsValid() ? EndObject.WorldPosition : WorldTransform.PointToWorld( EndPosition );
Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.8f );
Gizmo.Draw.Line( WorldTransform.PointToLocal( start ), WorldTransform.PointToLocal( end ) );
if ( Radius > 0f )
{
Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.15f );
var mid = WorldTransform.PointToLocal( Vector3.Lerp( start, end, 0.5f ) );
Gizmo.Draw.LineSphere( mid, Radius );
}
}
public override bool Emit( ParticleEffect target )
{
var start = StartObject.IsValid() ? StartObject.WorldPosition : WorldTransform.PointToWorld( StartPosition );
var end = EndObject.IsValid() ? EndObject.WorldPosition : WorldTransform.PointToWorld( EndPosition );
var t = System.Random.Shared.Float( 0f, 1f );
var pos = Vector3.Lerp( start, end, t );
// Perpendicular spread
if ( Radius > 0f )
{
var lineDir = (end - start).Normal;
var perp = lineDir.Cross( Vector3.Up );
if ( perp.LengthSquared < 0.001f )
perp = lineDir.Cross( Vector3.Forward );
perp = perp.Normal;
var spreadAngle = System.Random.Shared.Float( 0f, MathF.PI * 2f );
var spreadDist = System.Random.Shared.Float( 0f, Radius );
var biNormal = lineDir.Cross( perp ).Normal;
pos += (perp * MathF.Cos( spreadAngle ) + biNormal * MathF.Sin( spreadAngle )) * spreadDist;
}
var p = target.Emit( pos, Delta );
if ( VelocityMode != LineVelocityMode.None && Velocity != 0f )
{
var lineDir = (end - start).Normal;
Vector3 velDir = VelocityMode switch
{
LineVelocityMode.Radial => (pos - Vector3.Lerp( start, end, t )).Normal,
LineVelocityMode.Along => lineDir,
LineVelocityMode.Normal => lineDir.Cross( Vector3.Up ).Normal,
_ => Vector3.Zero,
};
p.Velocity += velDir * Velocity;
}
return true;
}
}