Marketplace

particle-systems

Creating visual effects using particle systems, physics simulation, and post-processing for polished, dynamic game graphics.

$ インストール

git clone https://github.com/pluginagentmarketplace/custom-plugin-game-developer /tmp/custom-plugin-game-developer && cp -r /tmp/custom-plugin-game-developer/skills/particle-systems ~/.claude/skills/custom-plugin-game-developer

// tip: Run this command in your terminal to install the skill


name: particle-systems version: "2.0.0" description: | Creating visual effects using particle systems, physics simulation, and post-processing for polished, dynamic game graphics. sasmp_version: "1.3.0" bonded_agent: 03-graphics-rendering bond_type: PRIMARY_BOND

parameters:

  • name: effect_type type: string required: false validation: enum: [explosion, fire, smoke, magic, weather, impact]
  • name: platform type: string required: false validation: enum: [pc, console, mobile, vr]

retry_policy: enabled: true max_attempts: 3 backoff: exponential

observability: log_events: [start, complete, error] metrics: [particle_count, draw_calls, gpu_time_ms]

Particle Systems

Particle System Architecture

┌─────────────────────────────────────────────────────────────┐
│                    PARTICLE LIFECYCLE                        │
├─────────────────────────────────────────────────────────────┤
│  EMISSION                                                    │
│  ├─ Spawn Rate (particles/second)                           │
│  ├─ Burst (instant spawn count)                             │
│  └─ Shape (point, sphere, cone, mesh)                       │
│                    ↓                                         │
│  SIMULATION                                                  │
│  ├─ Velocity (initial + over lifetime)                      │
│  ├─ Forces (gravity, wind, turbulence)                      │
│  ├─ Collision (world, depth buffer)                         │
│  └─ Noise (procedural movement)                             │
│                    ↓                                         │
│  RENDERING                                                   │
│  ├─ Billboard (camera-facing quads)                         │
│  ├─ Mesh (3D geometry per particle)                         │
│  ├─ Trail (ribbon following path)                           │
│  └─ GPU Instancing (batched draw)                           │
│                    ↓                                         │
│  DEATH                                                       │
│  └─ Lifetime expired → recycle or destroy                   │
└─────────────────────────────────────────────────────────────┘

Common Effect Recipes

EXPLOSION EFFECT:
┌─────────────────────────────────────────────────────────────┐
│  LAYERS:                                                     │
│  1. Flash (instant, bright, 0.1s)                           │
│  2. Core fireball (expanding sphere, 0.3s)                  │
│  3. Debris (physics-enabled chunks, 1-2s)                   │
│  4. Smoke (slow rising, fade out, 2-3s)                     │
│  5. Sparks (fast, gravity-affected, 0.5-1s)                 │
│  6. Shockwave (expanding ring, 0.2s)                        │
│                                                              │
│  SETTINGS:                                                   │
│  • Emission: Burst only (no rate)                           │
│  • Start speed: 5-20 (varies by layer)                      │
│  • Gravity: -9.8 for debris, 0 for smoke                    │
│  • Color: Orange→Red→Black over lifetime                    │
│  • Size: Start large, shrink (fireball) or grow (smoke)     │
└─────────────────────────────────────────────────────────────┘

FIRE EFFECT:
┌─────────────────────────────────────────────────────────────┐
│  LAYERS:                                                     │
│  1. Core flame (upward, orange-yellow, looping)             │
│  2. Ember particles (small, floating up)                    │
│  3. Smoke (dark, rises above flame)                         │
│  4. Light source (flickering point light)                   │
│                                                              │
│  SETTINGS:                                                   │
│  • Emission: Continuous (50-100/sec)                        │
│  • Velocity: Upward (2-5 units/sec)                         │
│  • Noise: Turbulence for natural movement                   │
│  • Color: White→Yellow→Orange→Red over lifetime             │
│  • Size: Start small, grow, then shrink                     │
│  • Blend: Additive for glow effect                          │
└─────────────────────────────────────────────────────────────┘

MAGIC SPELL EFFECT:
┌─────────────────────────────────────────────────────────────┐
│  LAYERS:                                                     │
│  1. Core glow (pulsing, bright center)                      │
│  2. Orbiting particles (circle around core)                 │
│  3. Trail particles (follow movement path)                  │
│  4. Impact burst (on hit/destination)                       │
│  5. Residual sparkles (lingering after effect)              │
│                                                              │
│  SETTINGS:                                                   │
│  • Emission: Rate + burst on cast/impact                    │
│  • Velocity: Custom curves for orbiting                     │
│  • Trails: Enable for mystical streaks                      │
│  • Color: Themed to element (blue=ice, red=fire)            │
│  • Blend: Additive for ethereal glow                        │
└─────────────────────────────────────────────────────────────┘

Unity Particle System Setup

// ✅ Production-Ready: Particle Effect Controller
public class ParticleEffectController : MonoBehaviour
{
    [Header("Effect Settings")]
    [SerializeField] private ParticleSystem mainEffect;
    [SerializeField] private ParticleSystem[] subEffects;
    [SerializeField] private AudioSource audioSource;
    [SerializeField] private Light effectLight;

