1
votes

I have lots of same simple objects that are affecting gameplay, thousands of them! Well, not thousands, but really many. So if I make them GameObjects, the FPS decreases, especially when spawning them. Even with pooling. I should try a different approach.

You know that particle system in Unity3D can render many particles very fast. It also automatically controls particles, emits and removes them. But in my case, positions and lifetimes of objects are managed by game logic, and particle system is not allowed to do anything without my command, even reorder particles.

I am trying to use SetParticles method to control particles. It works in test project, where I use GetParticles first. I can even remove particles setting lifetime to -1, but can't spawn new ones. Also it does not prevent particle system from controlling particles.

I can disable emission so no particles will be created automatically.
I can set particles speed to 0 so they will not move.
I can set lifetime to huge number so they will not be removed.

I have an pool of Particle instances, to avoid unnessessary GC allocations. Objects receive a reference to particle when they are spawned, change it when updated, and set lifetime -1 and return it to pool when deleted. The pool stores this:

private ParticleSystem.Particle [] _unusedParticles;
private int                        _unusedCount;
private ParticleSystem.Particle [] _array;

_unused array and counter are needed for pooling, and _array stores all particles, used and unused alike, and is used in SetParticles call.

The main disadvantage of this method is that it doesn't work, probably because SetParticles does not create new particles. Also I guess it doesn't do anything with particles draw order, that's why it's poorly suited for bullet hell games where bullet patterns should look nice.

What should I do to properly disable automatic control of particles and properly setup direct control, with spawning and removing?

3

3 Answers

1
votes

What you are looking for might be

    List<Matrix4x4> matrixes=new List<Matrix4x4>();
    for (...)
    {
        matrixes.Add(Matrix4x4.TRS( position,rotation,scale));
    }
    Graphics.DrawMeshInstanced(mesh,0,material, matrixes);

On each frame you can just update positions, rotations and scales, and get all the instances rendered on the GPU in one drawcall (pretty darn fast compared to seperate gameobjects). You can render up to 1000 instances in one call using this way

0
votes

Create an empty GameObject, then add the ParticleSystem as child. Set playOnAwake to true.

Now when you need it, set GameObject.SetActive to true else false.

To get each of them use ParticleSystem.GetParticles, modify them and ParticleSystem.GetParticles.

I hope this is what you are looking for.

ParticleSystem m_System;
ParticleSystem.Particle[] m_Particles;
public float m_Drift = 0.01f;

private void LateUpdate()
{
    InitializeIfNeeded();

    // GetParticles is allocation free because we reuse the m_Particles buffer between updates
    int numParticlesAlive = m_System.GetParticles(m_Particles);

    // Change only the particles that are alive
    for (int i = 0; i < numParticlesAlive; i++)
    {
        m_Particles[i].velocity += Vector3.up * m_Drift;
    }

    // Apply the particle changes to the Particle System
    m_System.SetParticles(m_Particles, numParticlesAlive);
}

void InitializeIfNeeded()
{
    if (m_System == null)
        m_System = GetComponent<ParticleSystem>();

    if (m_Particles == null || m_Particles.Length < m_System.main.maxParticles)
        m_Particles = new ParticleSystem.Particle[m_System.main.maxParticles];
}
0
votes

After we created a particle system in editor, we should disable emission and shape, so only core part and renderer stay active.

The most important part is that Simulation Speed must be zero. A particle system will no longer emit, remove or process particles automatically. Only your code now manages them.

I use this class to control particles. Instead of binding a particle to an object, it has API for registering objects, smoke in this case. Also, it stores a temporary array for particles to avoid GC allocations, and particle count, to avoid using particleCount property of the particle system.

In Update, which is called by my game logic, the following happens:

  • All objects that were despawned by game logic, are removed from list.
  • If object count is greater than array length, the array is resized.
  • If object count is greater than live particle count, _particleSystem.Emit call is made. If we call SetParticles without that, no new particles will appear. We must emit them first.
  • GetParticles is called. Simple, but probably not the most efficient solution, but it preserves particles data. It may be optimized by setting all particle data on array creation and resizing. If you do so, remove GetParticles call and uncomment the line above. Also, your game logic should manage particles even more carefully.
  • For each object, we let that object change a particle.
  • Each particle without object should be removed, so their lifetime is set to negative number.
  • SetParticles updates particles in system.
    public class SmokeSystem {

        private ParticleSystem             _particleSystem;
        private List <Smoke>               _smoke     = new List <Smoke> ();
        private ParticleSystem.Particle [] _particles = new ParticleSystem.Particle[256];
        private int                        _particleCount;


        public SmokeSystem (ParticleSystem particleSystem) {
            _particleSystem = particleSystem;
        }


        public void AddSmoke (Smoke smoke) => _smoke.Add (smoke);


        public void Update () {
            _smoke.RemoveAll (e => e.Despawned);

            if (_smoke.Count > _particles.Length) {
                int newSize = Max (_smoke.Count, 2 * _particles.Length);
                Array.Resize (ref _particles, newSize);
            }

            int count = _smoke.Count;
            if (count > _particleCount) {
                _particleSystem.Emit (count - _particleCount);
                // _particleCount = count;
            }
            _particleCount = _particleSystem.GetParticles (_particles);
            for (int i = 0; i < count; i++) {
                _smoke [i].UpdateParticle (ref _particles [i]);
            }
            for (int i = count; i < _particleCount; i++) {
                _particles [i].remainingLifetime = -1;
            }
            _particleSystem.SetParticles (_particles, _particleCount);
            _particleCount = count;
        }

    }

It does not depend on GPU instancing support so it will work on WebGL.