Editor component that gathers and manages ParticleEffect and ParticleEmitter children. It exposes controls for time scaling, pause/restart, playback scrubbing, emitting, and drives IPhaseDriven components from a chosen emitter.
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Sandbox;
[Title( "Particle Effects Manager" )]
[Category( "Particle" )]
[Description(
"This component let's you manage the TimeScale , the pause and restart of every particle effect component present in the childrens of this component gameobject so it is easier to preview the whole system" )]
public sealed class ParticleEffectsManager : Component, Component.ExecuteInEditor
{
[Property] public List<ParticleEffect> ParticleEffects { get; set; }
[Property] public List<ParticleEmitter> ParticleEmitters { get; set; }
[Property] List<IResetable> Resetables {get; set;}
[Property] List<IPhaseDriven> PhaseDriven { get; set; }
[Property, Group( "Parameters" )]
public bool DrivePhaseFromEmitter { get; set; } = true;
[Property, Group( "Parameters" ), ShowIf( nameof( DrivePhaseFromEmitter ), true )]
public ParticleEmitter PhaseMaster { get; set; }
[Property, Group( "Parameters" ), Range( -1f, 1f ), Step( 0.1f )]
float TimeScale { get; set; } = 1f;
[JsonIgnore] public bool IsPaused { get; set; }
[JsonIgnore] public bool IsRestart { get; set; }
[JsonIgnore]
public float LongestDuration
{
get
{
var duration = 0f;
foreach ( var emitter in ParticleEmitters )
{
if ( emitter.Duration.Evaluate(0,0) + emitter.Delay.Evaluate(0,0) > duration )
{
duration = emitter.Duration.Evaluate(0,0) + emitter.Delay.Evaluate(0,0);
}
}
// foreach (var resetable in Resetables )
// {
// if(resetable.Duration > duration )
// {
// duration = resetable.Duration;
// }
// }
return duration;
}
}
[Property, Group( "Actions" )] private bool Emit { get; set; }
[JsonIgnore, Range( 0f, 1f ), Step( 0.001f )]
public float PlayBack { get; set; }
[JsonIgnore] public bool IsPlayBack { get; set; }
[Property, Range( 0, 10 ), Step( 0.001f )]
public float DelayEffect { get; set; }
public int ParticleCount
{
get
{
int count = 0;
foreach ( var particleEffect in ParticleEffects )
{
count += particleEffect.ParticleCount;
}
return count;
}
}
public float PlaybackTime
{
get
{
float playbackTime = 0;
foreach ( var emitter in ParticleEmitters )
{
if ( emitter.IsValid() && emitter.time > playbackTime )
{
playbackTime = emitter.time;
}
}
return playbackTime;
}
}
protected override void OnEnabled()
{
SetParticleSystems();
if ( Scene.IsEditor ) return;
foreach ( var particleEmitter in ParticleEmitters )
{
particleEmitter.Delay = particleEmitter.Delay.Evaluate(0,0) + DelayEffect;
}
}
protected override void OnUpdate()
{
PushPhase();
if ( !Scene.IsEditor ) return;
CheckForNullEmitters();
if ( Emit ) EmitParticles();
if ( IsRestart ) Restart();
foreach ( var particleEffect in ParticleEffects )
{
CheckForNullEffect( particleEffect );
if ( IsPlayBack )
{
PlayBacking( particleEffect );
}
else
{
TimeScaling( particleEffect );
}
}
}
[Button]
[Group( "Actions" )]
[Description( "Gathers all ParticleEffects and ParticleEmitters in this gameobject hierarchy" )]
private void SetParticleSystems()
{
ParticleEffects = GameObject.GetComponentsInChildren<ParticleEffect>().ToList();
ParticleEmitters = GameObject.GetComponentsInChildren<ParticleEmitter>().ToList();
Resetables = GameObject.GetComponentsInChildren<IResetable>().ToList();
PhaseDriven = GameObject.GetComponentsInChildren<IPhaseDriven>().ToList();
_resolvedPhaseMaster = null;
}
private ParticleEmitter _resolvedPhaseMaster;
// Single clock for the whole effect. The engine clamps the particle step to
// 1/30s (ParticleGameSystem) and zeroes the emitter's time on each loop
// instead of wrapping it, so anything keeping its own accumulator slides out
// of phase a little every cycle - visibly, after a handful of loops in the
// editor where frame hitches are routine. Slaving those components to the
// emitter's own Delta makes that drift structurally impossible.
private void PushPhase()
{
if ( !DrivePhaseFromEmitter ) return;
if ( PhaseDriven is null || PhaseDriven.Count == 0 ) return;
var master = ResolvePhaseMaster();
if ( master is null ) return;
var phase = master.Delta;
foreach ( var driven in PhaseDriven )
{
if ( driven is null ) continue;
driven.SetPhase( phase );
}
}
private ParticleEmitter ResolvePhaseMaster()
{
if ( PhaseMaster.IsValid() ) return PhaseMaster;
if ( _resolvedPhaseMaster.IsValid() ) return _resolvedPhaseMaster;
if ( ParticleEmitters is null ) return null;
var longest = -1f;
foreach ( var emitter in ParticleEmitters )
{
if ( !emitter.IsValid() ) continue;
var cycle = emitter.Duration.Evaluate( 0, 0 ) + emitter.Delay.Evaluate( 0, 0 );
if ( cycle <= longest ) continue;
longest = cycle;
_resolvedPhaseMaster = emitter;
}
return _resolvedPhaseMaster;
}
private void EmitParticles()
{
Emit = false;
foreach ( var effect in ParticleEffects )
{
effect.Emit( 0, 0 );
}
}
private void TimeScaling( ParticleEffect particleEffect )
{
particleEffect.TimeScale = TimeScale;
var particleEmitter = particleEffect.Components.Get<ParticleEmitter>();
if ( TimeScale < 0 && particleEmitter.time <= 0 && !IsPaused )
{
var timing = particleEmitter.Duration.Evaluate(0,0) - 0.01f;
SetParticulesTiming( particleEffect, timing );
particleEmitter.time = timing;
}
else
{
particleEffect.Paused = IsPaused;
}
}
private void PlayBacking( ParticleEffect particleEffect )
{
particleEffect.TimeScale = 0;
var particleEmitter = particleEffect.Components.Get<ParticleEmitter>();
var remappedTiming = PlayBack.Remap( 0, 1f, 0, particleEmitter.Duration.Evaluate(0,0) + particleEmitter.Delay.Evaluate(0,0) );
particleEmitter.time = remappedTiming;
SetParticulesTiming( particleEffect, remappedTiming );
}
private void Restart()
{
IsPaused = false;
IsRestart = false;
foreach ( var particleEffect in ParticleEffects )
{
particleEffect.Clear();
particleEffect.ResetEmitters();
}
foreach( var resetable in Resetables )
{
resetable.ResetTime();
}
}
private void SetParticulesTiming( ParticleEffect particleEffect, float timing )
{
var particleEmitter = particleEffect.Components.Get<ParticleEmitter>();
particleEmitter.time = timing;
foreach ( var particule in particleEffect.Particles )
{
particule.Age = timing;
}
}
private void CheckForNullEffect( ParticleEffect particleEffect )
{
if ( !particleEffect.IsValid ) ParticleEffects.Remove( particleEffect );
}
private void CheckForNullEmitters( )
{
ParticleEmitters.RemoveAll( e => !e.IsValid() );
}
}