    [Header("Pooling")]
    [SerializeField] private bool usePooling = true;
    [SerializeField] private float autoReturnDelay = 3f;

    private ParticleSystem.MainModule _mainModule;
    private float _originalLightIntensity;

    public event Action OnEffectComplete;

    private void Awake()
    {
        _mainModule = mainEffect.main;
        if (effectLight != null)
            _originalLightIntensity = effectLight.intensity;
    }

    public void Play(Vector3 position, Quaternion rotation)
    {
        transform.SetPositionAndRotation(position, rotation);

        // Play all particle systems
        mainEffect.Play();
        foreach (var effect in subEffects)
        {
            effect.Play();
        }

        // Play audio
        if (audioSource != null)
            audioSource.Play();

        // Animate light
        if (effectLight != null)
            StartCoroutine(AnimateLight());

        // Auto-return to pool
        if (usePooling)
            StartCoroutine(ReturnToPoolAfterDelay());
    }

    public void Stop()
    {
        mainEffect.Stop(true, ParticleSystemStopBehavior.StopEmitting);
        foreach (var effect in subEffects)
        {
            effect.Stop(true, ParticleSystemStopBehavior.StopEmitting);
        }
    }

    private IEnumerator AnimateLight()
    {
        effectLight.intensity = _originalLightIntensity;
        effectLight.enabled = true;

        float duration = _mainModule.duration;
        float elapsed = 0f;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / duration;
            effectLight.intensity = Mathf.Lerp(_originalLightIntensity, 0f, t);
            yield return null;
        }

        effectLight.enabled = false;
    }

    private IEnumerator ReturnToPoolAfterDelay()
    {
        yield return new WaitForSeconds(autoReturnDelay);

        OnEffectComplete?.Invoke();

        // Reset for reuse
        Stop();
        if (effectLight != null)
        {
            effectLight.enabled = false;
            effectLight.intensity = _originalLightIntensity;
        }
    }

    public void SetColor(Color color)
    {
        var startColor = _mainModule.startColor;
        startColor.color = color;
        _mainModule.startColor = startColor;

        if (effectLight != null)
            effectLight.color = color;
    }

    public void SetScale(float scale)
    {
        transform.localScale = Vector3.one * scale;
    }
}

GPU Particles (Compute Shader)

// ✅ Production-Ready: GPU Particle Compute Shader
#pragma kernel UpdateParticles

struct Particle
{
    float3 position;
    float3 velocity;
    float4 color;
    float size;
    float lifetime;
    float maxLifetime;
};

RWStructuredBuffer<Particle> particles;
float deltaTime;
float3 gravity;
float3 windDirection;
float windStrength;
float turbulenceStrength;
float time;

float noise3D(float3 p)
{
    // Simple 3D noise for turbulence
    return frac(sin(dot(p, float3(12.9898, 78.233, 45.164))) * 43758.5453);
}

[numthreads(256, 1, 1)]
void UpdateParticles(uint3 id : SV_DispatchThreadID)
{
    Particle p = particles[id.x];

    if (p.lifetime <= 0)
        return;

    // Apply forces
    float3 acceleration = gravity;

    // Wind
    acceleration += windDirection * windStrength;

    // Turbulence
    float3 turbulence = float3(
        noise3D(p.position + time) - 0.5,
        noise3D(p.position + time + 100) - 0.5,
        noise3D(p.position + time + 200) - 0.5
    ) * turbulenceStrength;
    acceleration += turbulence;

    // Update velocity and position
    p.velocity += acceleration * deltaTime;
    p.position += p.velocity * deltaTime;

    // Update lifetime
    p.lifetime -= deltaTime;

    // Fade out
    float lifetimeRatio = p.lifetime / p.maxLifetime;
    p.color.a = lifetimeRatio;
    p.size *= (0.5 + 0.5 * lifetimeRatio);

    particles[id.x] = p;
}

Performance Optimization

PARTICLE BUDGET GUIDELINES:
┌─────────────────────────────────────────────────────────────┐
│  PLATFORM      │ MAX PARTICLES │ MAX DRAW CALLS │ NOTES    │
├────────────────┼───────────────┼────────────────┼──────────┤
│  Mobile Low    │ 500           │ 5              │ Simple   │
│  Mobile High   │ 2,000         │ 10             │ Moderate │
│  Console       │ 50,000        │ 50             │ Complex  │
│  PC Low        │ 10,000        │ 20             │ Moderate │
│  PC High       │ 1,000,000+    │ 100+           │ GPU sim  │
│  VR            │ 5,000         │ 15             │ 90 FPS!  │
└────────────────┴───────────────┴────────────────┴──────────┘

