Skip to content

Feature: Implement Particle System for Visual Effects #343

@kateusz

Description

@kateusz

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/ - Missing ParticleEmitterComponent.cs
  • Engine/Scene/Systems/ - Missing ParticleSystem.cs
  • Engine/Renderer/Renderer2D.cs - Needs particle rendering support
  • Editor/Panels/PropertiesPanel.cs - Needs inspector UI for particle emitter

Current Implementation

// N/A - Feature does not exist yet

Problems

  1. No ParticleEmitterComponent to define particle emission properties (rate, lifetime, velocity, etc.)
  2. No ParticleSystem to handle particle lifecycle management (spawning, updating, culling)
  3. No efficient batch rendering for potentially thousands of particles
  4. No editor UI for configuring particle emitters visually
  5. 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 ParticleEmitterComponent with all necessary emission and particle properties
  • Implement ParticleSystem for particle lifecycle management (spawn, update, destroy)
  • Add particle rendering support to Renderer2D with efficient batching
  • Implement billboard rendering for camera-facing particles
  • Add editor inspector UI for ParticleEmitterComponent in PropertiesPanel.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 implementations
  • Engine/Renderer/Renderer2D.cs - Existing batch rendering system
  • docs/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions