FxboxParticleSystem.cs
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
namespace fxbox;
/// <summary>
/// Component that integrates FXBox particle systems with s&box native particle components
/// </summary>
public class FXBoxNativeParticleSystem : Component, Component.ExecuteInEditor, ResourceLibrary.IEventListener
{
[Property] public ParticleResource ParticleSystem { get; set; }
[Property, Hide] private List<GameObject> Emitters { get; set; } = new();
[Property] public bool PlayOnStart { get; set; } = true;
[Property] public bool DestroyOnEnd { get; set; }
public Vector3 Velocity;
/// <summary>
/// Instance-specific float parameter overrides
/// </summary>
[Property, Group("Parameters"), Title("Float Parameters")]
public Dictionary<string, float> ParameterOverrides { get; set; } = new();
/// <summary>
/// Instance-specific vector parameter overrides
/// </summary>
[Property, Group("Parameters"), Title("Vector Parameters")]
public Dictionary<string, Vector3> VectorParameterOverrides { get; set; } = new();
/// <summary>
/// Instance-specific color parameter overrides
/// </summary>
[Property, Group("Parameters"), Title("Color Parameters")]
public Dictionary<string, ParticleGradient> ColorParameterOverrides { get; set; } = new();
private int _version = 0;
private Vector3 _lastPosition;
protected override void OnFixedUpdate()
{
Velocity = (WorldPosition - _lastPosition);
_lastPosition = WorldPosition;
if (ParticleSystem != null && ParticleSystem.Version != _version)
{
_version = ParticleSystem.Version;
UpdateEmitters();
}
}
// ==================== FLOAT PARAMETERS ====================
/// <summary>
/// Set a float parameter value for this specific instance
/// </summary>
public void Set(string name, float value)
{
if (ParticleSystem == null) return;
var param = ParticleSystem.FloatParameters?.FirstOrDefault(p => p.Name == name);
if (param == null)
{
//Log.Warning($"Float parameter '{name}' not found in particle system");
return;
}
ParameterOverrides[name] = value;
UpdateParameterValues();
}
public void Burst()
{
foreach ( var emitter in Emitters )
{
foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>( ) )
{
var target = particleEmitter.GetComponent<ParticleEffect>();
particleEmitter.ResetEmitter();
}
}
}
public void Burst( Vector3 worldPosition )
{
foreach ( var emitter in Emitters )
{
foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>( ) )
{
var target = particleEmitter.GetComponent<ParticleEffect>();
for(int i=0; i< particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i++)
{
particleEmitter.Emit( target );
}
}
}
foreach ( var emitter in Emitters )
{
foreach ( var particleEffect in emitter.GetComponentsInChildren<Sandbox.ParticleEffect>( ) )
{
foreach ( var particle in particleEffect.Particles )
{
if ( particle.Age <= 0.01f )
{
particle.Position += worldPosition - particleEffect.WorldTransform.Position;
}
}
}
}
}
/// <summary>
/// Get a float parameter value (override or default)
/// </summary>
public float GetFloatParameter(string name)
{
if (ParameterOverrides.TryGetValue(name, out float overrideValue))
{
return overrideValue;
}
return ParticleSystem?.GetParameterDefault(name) ?? 0f;
}
/// <summary>
/// Reset a float parameter to its default value
/// </summary>
public void ResetParameter(string name)
{
ParameterOverrides.Remove(name);
UpdateParameterValues();
}
// ==================== VECTOR PARAMETERS ====================
/// <summary>
/// Set a vector parameter value for this specific instance
/// </summary>
public void Set(string name, Vector3 value)
{
if (ParticleSystem == null) return;
var param = ParticleSystem.VectorParameters?.FirstOrDefault(p => p.Name == name);
if (param == null)
{
//Log.Warning($"Vector parameter '{name}' not found in particle system");
return;
}
VectorParameterOverrides[name] = value;
UpdateParameterValues();
}
public int GetAliveParticles()
{
var particles = 0;
foreach ( var emitter in Emitters )
{
var target = emitter.GetComponent<ParticleEffect>();
particles += target.ParticleCount;
}
return particles;
}
/// <summary>
/// Get a vector parameter value (override or default)
/// </summary>
public Vector3 GetVectorParameter(string name)
{
if (VectorParameterOverrides.TryGetValue(name, out Vector3 overrideValue))
{
return overrideValue;
}
return ParticleSystem?.GetVectorParameterDefault(name) ?? Vector3.Zero;
}
/// <summary>
/// Reset a vector parameter to its default value
/// </summary>
public void ResetVectorParameter(string name)
{
VectorParameterOverrides.Remove(name);
UpdateParameterValues();
}
// ==================== COLOR PARAMETERS ====================
/// <summary>
/// Set a color parameter value for this specific instance
/// </summary>
public void Set(string name, Color value)
{
if (ParticleSystem == null) return;
var param = ParticleSystem.ColorParameters?.FirstOrDefault(p => p.Name == name);
if (param == null)
{
//Log.Warning($"Color parameter '{name}' not found in particle system");
return;
}
ColorParameterOverrides[name] = value;
UpdateParameterValues();
}
/// <summary>
/// Get a color parameter value (override or default)
/// </summary>
public ParticleGradient GetColorParameter(string name)
{
if (ColorParameterOverrides.TryGetValue(name, out ParticleGradient overrideValue))
{
return overrideValue;
}
return ParticleSystem?.GetColorParameterDefault(name) ?? Color.White;
}
/// <summary>
/// Reset a color parameter to its default value
/// </summary>
public void ResetColorParameter(string name)
{
ColorParameterOverrides.Remove(name);
UpdateParameterValues();
}
// ==================== GENERAL ====================
/// <summary>
/// Reset all parameters to their default values
/// </summary>
public void ResetAllParameters()
{
ParameterOverrides.Clear();
VectorParameterOverrides.Clear();
ColorParameterOverrides.Clear();
UpdateParameterValues();
}
/// <summary>
/// Helper buttons for the inspector
/// </summary>
[Button("Reset All Parameters")]
[Group("Parameters")]
public void ResetAllParametersButton()
{
ResetAllParameters();
}
[Button("Initialize All Parameters")]
[Group("Parameters")]
[Description("Copy all parameters from the resource as overrides")]
public void InitializeParametersFromResource()
{
if (ParticleSystem == null) return;
ParameterOverrides.Clear();
if (ParticleSystem.FloatParameters != null)
{
foreach (var param in ParticleSystem.FloatParameters)
{
ParameterOverrides[param.Name] = param.DefaultValue;
}
}
VectorParameterOverrides.Clear();
if (ParticleSystem.VectorParameters != null)
{
foreach (var param in ParticleSystem.VectorParameters)
{
VectorParameterOverrides[param.Name] = param.DefaultValue;
}
}
ColorParameterOverrides.Clear();
if (ParticleSystem.ColorParameters != null)
{
foreach (var param in ParticleSystem.ColorParameters)
{
ColorParameterOverrides[param.Name] = param.DefaultValue;
}
}
}
/// <summary>
/// Update parameter values without rebuilding emitters
/// </summary>
private void UpdateParameterValues()
{
if (ParticleSystem?.Emitters == null) return;
foreach (var emitterObject in Emitters)
{
if (!emitterObject.IsValid()) continue;
var controller = emitterObject.GetComponent<FXBoxParticleController>();
if (controller != null)
{
UpdateModulesWithParameters(emitterObject, controller.EmitterData);
}
}
}
public void OnSave(GameResource resource)
{
Log.Info("The scene has stopped");
}
protected override void OnStart()
{
UpdateEmitters();
}
protected override void OnEnabled()
{
UpdateEmitters();
base.OnEnabled();
}
protected override void DrawGizmos()
{
Gizmo.Hitbox.Sprite( 0, 50, false );
if ( Gizmo.IsHovered || Gizmo.IsSelected)
{
Gizmo.Draw.Color = Color.White;
if ( Gizmo.IsSelected )
{
Gizmo.Draw.Color = Color.Yellow;
}
}
else
{
Gizmo.Draw.Color = Color.Gray;
}
Gizmo.Draw.Sprite( 0, 50, Texture.Load( "images/particlehover.vtex" ), false );
}
public void UpdateEmitters()
{
// Clean up existing emitters
foreach (var emitter in Emitters)
{
emitter?.DestroyImmediate();
}
Emitters.Clear();
if (ParticleSystem?.Emitters == null) return;
// Create emitters from ParticleResource
foreach (var emitterData in ParticleSystem.Emitters)
{
if (!emitterData.Enabled) continue;
var emitterObject = new GameObject(GameObject);
emitterObject.Flags = emitterObject.Flags.WithFlag( GameObjectFlags.Hidden, true );
emitterObject.Name = emitterData.Name;
Emitters.Add(emitterObject);
// Add ParticleEffect component
var particleEffect = emitterObject.GetOrAddComponent<ParticleEffect>();
particleEffect.MaxParticles = emitterData.MaxParticles;
// Create native components from modules
CreateModuleComponents(emitterObject, emitterData);
// Add controller to handle particle updates
var controller = emitterObject.GetOrAddComponent<FXBoxParticleController>();
controller.EmitterData = emitterData;
controller.ParticleEffect = particleEffect;
controller.InitializeModules = emitterData.InitializeModules;
controller.ParticleSystemComponent = this;
}
}
private void CreateModuleComponents(GameObject go, ParticleEmitter emitterData)
{
// Create components from all modules that implement IParticleComponentCreator
var allModules = emitterData.SpawnModules
.Concat(emitterData.InitializeModules)
.Concat(emitterData.UpdateModules)
.Concat(emitterData.RenderModules);
var particleModules = allModules.ToList();
foreach (var module in particleModules.OfType<IParticleComponentCreator>())
{
if (module is ParticleModule pm && pm.Enabled)
{
module.CreateComponent(go);
}
}
var context = new ParticleExecutionContext();
context.Effect = go.GetComponent<ParticleEffect>();
context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
context.SystemComponent = this;
foreach (var module in particleModules)
{
module.Initialize(context);
}
// Ensure we have at least a basic emitter if none was created
if (!go.GetComponent<Sandbox.ParticleEmitter>().IsValid())
{
var emitter = go.AddComponent<ParticleSphereEmitter>();
emitter.Duration = ParticleSystem.Duration;
emitter.Loop = ParticleSystem.Looping;
emitter.DestroyOnEnd = DestroyOnEnd;
}
var emitters = go.GetComponentsInChildren<Sandbox.ParticleEmitter>();
foreach ( var emit in emitters )
{
emit.Duration = ParticleSystem.Duration;
emit.Loop = ParticleSystem.Looping;
emit.DestroyOnEnd = DestroyOnEnd;
}
}
private void UpdateModulesWithParameters(GameObject go, ParticleEmitter emitterData)
{
var context = new ParticleExecutionContext();
context.Effect = go.GetComponent<ParticleEffect>();
context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
context.SystemComponent = this;
var allModules = emitterData.SpawnModules
.Concat(emitterData.InitializeModules)
.Concat(emitterData.UpdateModules)
.Concat(emitterData.RenderModules);
foreach (var module in allModules)
{
module.Initialize(context);
}
}
protected override void OnDisabled()
{
// Clean up existing emitters
foreach (var emitter in Emitters)
{
emitter?.Destroy();
}
Emitters.Clear();
base.OnDisabled();
}
}
/// <summary>
/// Controller that executes particle update modules on each particle
/// </summary>
public class FXBoxParticleController : ParticleController
{
[Property, Hide] public ParticleEmitter EmitterData { get; set; }
[Property, Hide] public new ParticleEffect ParticleEffect { get; set; }
[Property, Hide] public FXBoxNativeParticleSystem ParticleSystemComponent { get; set; }
private TimeSince _timeSinceCreated = 0;
public List<ParticleModule> InitializeModules { get; set; } = new List<ParticleModule>();
protected override void OnUpdate()
{
if(_timeSinceCreated > ParticleSystemComponent.ParticleSystem.Duration && !ParticleSystemComponent.ParticleSystem.Looping&& ParticleEffect.Particles.Count <= 0)
{
if ( !Scene.IsEditor )
{
GameObject.Root.Destroy();
return;
}
}
var context = new ParticleExecutionContext
{
Particle = null,
Effect = ParticleEffect,
Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
SystemComponent = ParticleSystemComponent
};
foreach ( var init in EmitterData.InitializeModules )
{
init.Execute( context );
}
foreach ( var spawn in EmitterData.SpawnModules )
{
spawn.Execute( context );
}
}
protected override void OnParticleStep(Particle particle, float delta)
{
base.OnParticleStep(particle, delta);
if (EmitterData == null) return;
var context = new ParticleExecutionContext
{
Particle = particle,
Effect = ParticleEffect,
Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
SystemComponent = ParticleSystemComponent
};
// Execute all update modules that implement IParticleUpdater
foreach (var module in EmitterData.UpdateModules.OfType<IParticleUpdater>())
{
if (module is ParticleModule pm && pm.Enabled)
{
module.UpdateParticle(context, delta);
}
}
}
protected override void OnParticleCreated(Particle p)
{
p.Position = ParticleEffect.WorldTransform.Position;
InitializeModules ??= EmitterData?.InitializeModules ?? new List<ParticleModule>();
var context = new ParticleExecutionContext
{
Particle = p,
Effect = ParticleEffect,
Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
SystemComponent = ParticleSystemComponent
};
foreach (var module in InitializeModules)
{
module.Initialize(context);
}
}
}
/// <summary>
/// Interface for modules that can create native components
/// </summary>
public interface IParticleComponentCreator
{
void CreateComponent(GameObject go);
}
/// <summary>
/// Interface for modules that update particles
/// </summary>
public interface IParticleUpdater
{
void UpdateParticle(ParticleExecutionContext particle, float delta);
}
[Flags]
public enum FXCopyFlags
{
Rotation = 1,
Scale = 2,
}