OPTIMIZATION TECHNIQUES:
┌─────────────────────────────────────────────────────────────┐
│  REDUCE COUNT:                                               │
│  • LOD system (fewer particles at distance)                 │
│  • Cull off-screen emitters                                 │
│  • Limit max particles per system                           │
│                                                              │
│  REDUCE OVERDRAW:                                            │
│  • Use smaller particle sizes                               │
│  • Reduce transparency layers                               │
│  • Use cutout instead of transparent                        │
│                                                              │
│  BATCH DRAWS:                                                │
│  • Share materials between systems                          │
│  • Use texture atlases                                      │
│  • Enable GPU instancing                                    │
│                                                              │
│  GPU SIMULATION:                                             │
│  • Use compute shaders for >10K particles                   │
│  • Move physics to GPU                                      │
│  • VFX Graph (Unity) / Niagara (Unreal)                    │
└─────────────────────────────────────────────────────────────┘

Effect Pooling System

// ✅ Production-Ready: VFX Pool Manager
public class VFXPoolManager : MonoBehaviour
{
    public static VFXPoolManager Instance { get; private set; }

    [System.Serializable]
    public class EffectPool
    {
        public string effectName;
        public ParticleEffectController prefab;
        public int initialSize = 5;
        public int maxSize = 20;
    }

    [SerializeField] private EffectPool[] effectPools;

    private Dictionary<string, Queue<ParticleEffectController>> _pools;
    private Dictionary<string, EffectPool> _poolConfigs;

    private void Awake()
    {
        Instance = this;
        InitializePools();
    }

    private void InitializePools()
    {
        _pools = new Dictionary<string, Queue<ParticleEffectController>>();
        _poolConfigs = new Dictionary<string, EffectPool>();

        foreach (var pool in effectPools)
        {
            _pools[pool.effectName] = new Queue<ParticleEffectController>();
            _poolConfigs[pool.effectName] = pool;

            // Pre-warm pool
            for (int i = 0; i < pool.initialSize; i++)
            {
                var effect = CreateEffect(pool);
                _pools[pool.effectName].Enqueue(effect);
            }
        }
    }

    private ParticleEffectController CreateEffect(EffectPool pool)
    {
        var effect = Instantiate(pool.prefab, transform);
        effect.gameObject.SetActive(false);
        effect.OnEffectComplete += () => ReturnToPool(pool.effectName, effect);
        return effect;
    }

    public ParticleEffectController SpawnEffect(
        string effectName,
        Vector3 position,
        Quaternion rotation)
    {
        if (!_pools.ContainsKey(effectName))
        {
            Debug.LogError($"Effect pool '{effectName}' not found!");
            return null;
        }

        var pool = _pools[effectName];
        ParticleEffectController effect;

        if (pool.Count > 0)
        {
            effect = pool.Dequeue();
        }
        else if (_poolConfigs[effectName].maxSize > pool.Count)
        {
            effect = CreateEffect(_poolConfigs[effectName]);
        }
        else
        {
            Debug.LogWarning($"Pool '{effectName}' exhausted!");
            return null;
        }

        effect.gameObject.SetActive(true);
        effect.Play(position, rotation);
        return effect;
    }

    private void ReturnToPool(string effectName, ParticleEffectController effect)
    {
        effect.gameObject.SetActive(false);
        _pools[effectName].Enqueue(effect);
    }
}

// Usage
public class WeaponController : MonoBehaviour
{
    public void OnHit(Vector3 hitPoint, Vector3 hitNormal)
    {
        VFXPoolManager.Instance.SpawnEffect(
            "ImpactSparks",
            hitPoint,
            Quaternion.LookRotation(hitNormal));
    }
}

🔧 Troubleshooting

┌─────────────────────────────────────────────────────────────┐
│ PROBLEM: Particles popping in/out                           │
├─────────────────────────────────────────────────────────────┤
│ SOLUTIONS:                                                   │
│ → Add fade-in at birth (size/alpha over lifetime)           │
│ → Increase soft particle distance                           │
│ → Check culling settings                                    │
│ → Extend lifetime with fade-out                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ PROBLEM: Low frame rate with particles                      │
├─────────────────────────────────────────────────────────────┤
│ SOLUTIONS:                                                   │
│ → Reduce max particle count                                 │
│ → Use LOD (fewer particles at distance)                     │
│ → Switch to GPU simulation                                  │
│ → Reduce overdraw (smaller/fewer particles)                 │
│ → Use simpler shaders                                       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ PROBLEM: Particles clipping through geometry                │
├─────────────────────────────────────────────────────────────┤
│ SOLUTIONS:                                                   │
│ → Enable collision module                                   │
│ → Use depth fade/soft particles                             │
│ → Adjust near clip plane                                    │
│ → Position emitter away from surfaces                       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ PROBLEM: Effect looks different in build                    │
├─────────────────────────────────────────────────────────────┤
│ SOLUTIONS:                                                   │
│ → Check quality settings (particle raycast budget)          │
│ → Verify shader compatibility                               │
│ → Test on target hardware                                   │
│ → Check for editor-only components                          │
└─────────────────────────────────────────────────────────────┘

Engine-Specific Tools

EngineSystemGPU SupportVisual Editor
UnityShurikenVFX GraphYes (VFX Graph)
UnityVFX GraphNativeYes
UnrealCascadeLimitedYes
UnrealNiagaraNativeYes
GodotGPUParticlesYesInspector

Use this skill: When creating visual effects, polishing gameplay, or optimizing particle performance.