Minie: Repeated calls to ContactListener started/ended contact

Hi, I implemented the ContactListener for the first time in my game and I noticed it is retriggering duplicated callbacks.
It took me a while to find the minimal set of conditions to get this behavior as there are a few things involved.

I identified two things introducing the issue:

  • Compound shape
  • Varying velocity

Here’s the sample code:

package jc.workbench;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.ContactListener;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.shapes.BoxCollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.CylinderCollisionShape;
import com.jme3.bullet.objects.PhysicsGhostObject;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Vector3f;
import com.jme3.system.AppSettings;
import java.util.Random;

public class TestContactListener extends SimpleApplication {

  static long tick = 0;
  static int count = 0;

  private Random random = new Random();
  private PhysicsRigidBody vehicle;

  public static void main(String[] args) {
    var app = new TestContactListener();
    app.setShowSettings(true);
    app.setSettings(new AppSettings(true));
    app.settings.setResolution(1500, 900);
    app.start();
  }

  @Override
  public void simpleInitApp() {
    // cam
    cam.setLocation(new Vector3f(0, 0, 20));

    // physics space
    var physicsState = new BulletAppState();
    physicsState.setDebugEnabled(true);
    stateManager.attach(physicsState);
    var space = physicsState.getPhysicsSpace();
    
    // the contact listener
    space.addContactListener(new SampleContactListener());

    // ghost volume
    var ghost = new PhysicsGhostObject(new BoxCollisionShape(2f));
    space.add(ghost);

    var cylinder = new CylinderCollisionShape(1f, 2f, 1);
    var compound = new CompoundCollisionShape();
    compound.addChildShape(cylinder, new Vector3f(0, 1f, 0));
    vehicle = new PhysicsRigidBody(compound, 1f);
    // only fails with the compound
    // vehicle = new PhysicsRigidBody(cylinder, 1f);

    space.add(vehicle);

    vehicle.setGravity(Vector3f.ZERO);
    vehicle.setPhysicsLocation(new Vector3f(-10, 1.5f, 0));
  }


  @Override
  public void simpleUpdate(float tpf) {
    tick++;
    // move back to start after clearing the ghost
    if (vehicle.getPhysicsLocation().x > 10) {
      vehicle.setPhysicsLocation(new Vector3f(-10, 1.5f, 0));
      // should have triggered only once, otherwise log the count
      if (count == 1) {
        System.out.println("======== Success!! ==========");
      } else {
        System.out.println("======== Invalid count: " + count + " ==========");
      }
      count = 0;
    }
    // only fails when varying velocity
    var fspeed = 30f + random.nextFloat(10f);
    vehicle.setLinearVelocity(new Vector3f(fspeed, 0, 0));
    cam.lookAt(vehicle.getPhysicsLocation(), Vector3f.UNIT_Y);
    super.simpleUpdate(tpf);
  }

  static class SampleContactListener implements ContactListener {

    @Override
    public void onContactStarted(long manifoldId) {
      System.out.printf("(%d) contact started %s\n", tick, manifoldId);
      count++;
    }

    @Override
    public void onContactEnded(long manifoldId) {
      System.out.printf("(%d) contact ended   %s\n", tick, manifoldId);
    }

    @Override
    public void onContactProcessed(PhysicsCollisionObject pcoA, PhysicsCollisionObject pcoB, long manifoldPointId) {
    }
  }
}

Experiment setup:
There are two physics objects involved: A ghost box and a solid compound with a single offsetted cylinder.
The cylinder is moving across the box and it’s expected to register one entry and one exit, the program will check this and print an error if more times are registered.
The experiment is repeated over and over.

Results:
Some iterations go as planned, but others register duplicates:

(11) contact started 140027236966432
(21) contact ended   140027236966432
======== Success!! ==========
(45) contact started 140027236966432
(56) contact ended   140027236966432
======== Success!! ==========
(81) contact started 140027236966432
(91) contact ended   140027236966432
======== Success!! ==========
(116) contact started 140027236966432
(126) contact ended   140027236966432
======== Success!! ==========
(151) contact started 140027236966432
(152) contact ended   140027236966432
(152) contact started 140027236966432
(153) contact ended   140027236966432
(153) contact started 140027236966432
(153) contact ended   140027236966432
(153) contact started 140027236966432
(155) contact ended   140027236966432
(155) contact started 140027236966432
(156) contact ended   140027236966432
(156) contact started 140027236966432
(157) contact ended   140027236966432
(157) contact started 140027236966432
(158) contact ended   140027236966432
(158) contact started 140027236966432
(159) contact ended   140027236966432
(159) contact started 140027236966432
(160) contact ended   140027236966432
(160) contact started 140027236966432
(161) contact ended   140027236966432
======== Invalid count: 10 ==========
(186) contact started 140027236966432
(196) contact ended   140027236966432
======== Success!! ==========
(221) contact started 140027236966432
(232) contact ended   140027236966432
======== Success!! ==========

Notice some things:

  1. The velocity randomly changes on every tick but always with the same sign (doesn’t go back). If I remove the random component the error doesn’t happen.
  2. The cylinder is in a compound, if I omit the compound and just use the cylinder the error doesn’t happen.
  3. The manifold id is printed and it’s always the same.
  4. The contact is interrupted and resumed in the same tick, sometimes even more than once (this opens an opportunity for a workaround).

I’m using Minie v8.0.0, jme 3.6.1, java 17 and linux_amd64.
I’ll appreciate some advise :slight_smile:

1 Like

Duplicate contact events are common and expected. I’ll investigate, but you should design your application to deal with duplicate events.

UPDATE: I ran the test app and didn’t see anything unexpected. In particular, I didn’t see “Invalid count” in the standard output.

Here is typical output:

Debug_Libbulletjme version 21.1.0 initializing
May 07, 2024 2:14:47 PM com.jme3.bullet.objects.PhysicsRigidBody rebuildRigidBody
INFO: Created 7dbaa0152f60.
(11) contact started 138239690731552
(21) contact ended   138239690731552
======== Success!! ==========
(46) contact started 138239690731552
(55) contact ended   138239690731552
======== Success!! ==========
(80) contact started 138239690731552
(90) contact ended   138239690731552
======== Success!! ==========
(113) contact started 138239690731552
(125) contact ended   138239690731552
======== Success!! ==========
(150) contact started 138239690731552
(159) contact ended   138239690731552
======== Success!! ==========
(184) contact started 138239690731552
(194) contact ended   138239690731552
======== Success!! ==========
(218) contact started 138239690731552
(228) contact ended   138239690731552
======== Success!! ==========
(253) contact started 138239690731552
(263) contact ended   138239690731552
======== Success!! ==========
(287) contact started 138239690731552
(297) contact ended   138239690731552
======== Success!! ==========

The manifold ID is the same each time because Bullet re-uses IDs.

Oh, that’s interesting, thanks for trying!
Do you have more updated dependencies?
I see it says Debug_Libbulletjme version 21.1.0 while mine says Libbulletjme version 20.1.0.

I find this behavior strange, based on previous questions on the hub I was expecting duplicated starts with different manifolds, something like “start, start, start, end, end, end” but in my case it’s “start, end, start, end, start, end”.
So it seems it’s telling me that the contact stopped and resumed, while it’s not the case, it’s continuing.

1 Like

Do you have more updated dependencies?
I see it says Debug_Libbulletjme version 21.1.0 while mine says Libbulletjme version 20.1.0.

I have local changes in my Minie codebase, but nothing that should affect this test.

I re-ran the test app. Some runs output “Invalid count” and some don’t. I believe that’s due to randomness built into the test with the Random class.

