Skip to content

fix(particle): #1417 free Particle instead of Point2D in ParticleSystem.onUpdate#1480

Open
AndrewAssad2 wants to merge 2 commits into
AlmasB:devfrom
AndrewAssad2:fix/1417-particle-pool-recycling
Open

fix(particle): #1417 free Particle instead of Point2D in ParticleSystem.onUpdate#1480
AndrewAssad2 wants to merge 2 commits into
AlmasB:devfrom
AndrewAssad2:fix/1417-particle-pool-recycling

Conversation

@AndrewAssad2

Copy link
Copy Markdown

Summary

Fixes #1417.

In ParticleSystem.onUpdate, expired Particle instances are not returned to the Pools cache. The call Pools.free(p) passes the wrong argument — p is the emitter's Point2D position (from the outer destructured lambda parameter (emitter, p)), not the dead Particle. Because Pools.free silently does nothing for types it doesn't know about, the bug is invisible at runtime but causes the particle pool to never refill, leading to extra GC pressure when particle effects run for a long time.

Where the bug is

File: fxgl-core/src/main/kotlin/com/almasb/fxgl/particle/ParticleSystem.kt

emitters.forEach { (emitter, p) ->           // p : Point2D
    ...
    while (iter.hasNext()) {
        val particle = iter.next()           // particle : Particle
        if (particle.update(tpf)) {
            iter.remove()
            pane.children.remove(particle.view)
            Pools.free(p)                    // bug: frees Point2D, not Particle
        }
        ...
    }
}

The same pattern in ParticleComponent.kt (line 54) works correctly because its loop is not nested inside a forEach whose lambda parameter is also called p.

What I changed

In ParticleSystem.kt:

  1. Changed Pools.free(p) to Pools.free(particle) so the real Particle gets returned to the pool.
  2. Renamed the outer lambda parameter from p to position so the shadowing can't happen again.
  3. Updated emitter.emit(p.x, p.y) to emitter.emit(position.x, position.y) so the rename is consistent.

No public API changes.

Test

I added a new test Expired particles are returned to the pool in ParticleSystemTest.kt. It:

  • Sets up a ParticleSystem with one emitter (50 particles per frame, 5 max emissions, 0.5s lifespan).
  • Calls onUpdate(0.5) ten times so all 250 particles spawn and die.
  • Uses reflection to read the private Pools.typePools map and checks that a Particle pool entry exists.

Before the fix, no Particle pool ever gets registered (because Pools.free(Point2D) does nothing), so the test would fail. After the fix it passes.

Notes

  • @AlmasB approved this issue for me via email on 18 May 2026 before I started.
  • This is part of my CI646 (Programming Languages, Concurrency and Client-Server Computing) coursework at the University of Brighton.
  • I worked on this through the GitHub web interface, so the test hasn't been run locally — CI on this PR will be the first run.

…pdate

Closes AlmasB#1417.

In ParticleSystem.onUpdate, the outer destructured lambda parameter `p` (a Point2D representing the emitter position) shadowed the intended target of Pools.free. Because Pools.free is a no-op for unregistered types, Particle instances expired silently without being returned to the pool, defeating object pooling and producing GC pressure under heavy effects.

Changes:
- Rename the lambda parameter from `p` to `position` to remove shadowing.
- Update the corresponding emitter.emit(...) call accordingly.
- Replace Pools.free(p) with Pools.free(particle), so the dead Particle is recycled rather than the Point2D position.

No public API changes.
Adds a JUnit 5 test in ParticleSystemTest that verifies expired Particles are returned to the Pools cache after onUpdate.

The test drives a ParticleSystem through 10 update cycles (spawning and expiring 250 particles), then reflectively reads the private Pools.typePools map to assert that a Particle pool entry exists.

Before the fix for AlmasB#1417, the pool would not contain a Particle entry because Pools.free was being called with a Point2D argument and silently no-opped for unregistered types.

This test would fail on the previous code and passes with the fix applied.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant