There’s something deeply unsettling about watching a lunar lander execute a perfect landing, settle gently on the ground, and then explode.
Not because it hit too hard. Not because it tipped over. But because the physics engine thought it was still landing, frame after frame, accumulating collision impulses until the fitness function declared it a catastrophic failure.
The Symptom
Our LunarLander simulation uses Rapier2D for physics. The fitness function tracks landing impulses to determine how gentle the touchdown was. A soft landing means low impulses. A crash means high impulses. Simple enough.
Except the lander would touch down perfectly, come to a complete stop, and then the impulse values would keep climbing. The collision event system was firing continuously, as if the lander was repeatedly slamming into the ground at zero velocity.
The logs showed it clearly:
Frame 245: impulseY = 0.23 // Initial touchdown
Frame 246: impulseY = 0.24 // Still "landing"
Frame 247: impulseY = 0.25 // Why??
Frame 248: impulseY = 0.26 // STOP LANDING
Frame 249: impulseY = 0.27 // YOU'RE ALREADY ON THE GROUND
The Investigation
My first thought was the Rapier event system was broken. Maybe collision events weren’t being cleared properly? I checked the docs. I read the source code. Collision events should fire once per contact, not continuously.
Then I thought maybe it was our fitness tracking. Maybe we were accumulating impulses wrong? I reviewed that code three times. The logic was sound - we were just summing the impulse magnitudes from collision events. Nothing fancy.
The breakthrough came when I added logging to the actual Rapier collision handler:
events.on('collision', (contact) => {
console.log('Collision detected:', {
impulse: contact.totalForceMagnitude,
stopped: lander.linvel().length() < 0.01
})
})
The lander velocity was effectively zero. It wasn’t moving!! But collisions kept firing.
The Root Cause
Turns out this is a known behavior in physics engines. When two bodies are in continuous contact, they generate “resting contact” events every frame to prevent interpenetration. The physics engine applies tiny correction forces to keep the bodies separated.
From the physics engine’s perspective, these are micro-collisions. The bodies are trying to overlap, and the engine pushes them apart, generating an impulse in the process.
From our perspective, the lander is just sitting there. Landed. Done. Not colliding with anything.
The disconnect was that we were treating all collision events as landing events. We didn’t distinguish between “hitting the ground” and “sitting on the ground.”
The Fix
The solution was to only count collision impulses during the actual landing phase:
let hasLanded = false
events.on('collision', (contact) => {
const velocity = lander.linvel().length()
// Only count the impulse if we're still moving
if (!hasLanded && velocity > 0.01) {
impulseX += Math.abs(contact.impulse.x)
impulseY += Math.abs(contact.impulse.y)
}
// Mark as landed when we've come to rest
if (velocity < 0.01) {
hasLanded = true
}
})
Now the fitness function only tracks the dynamic phase of landing. Once the lander comes to rest, we stop accumulating impulses. Resting contacts are ignored.
Perfect landings stay perfect. The explosion rate dropped to zero.
The Deeper Issue
This bug revealed a conceptual mismatch between physics simulation and game logic. Physics engines model the world continuously - every frame, every micro-interaction matters. Game logic thinks in discrete states - “falling,” “landing,” “landed.”
The key insight was that collision events aren’t semantic. They don’t mean “something important happened.” They just mean “two bodies touched.” It’s up to us to interpret that touch in context.
A collision during rapid movement? That’s a crash. A collision while stationary? That’s just resting contact. Same event, different meaning.
The Lesson
When working with physics engines:
- Collision events are low-level, not high-level
- Distinguish between dynamic and static contact
- Use velocity thresholds to detect state changes
- Don’t assume event frequency correlates with severity
- Log both events AND state when debugging physics
The ground doesn’t stop generating contact forces just because your lander stopped moving. You have to tell the system when to stop caring about those forces.
And that’s the difference between a successful landing and a phantom explosion.