it seems it’s telling me that the contact stopped and resumed, while it’s not the case, it’s continuing.

Geometrically, that’s a correct picture. However, Bullet creates a contact when discovers overlap between the AABBs of the collision objects, regardless of whether their collision shapes intersect.

In the case of the test app, the new contact manifold typically contains a single point.

Depending on how fast vehicle is moving, Bullet may discover (during the following simulation step) that the relative movement orthogonal to the normal exceeds the collision margin:

In that case, it destroys the contact. Later during the same simulation step, it re-discovers the AABB overlap and creates a new contact, reusing the ID it just destroyed.

This seems inefficient, but I wouldn’t call it a bug. Persistent manifolds in Bullet are primarily for estimating contact forces between rigid bodies. In this situation, one of the bodies is a ghost object, so there is no contact force.

Perhaps it’s time to discuss why you’re using a ContactListener and whether there might be a better way to achieve your goal.

Thanks for trying it again, it’s good to know it’s not just a problem with my setup.

Thanks for the clear explanation

I’m making a framework for making fps games, and I have a layer around minie so the game logic can be scripted over simpler abstractions.
Here I want to detect when two entities touch/overlap and call the script once when the contact starts and once again when the contact ends.
Some example usages for this facility:

  1. Damaging floor: When the player walks over some surfaces damage is applied at a fixed rate (5 HP/sec) so if there are jittery calls it resets the time count and the damage rate cannot be calculated accurately.
  2. Healing water, recharging ammo pads, etc: Same as before but with positive effect.
  3. Swimming mechanic: When the player submerges themself in a body of water the movement mechanic shifts to a simulated diving, switching back to walking when the contact stops.

Generally speaking I’m only interested in solid-ghost continuous overlapping for this.
For solid-solid I have my needs covered with collisions (e.g. a rocket hits an enemy), also solid-ghosts collision is good for one off events (triggering a scripted event when crossing a doorway, etc.).

I believe I can work around this resetting by delaying the event delivery until after the simulation step, but I would really like to hear your advise.

1 Like

Minie’s best solution for intrusion detection depends on the number of intruders (moving objects), the number of zones (sensitive areas), how fast the intruders move, and how precisely you define the shapes of intruders and zones.

  1. As long as the number of intruders-zone pairs is small, you can iterate over them periodically: either a few times per second or after each simulation step.
  • For low-precision detection, you might simply compare the center-to-center squared distance (or the taxicab distance) of each pair to a threshold.
  • For more precise detection of slow-moving intruders, you might use space.pairTest().
  • For fast-moving intruders, you might need to perform a space.sweepTest() for each intruder before each simulation step.
  1. For low-precision detection with (a) many intruders and few zones or (b) many zones and few intruders, it becomes worthwhile to query ghost.getOverlappingObjects() after each simulation step. For this approach, limit the number of ghosts. Also, it may help to put intruders and zones into separate collision groups and all other collision objects into a third group.

  2. For many intruders and many zones, it’s necessary to be more efficient, making thoughtful use of listeners, collision groups, and ignore lists.

  • For low-precision detection, I’d suggest using a collision-group listener (space.addCollisionGroupListener()).
  • For high-precision detection, I’d suggest an ordinary collision listener (space.addCollisionListener()). Ordinary collision events are delayed until after the simulation step.

For a framework, you might want to implement more than one of these approaches. However, I advise starting with simple, straightforward code and deferring optimizations until you’re convinced they’re necessary.

There’s also a page devoted to collision management in the Minie tutorial.

4 Likes

Thank you! That’s a great write up, I bookmarked it to keep as reference.

I believe for my case the ghost.getOverlappingObjects() is a good alternative.

For the time being, however, I enhanced my contact listener implementation with a “debouncer”, so basically I delay the contact started/ended events until the end of the step (tick listener) and I filter out the duplicates.
So far it seems to work as I expect and I believe it doesn’t have a performance impact.

1 Like