-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
Severity
MEDIUM 🟡
Category
Rendering, ECS Architecture, Visual Effects
Description
The engine currently lacks a particle system for creating visual effects such as explosions, smoke, fire, rain, and other dynamic particle-based graphics. A particle system is essential for modern game development, allowing developers to create rich visual feedback and atmospheric effects efficiently.
Without this system, game developers must either implement custom particle solutions for each project or forgo particle effects entirely, limiting the visual capabilities of games built with this engine.
Affected Files
Engine/Scene/Components/- MissingParticleEmitterComponent.csEngine/Scene/Systems/- MissingParticleSystem.csEngine/Renderer/Renderer2D.cs- Needs particle rendering supportEditor/Panels/PropertiesPanel.cs- Needs inspector UI for particle emitter
Current Implementation
// N/A - Feature does not exist yetProblems
- No ParticleEmitterComponent to define particle emission properties (rate, lifetime, velocity, etc.)
- No ParticleSystem to handle particle lifecycle management (spawning, updating, culling)
- No efficient batch rendering for potentially thousands of particles
- No editor UI for configuring particle emitters visually
- Missing particle texture atlas support and texture animation
Impact
- Limited visual capabilities: Developers cannot create common VFX like explosions, magic effects, weather systems
- Inconsistent implementations: Each game project may implement particles differently, leading to performance issues
- Reduced competitiveness: Modern game engines are expected to provide particle systems out of the box
- Workflow inefficiency: Artists and designers need visual tools to iterate on particle effects quickly
Recommended Solution
1. Create ParticleEmitterComponent
// Engine/Scene/Components/ParticleEmitterComponent.cs
public class ParticleEmitterComponent
{
// Emission properties
public float EmissionRate { get; set; } = 10f; // particles per second
public int MaxParticles { get; set; } = 1000;
public bool IsEmitting { get; set; } = true;
public bool IsLooping { get; set; } = true;
public float Duration { get; set; } = 5f; // emitter lifetime
// Particle properties
public float ParticleLifetime { get; set; } = 2f;
public Vector2 ParticleLifetimeVariation { get; set; } = new(0.5f, 0.5f);
public Vector3 StartVelocity { get; set; } = new(0, 1, 0);
public Vector3 VelocityVariation { get; set; } = new(0.5f, 0.5f, 0.5f);
public Vector3 StartSize { get; set; } = Vector3.One * 0.1f;
public Vector3 EndSize { get; set; } = Vector3.One * 0.05f;
public Vector4 StartColor { get; set; } = Vector4.One;
public Vector4 EndColor { get; set; } = new(1, 1, 1, 0);
// Physics
public Vector3 Gravity { get; set; } = new(0, -9.81f, 0);
// Rendering
public Texture2D? ParticleTexture { get; set; }
public BlendMode BlendMode { get; set; } = BlendMode.Alpha;
// Internal state (not serialized)
internal List<Particle> ActiveParticles { get; } = new();
internal float TimeSinceLastEmission { get; set; }
internal float EmitterTime { get; set; }
}
internal struct Particle
{
public Vector3 Position;
public Vector3 Velocity;
public Vector4 Color;
public Vector3 Size;
public float Lifetime;
public float Age;
}2. Create ParticleSystem
// Engine/Scene/Systems/ParticleSystem.cs
public class ParticleSystem
{
public void OnUpdate(Scene scene, float deltaTime)
{
var emitters = scene.GetAllEntitiesWith<ParticleEmitterComponent>();
foreach (var entity in emitters)
{
var emitter = entity.GetComponent<ParticleEmitterComponent>();
var transform = entity.GetComponent<TransformComponent>();
UpdateEmitter(emitter, transform, deltaTime);
UpdateParticles(emitter, deltaTime);
}
}
private void UpdateEmitter(ParticleEmitterComponent emitter, TransformComponent transform, float deltaTime)
{
if (!emitter.IsEmitting) return;
emitter.EmitterTime += deltaTime;
emitter.TimeSinceLastEmission += deltaTime;
float emissionInterval = 1f / emitter.EmissionRate;
while (emitter.TimeSinceLastEmission >= emissionInterval &&
emitter.ActiveParticles.Count < emitter.MaxParticles)
{
SpawnParticle(emitter, transform);
emitter.TimeSinceLastEmission -= emissionInterval;
}
if (!emitter.IsLooping && emitter.EmitterTime >= emitter.Duration)
{
emitter.IsEmitting = false;
}
}
private void SpawnParticle(ParticleEmitterComponent emitter, TransformComponent transform)
{
var particle = new Particle
{
Position = transform.Translation,
Velocity = RandomizeVector(emitter.StartVelocity, emitter.VelocityVariation),
Color = emitter.StartColor,
Size = emitter.StartSize,
Lifetime = Random.Shared.NextSingle() *
(emitter.ParticleLifetimeVariation.Y - emitter.ParticleLifetimeVariation.X) +
emitter.ParticleLifetime,
Age = 0
};
emitter.ActiveParticles.Add(particle);
}
private void UpdateParticles(ParticleEmitterComponent emitter, float deltaTime)
{
for (int i = emitter.ActiveParticles.Count - 1; i >= 0; i--)
{
var particle = emitter.ActiveParticles[i];
particle.Age += deltaTime;
if (particle.Age >= particle.Lifetime)
{
emitter.ActiveParticles.RemoveAt(i);
continue;
}
// Update physics
particle.Velocity += emitter.Gravity * deltaTime;
particle.Position += particle.Velocity * deltaTime;
// Interpolate color and size
float t = particle.Age / particle.Lifetime;
particle.Color = Vector4.Lerp(emitter.StartColor, emitter.EndColor, t);
particle.Size = Vector3.Lerp(emitter.StartSize, emitter.EndSize, t);
emitter.ActiveParticles[i] = particle;
}
}
private Vector3 RandomizeVector(Vector3 baseVector, Vector3 variation)
{
return new Vector3(
baseVector.X + (Random.Shared.NextSingle() * 2 - 1) * variation.X,
baseVector.Y + (Random.Shared.NextSingle() * 2 - 1) * variation.Y,
baseVector.Z + (Random.Shared.NextSingle() * 2 - 1) * variation.Z
);
}
}3. Add Particle Rendering to Renderer2D
// Engine/Renderer/Renderer2D.cs
public static class Renderer2D
{
public static void RenderParticles(ParticleEmitterComponent emitter, Matrix4x4 viewProjection)
{
if (emitter.ActiveParticles.Count == 0) return;
BeginBatch();
SetBlendMode(emitter.BlendMode);
foreach (var particle in emitter.ActiveParticles)
{
DrawBillboardQuad(
particle.Position,
particle.Size,
particle.Color,
emitter.ParticleTexture
);
}
EndBatch();
}
}Implementation Checklist
- Create
ParticleEmitterComponentwith all necessary emission and particle properties - Implement
ParticleSystemfor particle lifecycle management (spawn, update, destroy) - Add particle rendering support to
Renderer2Dwith efficient batching - Implement billboard rendering for camera-facing particles
- Add editor inspector UI for
ParticleEmitterComponentinPropertiesPanel.cs - Support texture atlases and sprite sheet animation for particles
- Implement serialization for particle emitter component
- Add object pooling for particle structs to reduce allocations
- Create sample particle effects (fire, smoke, explosion, rain) as presets
- Document particle system API and usage examples in
docs/modules/ - Add performance benchmarks for particle rendering (test with 10k+ particles)
- Integrate particle system into Scene update loop
References
Engine/Scene/Systems/- Reference other system implementationsEngine/Renderer/Renderer2D.cs- Existing batch rendering systemdocs/modules/ecs-gameobject.md- ECS architecture documentation- Similar engines: Unity ParticleSystem, Unreal Niagara, Godot CPUParticles2D/3D
- Performance considerations: https://docs.unity3d.com/Manual/PartSysPerformance.html