Files
sbox-public/engine/Sandbox.Engine/Scene/Components/Particles/ParticleEffect.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

918 lines
25 KiB
C#

using System.Collections.Concurrent;
using System.Text.Json.Serialization;
using System.Threading;
namespace Sandbox;
/// <summary>
/// Defines and holds particles. This is the core of the particle system.
/// </summary>
[Expose]
[Title( "Particle Effect" )]
[Category( "Particles" )]
[Icon( "shower" )]
[EditorHandle( "materials/gizmo/particles.png" )]
public sealed partial class ParticleEffect : Component, Component.ExecuteInEditor, Component.ITemporaryEffect, Component.ITintable
{
/// <summary>
/// The maximum number of particles that can exist in this effect at once.
/// </summary>
[Property, Header( "Limits" )]
public int MaxParticles { get; set; } = 1000;
/// <summary>
/// The lifetime of each particle, in seconds.
/// </summary>
[Property]
public ParticleFloat Lifetime { get; set; } = 1.0f;
/// <summary>
/// Scales the simulation time for this effect.
/// </summary>
[Property, Header( "Time" ), Range( 0, 1 )]
public float TimeScale { get; set; } = 1.0f;
/// <summary>
/// How many seconds to pre-warm this effect by when creating.
/// </summary>
[Property, Range( 0, 1 )]
public float PreWarm { get; set; } = 0.0f;
/// <summary>
/// The delay before a particle starts after being emitted, in seconds.
/// </summary>
[Property]
public ParticleFloat StartDelay { get; set; } = 0.0f;
/// <summary>
/// Per-particle time scale multiplier. Allows each particle to have a unique simulation speed.
/// </summary>
[Property]
public ParticleFloat PerParticleTimeScale { get; set; } = 1.0f;
public enum TimingMode
{
/// <summary>
/// Use game simulation time (affected by game time scale).
/// </summary>
GameTime,
/// <summary>
/// Use real-world time (ignores game time scale).
/// </summary>
RealTime,
}
/// <summary>
/// How time is updated for this effect.
/// </summary>
[Property]
public TimingMode Timing { get; set; } = TimingMode.GameTime;
/// <summary>
/// The initial velocity of the particle when it is created. This is applied before any forces are applied.
/// </summary>
[Property, Feature( "Move" )]
public ParticleVector3 InitialVelocity { get; set; }
/// <summary>
/// Apply an element of random velocity to the particle when it is created, in a random direction.
/// </summary>
[Title( "Random Velocity" )]
[Property, Feature( "Move", Icon = "animation", Description = "The spatial properties of each particle" )]
public ParticleFloat StartVelocity { get; set; } = 0.0f;
/// <summary>
/// The damping factor applied to particle velocity over time.
/// This reduces the velocity of particles, simulating resistance or drag.
/// </summary>
[Property, Feature( "Move" )]
public ParticleFloat Damping { get; set; } = 0.0f;
/// <summary>
/// Move this delta constantly. Ignores velocity, collisions and drag.
/// </summary>
[Property, Feature( "Move" )]
public ParticleVector3 ConstantMovement { get; set; }
[Hide, JsonIgnore]
[Obsolete( "Use LocalSpace instead" )]
[Property, Feature( "Move" )]
public SimulationSpace Space { get; set; }
/// <summary>
/// When 1 particles will be moved in local space relative to the emitter GameObject's transform.
/// This allows particles to be emitted in a local space, like a fire effect that moves with the player, but the particles can slowly move to world space.
/// </summary>
[Property, Feature( "Move" )]
public ParticleFloat LocalSpace { get; set; } = 0.0f;
/// <summary>
/// Enables or disables rotation for particles.
/// </summary>
[Property, FeatureEnabled( "Rotation", Icon = "flip_camera_android", Description = "The rotation of the particles (not the emitter)" )]
public bool ApplyRotation { get; set; } = false;
/// <summary>
/// The pitch rotation of the particles.
/// </summary>
[Property, Feature( "Rotation" )]
public ParticleFloat Pitch { get; set; } = 0.0f;
/// <summary>
/// The yaw rotation of the particles.
/// </summary>
[Property, Feature( "Rotation" )]
public ParticleFloat Yaw { get; set; } = 0.0f;
/// <summary>
/// The roll rotation of the particles.
/// </summary>
[Property, Feature( "Rotation" )]
public ParticleFloat Roll { get; set; } = 0.0f;
/// <summary>
/// Enables or disables color application for particles.
/// </summary>
[Property, FeatureEnabled( "Color", Icon = "color_lens", Description = "The visual properties of each particle" )]
public bool ApplyColor { get; set; } = false;
/// <summary>
/// Enables or disables alpha application for particles.
/// </summary>
[Property, Feature( "Color" )]
public bool ApplyAlpha { get; set; } = false;
/// <summary>
/// The tint color applied to particles.
/// </summary>
[Property, Feature( "Color" )]
public Color Tint { get; set; } = Color.White;
/// <summary>
/// The gradient used to color particles over their lifetime.
/// </summary>
[Property, Feature( "Color" )]
public ParticleGradient Gradient { get; set; } = Color.White;
/// <summary>
/// The brightness multiplier applied to particles.
/// </summary>
[Property, Feature( "Color" )]
public ParticleFloat Brightness { get; set; } = 1.0f;
/// <summary>
/// The alpha transparency of particles.
/// </summary>
[Property, Feature( "Color" )]
public ParticleFloat Alpha { get; set; } = 1.0f;
/// <summary>
/// Enables or disables shape application for particles.
/// </summary>
[Property, FeatureEnabled( "Shape", Icon = "crop", Description = "The scale/size of each particle" )]
public bool ApplyShape { get; set; } = false;
/// <summary>
/// The scale of particles.
/// </summary>
[Property, Feature( "Shape" )]
public ParticleFloat Scale { get; set; } = 1.0f;
/// <summary>
/// The stretch factor of particles, affecting their aspect ratio.
/// </summary>
[Property, Feature( "Shape" )]
public ParticleFloat Stretch { get; set; } = 0.0f;
/// <summary>
/// Enables or disables the application of forces to particles.
/// </summary>
[Property, FeatureEnabled( "Force", Icon = "cloud_sync", Description = "A force that is applied to each particle in the effect" )]
public bool Force { get; set; }
/// <summary>
/// The direction of the force applied to particles.
/// </summary>
[Property, Feature( "Force" )]
public Vector3 ForceDirection { get; set; }
/// <summary>
/// The scale of the force applied to each particle.
/// This multiplier determines the intensity of the force applied to particles.
/// </summary>
[Property, Feature( "Force" )]
public ParticleFloat ForceScale { get; set; } = 1.0f;
/// <summary>
/// The orbital force applied to particles, causing them to rotate around a point.
/// </summary>
[Property, Feature( "Force" )]
public ParticleVector3 OrbitalForce { get; set; }
/// <summary>
/// The pull strength of the orbital force, drawing particles closer to the center.
/// </summary>
[Property, Feature( "Force" )]
public ParticleFloat OrbitalPull { get; set; }
/// <summary>
/// The simulation space in which forces are applied.
/// Forces can be applied in either local space (relative to the emitter) or world space.
/// </summary>
[Title( "Space" )]
[Property, Feature( "Force" )]
public SimulationSpace ForceSpace { get; set; }
/// <summary>
/// Enables or disables collision behavior for particles.
/// </summary>
[Property, FeatureEnabled( "Collision", Icon = "file_download", Description = "What should happen when particles collide with the world" )]
public bool Collision { get; set; }
/// <summary>
/// The chance that a particle will die upon collision.
/// </summary>
[Property, Feature( "Collision" )]
public ParticleFloat DieOnCollisionChance { get; set; } = 0.0f;
/// <summary>
/// The radius used for collision detection.
/// </summary>
[Property, Feature( "Collision" )]
public float CollisionRadius { get; set; } = 1.0f;
/// <summary>
/// The set of tags to ignore during collision detection.
/// </summary>
[Property, Feature( "Collision" )]
public TagSet CollisionIgnore { get; set; }
/// <summary>
/// The bounce factor applied to particles upon collision.
/// </summary>
[Property, Feature( "Collision" )]
public ParticleFloat Bounce { get; set; } = 1.0f;
/// <summary>
/// The friction factor applied to particles upon collision.
/// </summary>
[Property, Feature( "Collision" )]
public ParticleFloat Friction { get; set; } = 1.0f;
/// <summary>
/// The bumpiness factor applied to particles upon collision.
/// </summary>
[Property, Feature( "Collision" )]
public ParticleFloat Bumpiness { get; set; } = 0.0f;
/// <summary>
/// The strength of the push force applied to particles upon collision.
/// </summary>
[Property, Feature( "Collision" )]
public ParticleFloat PushStrength { get; set; } = 0.0f;
/// <summary>
/// Enables or disables the use of a sheet sequence for particles.
/// </summary>
[Title( "Sheet" )]
[Property, FeatureEnabled( "SheetSequence", Icon = "apps" )]
public bool SheetSequence { get; set; }
/// <summary>
/// Which sequence to use.
/// </summary>
[Property, Feature( "SheetSequence" )]
public ParticleFloat SequenceId { get; set; } = 0.0f;
/// <summary>
/// Allows control of the sequence time, which spans from 0 to 1 for one loop.
/// </summary>
[Property, Feature( "SheetSequence" ), Range( 0, 1 )]
public ParticleFloat SequenceTime { get; set; } = 1.0f;
/// <summary>
/// Increment the sequence time by this much.
/// </summary>
[Property, Feature( "SheetSequence" )]
public ParticleFloat SequenceSpeed { get; set; } = 1.0f;
/// <summary>
/// Enables or disables the use of prefabs for particles.
/// </summary>
[Property, FeatureEnabled( "Prefab", Icon = "widgets", Description = "Attach a prefab to a particle" )]
public bool UsePrefabFeature { get; set; } = false;
/// <summary>
/// Will choose a random prefab to spawn from this list.
/// </summary>
[Header( "Follower" )]
[Property, Feature( "Prefab" )]
public List<GameObject> FollowerPrefab { get; set; }
/// <summary>
/// If 1 then we'll always spawn a prefab. If 0.5 then we'll spawn a prefab 50% of the time.
/// </summary>
[Property, Feature( "Prefab" ), Title( "Spawn Chance" )]
public ParticleFloat FollowerPrefabChance { get; set; } = 1;
/// <summary>
/// When true the prefab will be destroyed at the end of the particle's life.
/// </summary>
[Property, Feature( "Prefab" ), Title( "Kill on death" )]
public bool FollowerPrefabKill { get; set; } = true;
/// <summary>
/// Will choose a random prefab to spawn from this list.
/// </summary>
[Header( "On Collision" )]
[Property, Feature( "Prefab" )]
public List<GameObject> CollisionPrefab { get; set; }
/// <summary>
/// Will choose a random prefab to spawn from this list.
/// </summary>
[Property, Feature( "Prefab" ), Title( "Align With Surface" )]
public bool CollisionPrefabAlign { get; set; }
/// <summary>
/// We will by default align to the particle's angle, but we can also randomize that.
/// </summary>
[Property, Feature( "Prefab" ), Title( "Rotation" ), ShowIf( "CollisionPrefabAlign", true )]
public ParticleFloat CollisionPrefabRotation { get; set; }
/// <summary>
/// If 1 then we'll always spawn a prefab. If 0.5 then we'll spawn a prefab 50% of the time.
/// </summary>
[Property, Feature( "Prefab" ), Title( "Spawn Chance" )]
public ParticleFloat CollisionPrefabChance { get; set; } = 1;
/// <summary>
/// Called any time a particle is destroyed.
/// </summary>
[Property, Header( "Actions" )]
public Action<Particle> OnParticleDestroyed { get; set; }
/// <summary>
/// Called any time a particle is created.
/// </summary>
[Property]
public Action<Particle> OnParticleCreated { get; set; }
/// <summary>
/// Active particles in the effect.
/// Active particles are those currently being simulated and rendered.
/// </summary>
public List<Particle> Particles { get; } = new List<Particle>();
/// <summary>
/// Delayed particles in the effect.
/// Delayed particles are those that have been emitted but are waiting to be activated based on their start delay.
/// </summary>
public List<Particle> DelayedParticles { get; } = new List<Particle>();
/// <summary>
/// The total number of particles in the effect, including both active and delayed particles.
/// </summary>
public int ParticleCount => Particles.Count + DelayedParticles.Count;
/// <summary>
/// Whether the particle effect has reached its maximum capacity.
/// This is determined by comparing the total particle count to the <see cref="MaxParticles"/> property.
/// </summary>
public bool IsFull => ParticleCount >= MaxParticles;
/// <summary>
/// Whether the particle simulation is currently paused.
/// When paused, particles will not update their positions, velocities, or other properties.
/// </summary>
[JsonIgnore]
public bool Paused { get; set; }
Transform lastTransform;
ConcurrentQueue<Particle> deleteList = new ConcurrentQueue<Particle>();
/// <summary>
/// Called before the particles are stepped.
/// This allows custom logic to be executed before the simulation advances.
/// </summary>
public Action<float> OnPreStep { get; set; }
/// <summary>
/// Called after the particles are stepped.
/// This allows custom logic to be executed after the simulation advances.
/// </summary>
public Action<float> OnPostStep { get; set; }
/// <summary>
/// Called after each particle is stepped.
/// This provides an opportunity to modify individual particles during the simulation.
/// </summary>
public Action<Particle, float> OnStep { get; set; }
/// <summary>
/// The bounding box that encompasses all active particles.
/// This is useful for determining the spatial extent of the particle effect.
/// </summary>
public BBox ParticleBounds { get; internal set; }
/// <summary>
/// The size of the largest particle in the effect.
/// This is determined by the maximum scale of any particle along its x, y, or z axis.
/// </summary>
public float MaxParticleSize { get; internal set; }
Color ITintable.Color { get => Tint; set => Tint = value; }
List<GameObject> _spawnedGameObjects = new();
public enum SimulationSpace
{
/// <summary>
/// Forces are applied in world space, independent of the emitter's position or rotation.
/// </summary>
World,
/// <summary>
/// Forces are applied in local space, relative to the emitter's position and rotation.
/// </summary>
Local
}
bool isWarmed;
protected override void OnEnabled()
{
base.OnEnabled();
isWarmed = false;
lastTransform = WorldTransform;
}
protected override void OnDisabled()
{
Clear();
}
public void Clear()
{
foreach ( var p in Particles.ToArray() )
{
Terminate( p );
}
Particles.Clear();
DelayedParticles.Clear();
foreach ( var go in _spawnedGameObjects )
{
if ( !go.IsValid() ) continue;
go.Destroy();
}
_spawnedGameObjects.Clear();
}
public void ResetEmitters()
{
Components.ExecuteEnabledInSelfAndDescendants<ParticleEmitter>( e => e.ResetEmitter() );
}
long _maxDistance;
long _maxSize;
float _timeDelta;
SceneTrace _trace;
bool _parentMoved;
Transform _worldTx;
Vector3 _worldForce;
internal void UpdateParticle( int index )
{
var p = Particles[index];
// keep updating deathtime, incase we're in the editor and they're changing shit
p.DeathTime = p.BornTime + Lifetime.Evaluate( p.Rand( 155, 100 ), p.Rand( 145, 100 ) );
float delta = MathX.Remap( p.BornTime + p.Age, p.BornTime, p.DeathTime );
p.LifeDelta = delta;
var timeScale = PerParticleTimeScale.Evaluate( p, 3355 ) * _timeDelta * p.TimeScale;
var frame = p.Frame;
p.Age += timeScale;
p.Frame++;
// delay - not spawned yet (BornTime is in the future)
if ( p.LifeDelta < 0 )
return;
var damping = Damping.Evaluate( p, 8234 );
var forceScale = ForceScale.Evaluate( p, 7723 );
var localSpace = LocalSpace.Evaluate( p, 254 ).Clamp( 0, 1 );
if ( _parentMoved && frame > 0 && localSpace > 0.001f )
{
var localPos = lastTransform.PointToLocal( p.Position );
var worldPos = _worldTx.PointToWorld( localPos );
p.Position = p.Position.LerpTo( worldPos, localSpace );
}
p.ApplyDamping( damping * timeScale );
OnStep?.Invoke( p, p.LifeDelta );
if ( Force && forceScale != 0.0f )
{
if ( !ForceDirection.IsNearlyZero() )
{
p.Velocity += forceScale * (ForceSpace == SimulationSpace.Local ? _worldForce : ForceDirection) * timeScale;
}
if ( !OrbitalForce.IsNearlyZero() )
{
var force = OrbitalForce.Evaluate( delta, p.Rand( 8363 ), p.Rand( 5216 ), p.Rand( 2323 ) );
var localOffset = (_worldTx.Position - p.Position).Normal;
var rotatedOffset = localOffset.RotateAround( 0, new Angles( force ) );
var rotDelta = localOffset - rotatedOffset;
p.Velocity += forceScale * rotDelta * timeScale;
}
if ( !OrbitalPull.IsNearlyZero() )
{
var localOffset = (_worldTx.Position - p.Position) / 100.0f;
p.Velocity += forceScale * localOffset * timeScale * OrbitalPull.Evaluate( delta, p.Rand( 4333 ) );
}
}
// Apply constant movement
if ( !ConstantMovement.IsNearlyZero() )
{
p.Position += ConstantMovement.Evaluate( p, 4395 ) * _timeDelta;
}
if ( Collision )
{
var bounce = Bounce.Evaluate( p, 3478 );
var friction = Friction.Evaluate( p, 7579 );
var bumpiness = Bumpiness.Evaluate( p, 2380 );
var push = PushStrength.Evaluate( p, 5281 );
var die = DieOnCollisionChance.Evaluate( p, 4582 ) > 0.5f;
var radius = MathF.Max( 0.01f, CollisionRadius );
if ( Scene.IsEditor ) push = 0;
var hitTime = Time.Now - p.HitTime;
var collided = p.MoveWithCollision( bounce, friction, bumpiness, push, die, timeScale, radius, _trace );
if ( collided && hitTime > 0.3f && UsePrefabFeature && CollisionPrefabChance.Evaluate( delta, Random.Shared.Float( 0, 1 ) ) > Random.Shared.Float( 0, 1 ) )
{
var prefabSource = Random.Shared.FromList( CollisionPrefab );
if ( prefabSource is not null )
{
Rotation angle = p.Angles;
Vector3 position = p.Position;
if ( CollisionPrefabAlign )
{
angle = Rotation.LookAt( p.HitNormal, p.Angles.Forward );
var rot = CollisionPrefabRotation.Evaluate( delta, Random.Shared.Float( 0, 1 ) );
angle = Rotation.FromYaw( rot ) * angle;
position = p.HitPos;
}
// Queue the collision prefabs to spawn on the main thread
ParticleCollisionPrefabs.Add( new( prefabSource, position, angle ) );
}
}
}
else
{
p.Position += p.Velocity * timeScale;
}
if ( ApplyColor )
{
var brightness = Brightness.Evaluate( p, 4626 );
p.Color = Tint * Gradient.Evaluate( p, 8752 ); // TODO, gradient, between two gradients etc
p.Color *= new Color( brightness, 1.0f );
}
if ( ApplyAlpha )
{
p.Alpha = Alpha.Evaluate( p, 8525 );
}
if ( ApplyShape )
{
p.Size = Scale.Evaluate( p, 6211 );
var aspect = Stretch.Evaluate( p, 62415 );
if ( aspect < 0 )
{
p.Size.x *= aspect.Remap( 0, -1, 1, 2, false );
}
else if ( aspect > 0 )
{
p.Size.y *= aspect.Remap( 0, 1, 1, 2, false );
}
}
if ( ApplyRotation )
{
p.Angles.pitch = Pitch.Evaluate( p, 2363 );
p.Angles.yaw = Yaw.Evaluate( p, 8762 );
p.Angles.roll = Roll.Evaluate( p, 3675 );
}
if ( SheetSequence )
{
p.SequenceTime.x = SequenceTime.Evaluate( p, 7234 );
p.SequenceTime.y += SequenceSpeed.Evaluate( p, 1351 ) * timeScale;
p.Sequence = (int)SequenceId.Evaluate( p, 1051 );
}
if ( delta >= 1.0f )
{
deleteList.Enqueue( p );
if ( p.hasUpdated )
{
p.hasUpdated = false;
p.OnDisabled();
}
return;
}
if ( !p.hasUpdated )
{
p.hasUpdated = true;
p.OnEnabled();
}
p.OnUpdate( delta );
if ( p.Follower.IsValid() )
{
p.Follower.WorldTransform = new Transform( p.Position, p.Angles, p.Follower.WorldScale );
}
var distanceFromOrigin = (long)Vector3.DistanceBetween( _worldTx.Position, p.Position );
if ( Interlocked.Read( ref _maxDistance ) <= distanceFromOrigin )
{
Interlocked.Exchange( ref _maxDistance, distanceFromOrigin );
}
var size = (long)float.Max( p.Size.x, p.Size.y );
if ( Interlocked.Read( ref _maxSize ) <= size )
{
Interlocked.Exchange( ref _maxSize, size );
}
}
public void Step( float timeDelta )
{
PreStep( timeDelta );
System.Threading.Tasks.Parallel.For( 0, Particles.Count, UpdateParticle );
PostStep();
}
internal readonly record struct ParticleWork( ParticleEffect effect, int startIndex, int endIndex );
internal void CollectWork( List<ParticleWork> work )
{
int count = Particles.Count;
int chunkSize = 16;
for ( int i = 0; i < count; i += chunkSize )
{
work.Add( new ParticleWork( this, i, Math.Min( i + chunkSize, count ) ) );
}
}
internal void TryPreWarm()
{
if ( isWarmed ) return;
isWarmed = true;
float timeStep = 0.2f;
if ( PreWarm < timeStep ) timeStep = PreWarm;
for ( float i = 0; i < PreWarm; i += timeStep )
{
Step( timeStep );
}
}
internal void PreStep( float timeDelta )
{
_maxDistance = 15;
_maxSize = 1;
_timeDelta = Paused ? 0.0f : timeDelta * TimeScale;
_worldTx = WorldTransform;
if ( ForceSpace == SimulationSpace.Local )
_worldForce = _worldTx.Rotation * ForceDirection;
Vector3 lastPos = lastTransform.Position;
Transform deltaTransform = _worldTx.ToLocal( lastTransform );
_parentMoved = deltaTransform != global::Transform.Zero;
OnPreStep?.Invoke( _timeDelta );
_trace = Scene.Trace.WithoutTags( CollisionIgnore );
RunDelayedParticles();
}
internal void PostStep()
{
while ( deleteList.TryDequeue( out var delete ) )
{
Terminate( delete );
}
ParticleBounds = BBox.FromPositionAndSize( _worldTx.Position, _maxDistance * 2.0f );
MaxParticleSize = _maxSize;
OnPostStep?.Invoke( _timeDelta );
lastTransform = WorldTransform;
}
[Obsolete( "Pass in a delta" )]
public Particle Emit( Vector3 position )
{
return Emit( position, Random.Shared.Float( 0, 1 ) );
}
void RunDelayedParticles()
{
for ( int i = DelayedParticles.Count - 1; i >= 0; i-- )
{
if ( DelayedParticles[i].BornTime > Time.Now ) continue;
var p = DelayedParticles[i];
Particles.Add( p );
DelayedParticles.RemoveAt( i );
SceneMetrics.ParticlesCreated++;
try
{
OnParticleCreated?.Invoke( p );
}
catch ( System.Exception e )
{
Log.Warning( e );
}
}
}
readonly record struct ParticleCollisionPrefab( GameObject prefabSource, Vector3 position, Rotation rotation );
ConcurrentBag<ParticleCollisionPrefab> ParticleCollisionPrefabs = [];
internal void SpawnDeferredParticleCollisionPrefabs()
{
foreach ( var collision in ParticleCollisionPrefabs )
{
if ( !collision.prefabSource.IsValid() )
continue;
var go = collision.prefabSource.Clone( collision.position, collision.rotation );
go.Flags |= GameObjectFlags.Absolute | GameObjectFlags.Hidden | GameObjectFlags.NotSaved;
if ( Scene.IsEditor )
{
_spawnedGameObjects.Add( go );
}
}
ParticleCollisionPrefabs.Clear();
}
/// <summary>
/// Emit a particle at the given position.
/// </summary>
/// <param name="position">The position in which to spawn the particle</param>
/// <param name="delta">The time delta of the spawn. The first spawned particle is 0, the last spawned particle is 1. This is used to evaluate the spawn particles like lifetime and delay.</param>
/// <returns>A particle, will never be null. It's up to you to obey max particles.</returns>
public Particle Emit( Vector3 position, float delta )
{
var delay = StartDelay.Evaluate( delta, Random.Shared.Float() );
var p = Particle.Create();
p.Position = position;
p.StartPosition = position;
p.Radius = 1.0f;
p.Velocity = Vector3.Random.Normal * StartVelocity.Evaluate( delta, Random.Shared.Float() );
p.Velocity += InitialVelocity.Evaluate( delta, Random.Shared.Float(), Random.Shared.Float(), Random.Shared.Float() );
p.BornTime += delay;
p.DeathTime = p.BornTime + Lifetime.Evaluate( delta, p.Rand( 145, 100 ) );
if ( UsePrefabFeature && FollowerPrefabChance.Evaluate( delta, Random.Shared.Float( 0, 1 ) ) > Random.Shared.Float() )
{
var prefabSource = Random.Shared.FromList( FollowerPrefab );
if ( prefabSource.IsValid() )
{
p.Follower = prefabSource.Clone( position, p.Angles );
p.Follower.Flags |= GameObjectFlags.Absolute | GameObjectFlags.Hidden | GameObjectFlags.NotSaved;
if ( Scene.IsEditor )
{
_spawnedGameObjects.Add( p.Follower );
}
}
}
if ( delay > 0 )
{
DelayedParticles.Add( p );
}
else
{
Particles.Add( p );
SceneMetrics.ParticlesCreated++;
try
{
OnParticleCreated?.Invoke( p );
}
catch ( System.Exception e )
{
Log.Warning( e );
}
}
return p;
}
public void Terminate( Particle p )
{
if ( p.hasUpdated )
{
p.hasUpdated = false;
p.OnDisabled();
}
if ( p.Follower.IsValid() )
{
// If they have a TemporaryEffect then we will trust that to destroy
if ( p.Follower.GetComponent<TemporaryEffect>() is { } te )
{
// but disable the looping effects so it doesn't live forever!
ITemporaryEffect.DisableLoopingEffects( p.Follower );
}
else
{
p.Follower.Destroy();
}
p.Follower = null;
}
try
{
OnParticleDestroyed?.Invoke( p );
}
catch ( System.Exception e )
{
Log.Warning( e );
}
Particles.Remove( p );
if ( Particle.Pool.Count < 512 )
Particle.Pool.Enqueue( p );
SceneMetrics.ParticlesDestroyed++;
}
internal void OnControllerDisabled( Component c )
{
foreach ( var p in Particles )
{
p.DisableListenersForComponent( c );
}
}
/// <summary>
/// Should return true if we have active particles
/// </summary>
bool Component.ITemporaryEffect.IsActive => ParticleCount > 0;
}