Search code examples
javajmonkeyengine

jMonkeyEngine multiple collision events for single collision


I have implemented and added a PhysicsCollisionListener to register when a projectile hits a player. But when a projectile hits a player. multiple events are triggered. I add my listener with bulletAppState.getPhysicsSpace().addCollisionListener(collisionListener) in my simpleInitApp() method. I remove the projectile after the collision.

What I need to do to get only one event for each projectile?

Here is my code:

public void collision(PhysicsCollisionEvent event) {
        //nodeA is a projectile
        if(event.getNodeA().getName().startsWith("Projectile")) {
            //projectile hits player
            if(event.getNodeB().getName().startsWith("Player")) {
                onHit(event.getNodeA(), event.getNodeB().getParent().getUserData("player");
            }
            //projectile hits projectile
            else if(event.getNodeB().getName().startsWith("Projectile")) {
               return; 
            }
            //in any case, remove projectile
            projectileNode.detachChild(event.getNodeA());
            bulletAppState.getPhysicsSpace().remove(event.getNodeA());
        }
        //nodeB is a projectile
        if(event.getNodeB().getName().startsWith("Projectile")) {
            //projectile hits player
            if(event.getNodeA().getName().startsWith("Player")) {
                onHit(event.getNodeB(), event.getNodeA().getParent().getUserData("player");
            }
            //in any case, remove projectile
            projectileNode.detachChild(event.getNodeB());
            bulletAppState.getPhysicsSpace().remove(event.getNodeB());
        }
    }

Solution

  • The problem is that the underlying jBullet engine runs in a different thread at a fixed framerate. If the state of the PhysicsSpace is altered from outside, changes are not immediately recognized.

    To quote the jME Wiki:

    http://hub.jmonkeyengine.org/wiki/doku.php/jme3:advanced:physics_listeners#physics_tick_listener
    Applying forces or checking for overlaps only has an effect right at a physics update cycle, which is not every frame. If you do physics interactions at arbitrary spots in the simpleUpdate() loop, calls will be dropped at irregular intervals, because they happen out of cycle.

    The solution is to remove the physics object from within a PhysicsTickListener which synchronizes the call with the framerate of jBullet. It's also somewhat described in the wiki. This implementation will only produce one collision event:

    private class ProjectileCollisionControl extends GhostControl implements PhysicsTickListener {
        public ProjectileCollisionControl(CollisionShape shape) {
            super(shape);
        }
    
        public void prePhysicsTick(PhysicsSpace space, float tpf) {}
    
        // Invoked after calculations and after events have been queued
        public void physicsTick(PhysicsSpace space, float tpf) {
            for(PhysicsCollisionObject o : getOverlappingObjects()) {
                Spatial other = (Spatial) o.getUserObject();
    
                // I just hit a player, remove myself
                if(other.getName().startsWith("Player"))
                {
                    space.remove(this);
                    space.removeTickListener(this);
                }
            }
        }
    }
    

    The projectiles now need a ProjectileCollisionControl. Setup like this:

    public void simpleInitApp() {
        BulletAppState state = new BulletAppState();
        getStateManager().attach(state);
    
        PhysicsSpace space = state.getPhysicsSpace();
        space.addCollisionListener(new PhysicsCollisionListener()
        {
            public void collision(PhysicsCollisionEvent event) {
                // Same code but without bulletAppState.getPhysicsSpace().remove()
            }
        });
    
        Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/ShowNormals.j3md");
        CollisionShape collisionShape = new BoxCollisionShape(new Vector3f(5, 5, 5));
    
        ProjectileCollisionControl ctrlA = new ProjectileCollisionControl(collisionShape);
        Box a = new Box(new Vector3f(0.4f, 0, 0), 1, 1, 1);
        Geometry boxGeomA = new Geometry("Box A", a);
        boxGeomA.setMaterial(mat);
        boxGeomA.addControl(ctrlA);
    
        ProjectileCollisionControl ctrlB = new ProjectileCollisionControl(collisionShape);
        Box b = new Box(new Vector3f(-0.4f, 0, 0), 1, 1, 1);
        Geometry boxGeomB = new Geometry("Box B", b);
        boxGeomB.setMaterial(mat);
        boxGeomB.addControl(ctrlB);
    
        getRootNode().attachChild(boxGeomA);
        getRootNode().attachChild(boxGeomB);
        space.add(ctrlA);
        space.add(ctrlB);
        space.addTickListener(ctrlA);
        space.addTickListener(ctrlB);
    }