Question about GhostControl.getOverlappingObjects

Hi @sgold, I need your help figuring out how to properly use getOverlappingObjects() method of GhostControl.

The game prototype I would like to replicate is the kamikaze, which detonates itself in the proximity of the player, also damaging the other enemies.
To realize this idea I thought of using a GhostControl, to apply the damage to all physical in-game objects included in the explosive beam.
For convenience, in this example, I move the kamikaze. By pressing the ENTER key, I print on the console all the objects obtained with the GhostControl, but in the result I only get the floor and of course the kamikaze himself, but not the boxes and NPCs.

I wrote an example test case to try to show you my doubt as clearly as possible.

The scenario is this, I have:

  • A floor with MeshCollisionShape, mass = 0, default COLLISION_GROUP_01
  • 15 boxes with BoxCollisionShape, mass = 15, COLLISION_GROUP_02
  • 8 NPC with BetterCharacterControl, mass = 40, COLLISION_GROUP_02
  • A player with a BetterCharacterControl, mass = 40, default COLLISION_GROUP_01 and a GhostControl with COLLISION_GROUP_04

Uncommenting the statemen 1 ghostControl.setCollideWithGroups(PhysicsCollisionObject.COLLISION_GROUP_02);
I thought I get only group 2 objects, instead I get group 1 objects too. I can’t understand the logic. What is the correct setup to get only the objects of a certain group?

Here is the test case 1:

/**
 *
 * @author capdevon
 */
public class Test_OverlappingSphere extends SimpleApplication implements ActionListener {
    
    /**
     * 
     * @param args 
     */
    public static void main(String[] args) {
        Test_OverlappingSphere app = new Test_OverlappingSphere();
        AppSettings settings = new AppSettings(true);
        settings.setResolution(800, 600);
        settings.setFrameRate(60);
        settings.setSamples(4);
        settings.setBitsPerPixel(32);
        settings.setGammaCorrection(true);
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
    }
    
    private BulletAppState physics;
    private Node player;
    private GhostControl ghostControl;
    
    @Override
    public void simpleInitApp() {

        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.6f, 0.7f, 1.0f));
        
        initPhysics();
        createFloor();
        createCharacters(8);
        createBoxItems(15);
        setupPlayer();
        setupChaseCamera();
        setupKeys();
    }
    
    /**
     * Initialize the physics simulation
     *
     */
    public void initPhysics() {
        physics = new BulletAppState();
        stateManager.attach(physics);

        physics.getPhysicsSpace().setAccuracy(0.01f); // 10-msec timestep
        physics.getPhysicsSpace().getSolverInfo().setNumIterations(15);
        physics.setDebugAxisLength(1);
        physics.setDebugEnabled(true);
    }
    
    private void createFloor() {
        Box box = new Box(20, 0.2f, 20);
        Geometry floorGeo = new Geometry("Floor.GeoMesh", box);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.DarkGray);
        floorGeo.setMaterial(mat);
        rootNode.attachChild(floorGeo);

        CollisionShape collShape = CollisionShapeFactory.createMeshShape(floorGeo);
        RigidBodyControl rBody = new RigidBodyControl(collShape, 0f);
        floorGeo.addControl(rBody);
        physics.getPhysicsSpace().add(rBody);
    }
    
    private void createCharacters(int tot) {
        float radius = 5f;
        for (int i = 1; i <= tot; i++) {
            float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius;
            float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius;

            Node node = new Node("Character." + i);
            node.attachChild(createLabel("Ch" + i, ColorRGBA.Green));
            node.setLocalTranslation(x, 1f, z);
            rootNode.attachChild(node);

            BetterCharacterControl bcc = new BetterCharacterControl(.5f, 2f, 40f);
            node.addControl(bcc);
            physics.getPhysicsSpace().add(bcc);
            bcc.getRigidBody().setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
        }
    }

    private void createBoxItems(int nFragments) {

        float halfExtent = 0.4f;

        // Common fields
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.randomColor());
        Box mesh = new Box(halfExtent, halfExtent, halfExtent);
        CollisionShape collShape = new BoxCollisionShape(halfExtent);

        for (int i = 0; i < nFragments; i++) {
            Node node = new Node("Box." + i);
            Geometry geo = new Geometry("Box.GeoMesh." + i, mesh);
            geo.setMaterial(mat);
            node.attachChild(geo);
            node.attachChild(createLabel("Bx" + i, ColorRGBA.Red));
            node.setLocalTranslation(getRandomPoint(10).setY(4));
            rootNode.attachChild(node);

            RigidBodyControl rBody = new RigidBodyControl(collShape, 15f);
            node.addControl(rBody);
            physics.getPhysicsSpace().add(rBody);
            rBody.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
        }
    }
    
    private Vector3f getRandomPoint(int radius) {
        int dx = FastMath.nextRandomInt(-radius, radius);
        int dz = FastMath.nextRandomInt(-radius, radius);
        return new Vector3f(dx, 0, dz);
    }

    public Spatial createLabel(String text, ColorRGBA color) {
        BitmapText bmp = new BitmapText(guiFont, false);
        bmp.setText(text);
        bmp.setColor(color);
        bmp.setSize(1);
        bmp.setBox(new Rectangle((-bmp.getLineWidth() / 2) * bmp.getSize(), 0f, bmp.getLineWidth() * bmp.getSize(), bmp.getLineHeight()));
        bmp.setQueueBucket(RenderQueue.Bucket.Transparent);
        bmp.setAlignment(BitmapFont.Align.Center);
        bmp.addControl(new BillboardControl());

        Node label = new Node("Label");
        label.attachChild(bmp);
        label.setLocalTranslation(0, 2, 0);
        label.scale(0.5f);
        
        return label;
    }
    
    private void setupPlayer() {
        player = new Node("Player");
        Box box = new Box(0.3f, 1.5f, 0.3f);
        Geometry body = new Geometry("Floor.GeoMesh", box);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Orange);
        body.setMaterial(mat);
        player.attachChild(body);
        rootNode.attachChild(player);
        
        BetterCharacterControl bcc = new BetterCharacterControl(.5f, 2f, 40f);
        player.addControl(bcc);
        physics.getPhysicsSpace().add(bcc);
        
        ghostControl = new GhostControl(new SphereCollisionShape(radius));
        player.addControl(ghostControl);
        physics.getPhysicsSpace().add(ghostControl);
        ghostControl.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_04);
		// -statement 1
		//ghostControl.setCollideWithGroups(PhysicsCollisionObject.COLLISION_GROUP_02);
        
        PlayerBaseControl baseControl = new PlayerBaseControl(inputManager);
        player.addControl(baseControl);
    }

    private void setupChaseCamera() {
        // disable the default 1st-person flyCam!
        stateManager.detach(stateManager.getState(FlyCamAppState.class));
        flyCam.setEnabled(false);

        ChaseCamera chaseCam = new ChaseCamera(cam, player, inputManager);
        //Uncomment this to look 2 world units above the target
        chaseCam.setLookAtOffset(Vector3f.UNIT_Y.mult(2));
        chaseCam.setMaxDistance(15);
        chaseCam.setMinDistance(6);
        chaseCam.setRotationSpeed(4f);
    }
    
    private void setupKeys() {
        addMapping("OverlapSphere", new KeyTrigger(KeyInput.KEY_RETURN));
        addMapping("TogglePhysxDebug", new KeyTrigger(KeyInput.KEY_0));
    }
    
    private void addMapping(String mappingName, Trigger... triggers) {
        inputManager.addMapping(mappingName, triggers);
        inputManager.addListener(this, mappingName);
    }
    
    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        //To change body of generated methods, choose Tools | Templates.
        
        if (name.equals("TogglePhysxDebug") && isPressed) {
            boolean debugEnabled = physics.isDebugEnabled();
            physics.setDebugEnabled(!debugEnabled);
            
        } else if (name.equals("OverlapSphere") && isPressed) {
            System.out.println("---overlapRequest---");
            for (PhysicsCollisionObject pco : ghostControl.getOverlappingObjects()) {
                String userObj = pco.getUserObject().toString();
                System.out.println(userObj);
            }
        }
    }
	
}

PlayerControl


import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;

/**
 * 
 * @author capdevon
 */
public class PlayerBaseControl extends AbstractControl implements ActionListener {
    
    private InputManager inputManager;
    private BetterCharacterControl bcc;
    
    boolean _StrafeLeft, _StrafeRight;
    boolean _MoveForward;
    boolean _MoveBackward;
    boolean _TurnLeft;
    boolean _TurnRight;
    boolean ducked;
    float m_MoveSpeed = 2.5f;
    
    Vector3f walkDirection = new Vector3f(0, 0, 0);
    Vector3f viewDirection = new Vector3f(0, 0, 1);
    Vector3f dz = new Vector3f();
    Vector3f dx = new Vector3f();
    Quaternion tempRot = new Quaternion();
    
    /**
     * 
     * @param inputManager 
     */
    public PlayerBaseControl(InputManager inputManager) {
        this.inputManager = inputManager;
    }
    
    @Override
    public void setSpatial(Spatial sp) {
        super.setSpatial(sp);
        if (spatial != null) {
            this.bcc = spatial.getControl(BetterCharacterControl.class);
            registerInputs();
        }
    }
    
    @Override
    public void controlUpdate(float tpf) {
        
        walkDirection.set(Vector3f.ZERO);

        if (_StrafeLeft || _StrafeRight) {
//            float k = _StrafeLeft ? 1f : -1;
//            spatial.getWorldRotation().mult(Vector3f.UNIT_X, dx);
//            walkDirection.addLocal(dx.multLocal(k * m_MoveSpeed));
        }
        if (_MoveForward || _MoveBackward) {
            float k = _MoveForward ? 1 : -1;
            spatial.getWorldRotation().mult(Vector3f.UNIT_Z, dz);
            walkDirection.addLocal(dz.multLocal(k * m_MoveSpeed));
        }
        if (_TurnLeft || _TurnRight) {
            float k = _TurnLeft ? FastMath.PI : -FastMath.PI;
            tempRot.fromAngleNormalAxis(tpf * k, Vector3f.UNIT_Y).multLocal(viewDirection);
            bcc.setViewDirection(viewDirection); //Turn!
        }

        bcc.setWalkDirection(walkDirection); //Walk!
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("StrafeLeft")) {
            _StrafeLeft = isPressed;
        } else if (name.equals("StrafeRight")) {
            _StrafeRight = isPressed;
        } else if (name.equals("MoveForward")) {
            _MoveForward = isPressed;
        } else if (name.equals("MoveBackward")) {
            _MoveBackward = isPressed;
        } else if (name.equals("RotateLeft")) {
            _TurnLeft = isPressed;
        } else if (name.equals("RotateRight")) {
            _TurnRight = isPressed;
        } else if (name.equals("Ducked") && isPressed) {
            ducked = !ducked;
            bcc.setDucked(ducked);
        } else if (name.equals("Jump") && isPressed) {
            bcc.jump();
        }
    }
    
    public void stop() {
//        _RunForward   = false;
        _MoveForward  = false;
        _MoveBackward = false;
        _TurnLeft     = false;
        _TurnRight    = false;
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        //To change body of generated methods, choose Tools | Templates.
    }
            
    /**
     * Custom Keybinding: Map named actions to inputs.
     */
    private void registerInputs() {
        addMapping("StrafeLeft",      new KeyTrigger(KeyInput.KEY_Q));
        addMapping("StrafeRight",     new KeyTrigger(KeyInput.KEY_E));
        addMapping("RunForward",      new KeyTrigger(KeyInput.KEY_SPACE));
        addMapping("MoveForward",     new KeyTrigger(KeyInput.KEY_W));
        addMapping("MoveBackward",    new KeyTrigger(KeyInput.KEY_S));
        addMapping("RotateLeft",      new KeyTrigger(KeyInput.KEY_A));
        addMapping("RotateRight",     new KeyTrigger(KeyInput.KEY_D));
        addMapping("Ducked",          new KeyTrigger(KeyInput.KEY_Z));
    }
    
    private void addMapping(String mapping, Trigger... triggers) {
        inputManager.addMapping(mapping, triggers);
        inputManager.addListener(this, mapping);
    }
    
}

Another strange case, If the GhostControl is associated with an object that never moves, the getOverlappingObject() method always returns all physical objects, regardless of which collision group they belong to.

Here is the test case 2:

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.shapes.BoxCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.GhostControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;

/**
 *
 * @author capdevon
 */
public class Test_CollisionGroups extends SimpleApplication implements ActionListener {

    /**
     * @param args 
     */
    public static void main(String[] args) {
        Test_CollisionGroups app = new Test_CollisionGroups();
        app.start();
    }
    
    private BulletAppState physics;
    private GhostControl ghostControl;
    
    @Override
    public void simpleInitApp() {
        cam.setLocation(Vector3f.UNIT_XYZ.mult(15f));
        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
        flyCam.setMoveSpeed(20f);
        
        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.6f, 0.7f, 1.0f));
        
        initPhysics();
        setupScene();
        setupKeys();
    }
    
    /**
     * Initialize the physics simulation
     *
     */
    public void initPhysics() {
        physics = new BulletAppState();
        stateManager.attach(physics);

        physics.getPhysicsSpace().setAccuracy(0.01f); // 10-msec timestep
        physics.getPhysicsSpace().getSolverInfo().setNumIterations(15);
        physics.setDebugAxisLength(1);
        physics.setDebugEnabled(true);
    }
    
    private void createFloor() {
        Box box = new Box(20, 0.2f, 20);
        box.scaleTextureCoordinates(new Vector2f(10, 10));
        Geometry floorGeo = new Geometry("Floor.GeoMesh", box);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.DarkGray);
        floorGeo.setMaterial(mat);
        rootNode.attachChild(floorGeo);

        CollisionShape collShape = CollisionShapeFactory.createMeshShape(floorGeo);
        RigidBodyControl rBody = new RigidBodyControl(collShape, 0f);
        floorGeo.addControl(rBody);
        physics.getPhysicsSpace().add(rBody);
    }

    private void setupScene() {
        createFloor();
        
        float radius = 5f;
        createCharacters(radius);
        createBoxItems(radius);
        createPlayer(radius);
    }
    
    private void createCharacters(float radius) {
        for (int i = 1; i <= 8; i++) {
            float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius;
            float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius;
            
            Node node = new Node("Character." + i);
            node.setLocalTranslation(x, 1f, z);
            rootNode.attachChild(node);
            
            BetterCharacterControl bcc = new BetterCharacterControl(.5f, 2f, 40f);
            node.addControl(bcc);
            physics.getPhysicsSpace().add(bcc);
            bcc.getRigidBody().setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
        }
    }
    
    private void createBoxItems(float radius) {
        float halfExtent = 0.4f;
        Box mesh = new Box(halfExtent, halfExtent, halfExtent);
        CollisionShape collShape = new BoxCollisionShape(halfExtent);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.randomColor());
        
        for (int i = 1; i <= 8; i++) {
            float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius/2;
            float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius/2;

            Node cube = new Node("Box." + i);
            Geometry geo = new Geometry("Box.GeoMesh." + i, mesh);
            geo.setMaterial(mat);
            cube.attachChild(geo);
            cube.setLocalTranslation(x, 1f, z);
            rootNode.attachChild(cube);

            RigidBodyControl rBody = new RigidBodyControl(collShape, 25f);
            cube.addControl(rBody);
            physics.getPhysicsSpace().add(rBody);
            rBody.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_04);
        }
    }
    
    private void createPlayer(float radius) {
        Node player = new Node("Player");
        rootNode.attachChild(player);
        
        BetterCharacterControl bcc = new BetterCharacterControl(.5f, 2f, 40f);
        player.addControl(bcc);
        physics.getPhysicsSpace().add(bcc);
        
        ghostControl = new GhostControl(new SphereCollisionShape(radius));
        player.addControl(ghostControl);
        physics.getPhysicsSpace().add(ghostControl);
        ghostControl.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_06);
//        ghostControl.setCollideWithGroups(PhysicsCollisionObject.COLLISION_GROUP_02);
    }
        
    private void setupKeys() {
        addMapping("OverlapSphere", new KeyTrigger(KeyInput.KEY_RETURN));
        addMapping("TogglePhysxDebug", new KeyTrigger(KeyInput.KEY_0));
    }
    
    private void addMapping(String mappingName, Trigger... triggers) {
        inputManager.addMapping(mappingName, triggers);
        inputManager.addListener(this, mappingName);
    }
    
    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        //To change body of generated methods, choose Tools | Templates.
        
        if (name.equals("TogglePhysxDebug") && isPressed) {
            boolean debugEnabled = physics.isDebugEnabled();
            physics.setDebugEnabled(!debugEnabled);
            
        } else if (name.equals("OverlapSphere") && isPressed) {
            System.out.println("--getOverlappingObjects");
            for (PhysicsCollisionObject pco : ghostControl.getOverlappingObjects()) {
                String userObj = pco.getUserObject().toString();
                System.out.println("\t" + userObj);
            }
        }
    }

    @Override
    public void simpleUpdate(float tpf) {
        //TODO: add update code
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }
}

1 Like

In my opinion, getOverlappingObjects() isn’t very useful. It’s based entirely on axis-aligned bounding boxes, which are often much larger than the corresponding collision shapes. Ghosts might be useful when there are hundreds of shapes, as a “first cut” to identify potential overlaps, but that’s about it. They’re included in Minie mostly for compatibility with the older physics libraries.

I also don’t find collision groups to be very useful. But if they work for your application, that’s fine.

To detect actual overlap between collision shapes, I would probably use PhysicsSpace.contactTest().

1 Like

ok thanks, I’ll do some tests and publish the result

1 Like

hi @sgold, after various tests I have written the solutions in two compact methods to hide all the technical details in a simple API.

The goal is to get all the physical objects inside a sphere centered at a desired point in the game world.

Let me know if you think these ideas are good, improvable, or bad.

The first method is based on your suggestion to use PhysicsSpace.contactTest.

Here is the method 1:

public class PhysxQuery {

    /**
     * DefaultRaycastLayers: ALL LAYERS
     */
    private static final int DefaultRaycastLayers = ~0;

	public static Set<Spatial> contactTest(Vector3f position, float radius) {
		return contactTest(position, radius, DefaultRaycastLayers);
	}

	/**
	 * Computes and stores colliders inside the sphere.
	 * @param position	- Center of the sphere.
	 * @param radius	- Radius of the sphere.
	 * @param layerMask	- A Layer mask defines which layers of colliders to include in the query.
	 * @return 
	 */
	public static Set<Spatial> contactTest(Vector3f position, float radius, int layerMask) {

		Set<Spatial> overlappingObjects = new HashSet<>(5);
		PhysicsGhostObject ghost = new PhysicsGhostObject(new SphereCollisionShape(radius));
		ghost.setPhysicsLocation(position);

		int numContacts = PhysicsSpace.getPhysicsSpace().contactTest(ghost, new PhysicsCollisionListener() {
			@Override
			public void collision(PhysicsCollisionEvent event) {
				// ghost is not linked to any Spatial, so one of the two nodes A and B is null.
				PhysicsCollisionObject pco = event.getNodeA() != null ? event.getObjectA() : event.getObjectB();
				logger.log(Level.INFO, "NodeA={0} NodeB={1} CollGroup={2}", new Object[]{event.getNodeA(), event.getNodeB(), pco.getCollisionGroup()});
				
				if (applyMask(layerMask, pco.getCollisionGroup())) {
					Spatial userObj = (Spatial) pco.getUserObject();
					overlappingObjects.add(userObj);
				}
			}
		});

		System.out.println("numContacts: " + numContacts);

		return overlappingObjects;
	}
	
	/**
	 * Check if a collisionGroup is in a layerMask
	 *
	 * @param layerMask
	 * @param collisionGroup
	 * @return
	 */
	private boolean applyMask(int layerMask, int collisionGroup) {
		return layerMask == (layerMask | collisionGroup);
	}
	
}

Usage:

Vector3f position = ...;
float radius = 5f;
int enemyLayer = PhysicsCollisionObject.COLLISION_GROUP_02;
Set<Spatial> set = PhysxQuery.contactTest(position, radius, enemyLayer);
System.out.println(StringUtils.join(set, "; "));

The result is better than GhostControl. It detects collisions well with the BetterCharacterControl, but is inaccurate with the Box.

Figure 1: Box1 is detected, while Box12 is not.

Figure 2: Box1 is detected, while Box12 is not.

Figure 3: Character3 and Character4 are not detected, correct.

The second method is based on simple mathematics, and the results are always predictable, but with the limitation that physical objects are only detected if their origin is within the sphere.

Here is the method 2:

public class PhysxQuery {

    /**
     * DefaultRaycastLayers ALL LAYERS
     */
    private static final int DefaultRaycastLayers = ~0;
    /**
     * IdentityFunction
     */
    private static final Function<PhysicsRigidBody, Boolean> IdentityFunction = x -> true;
	
    /**
     * Computes and stores colliders inside the sphere.
     * 
     * @param position	- Center of the sphere.
     * @param radius	- Radius of the sphere.
     * @param layerMask	- A Layer mask defines which layers of colliders to include in the query.
     * @param func		- Specifies a function to filter colliders.
     * @return
     */
	public static List<PhysicsRigidBody> overlapSphere(Vector3f position, float radius, int layerMask, Function<PhysicsRigidBody, Boolean> func) {
		
		logger.log(Level.INFO, "PhysxQuery--position: {0}, radius: {1}", new Object[]{position, radius});

		List<PhysicsRigidBody> lst = new ArrayList<>(10);
		for (PhysicsRigidBody rgb : PhysicsSpace.getPhysicsSpace().getRigidBodyList()) {
		
			if (applyMask(layerMask, rgb.getCollisionGroup()) && func.apply(rgb)) {

				Vector3f distance = rgb.getPhysicsLocation().subtract(position);
				if (distance.length() < radius) {
					lst.add(rgb);
				}
			}
		}
		return lst;
	}
	
	public static List<PhysicsRigidBody> overlapSphere(Vector3f position, float radius, int layerMask) {
        return overlapSphere(position, radius, layerMask, IdentityFunction);
    }

    public static List<PhysicsRigidBody> overlapSphere(Vector3f position, float radius) {
        return overlapSphere(position, radius, DefaultRaycastLayers, IdentityFunction);
    }
	
	/**
	 * Check if a collisionGroup is in a layerMask
	 *
	 * @param layerMask
	 * @param collisionGroup
	 * @return
	 */
	private boolean applyMask(int layerMask, int collisionGroup) {
		return layerMask == (layerMask | collisionGroup);
	}
	
}

As you can see, I use a BitMask to filter objects based on their collision group. An integrated solution that I found very practical to quickly identify the physical objects I need, without having to use a system based on names or userData.

For example:

// only layers 2 and 3
int a = PhysicsCollisionObject.COLLISION_GROUP_02 | PhysicsCollisionObject.COLLISION_GROUP_03;
// every layer except layer 1
int b = ~PhysicsCollisionObject.COLLISION_GROUP_01;
1 Like

It’s been a really long time but I think the only way that I was able to get this sort of “area of effect” to work using ghost objects was to collect the collision events during a frame and then filter out the ones that I wasn’t interested in.

2 Likes

I hate deciphering code fragments like these: out of context in a too-small scroll panel. Any chance of getting a complete test app that I could execute myself?

For Method 1, you might try using some collision shape other than SphereCollisionShape: MultiSphere, for instance.

you’re right, my fault. I uploaded all the classes to github. The test I was talking about, you can find it here. Thanks

Edit:
Use the WASD keys to move the player near the boxes and capsules.

  • Press the ENTER key to identify objects that collide with the sphere centered in the player’s origin, using the contactTest method.
  • Press the SPACE key to identify objects whose origin is within the radius of the sphere centered in the player’s origin, using the overlapSphere method.

The objects returned by the two methods are printed on the console.

1 Like

This might be overkill for a simple game or a demo app but for a complex game, I would use an improved version of method 2.

A GridSystem that periodically (i.e every 1s) iterates through all mobs (for static objects this is only needed to be done once) and calculates their grid cell and puts them in a map indexed by cell.

Map<Cell, RigidBody> bodyIndex = ... // if cell size is small enough that only one object can be in a cell at the same time

or

Map<Cell, List<RigidBody>> bodiesIndex = ... // if cell size is large enough that multiple objects can be in a cell at the same time

An example grid implementation

1 Like

@Ali_RS and @pspeed your approach is also interesting. Your idea is very similar to the Area object (see Signals), integrated into the Godot Engine with a very simple API.
https://docs.godotengine.org/en/stable/classes/class_area.html#signals

  • body_entered ( Node body )
  • body_exited ( Node body )

This discussion explains how to use it.

// Start Edit
compared to other engines, jMonkeyEngine does not have these built-in methods, but only has the PhysicsCollisionListener interface that exposes the
public void collision (PhysicsCollisionEvent event);
// End Edit

With my 2 proposals, I was hoping to write a compact solution, perhaps not great for a large area with lots of objects, but simple to use in a demo for anyone like in Unity.

I would like to know if I am correctly using the new PhysicsSpace.contactTest method of the Minie4.0.2 library, or if my approach is to be discarded. I don’t know if I’ll ever write a full game, for the moment I enjoy writing demos and sharing ideas with the community. As a programmer, I like to understand how things work. At the moment I am focusing on the mechanics of the kamikaze soldier of the Zombie Army game :sweat_smile:

I will keep you updated with the progress. Let me know if you have any other feedback. Thanks.

3 Likes

I’m ready to dive back into this discussion, starting with the Test_OverlappingSphere app.

1 Like

I’ve studied Test_OverlappingSphere (as it existed on 29 March) and I’m convinced you were using contactTest() correctly.

However, I’m seeing unexpected behavior that may indicate a bug in Bullet: sometimes the test reports contact with a character or box even though there’s a visible gap. Further investigation seems warranted.

1 Like

thank you, I await the outcome of the investigation with curiosity. :grinning:

1 Like

Bullet’s contact-test method generates contact points, which Minie converts into PhysicsCollisionEvent instances. One of the properties of a contact point is its distance1, which appears to be the distance between the colliding objects—in other words, the negative of the penetration depth.

For most contacts, distance1 is negative, as you’d expect, but sometimes it’s positive. I’m not sure why Bullet reports contacts with negative penetration depth. They might be important for simulating contact forces. But for contact tests, I think we should filter them out before the application can see them.

For now, the suggested workaround would be to add

if (event.getDistance1() > 0f) return;

to all collision listeners.

1 Like

Thank you so much @sgold for your time. With your suggestion it works now.

Do you think would using a ‘MultiSphere’ instead of a ‘SphereCollisionShape’ further improve the result?

One last thing, is it normal that in debug mode with DeugAsixLenght = 1, the debug axes do not follow the rotation of the capsules of the BetterCharacterControl? (see Test_OverlappingSphere.java)

physics.setDebugAxisLength(1);
physics.setDebugEnabled(true);
1 Like

Glad to know that the workaround works for you.

I expect MultiSphere with n=1 would be less efficient than SphereCollisionShape. I only suggested it because SphereCollisionShape has peculiar limitations that sometimes cause issues.

The collision object of a BetterCharacterControl does not rotate (even though the model does). That’s why the physics axes don’t rotate. So yes, what you saw is normal.

1 Like

Ok thank you. I commit the workaround code on github.
We can consider the topic SOLVED. Here is the final code:

    /**
     * Computes and stores colliders inside the sphere.
     *
     * @param position	- Center of the sphere.
     * @param radius	- Radius of the sphere.
     * @param layerMask	- A Layer mask defines which layers of colliders to include in the query.
     * @return
     */
    public static Set<Spatial> contactTest(Vector3f position, float radius, int layerMask) {

        Set<Spatial> overlappingObjects = new HashSet<>(5);
        PhysicsGhostObject ghost = new PhysicsGhostObject(new SphereCollisionShape(radius));
        ghost.setPhysicsLocation(position);

        int numContacts = PhysicsSpace.getPhysicsSpace().contactTest(ghost, new PhysicsCollisionListener() {
            @Override
            public void collision(PhysicsCollisionEvent event) {
            	
            	if (event.getDistance1() > 0f) {
            		// Discard contacts with positive distance between the colliding objects
            		return;
            	}

                // ghost is not linked to any Spatial, so one of the two nodes A and B is null.
                PhysicsCollisionObject pco = event.getNodeA() != null ? event.getObjectA() : event.getObjectB();
                logger.log(Level.INFO, "NodeA={0}, NodeB={1}, CollGroup={2}", new Object[]{event.getNodeA(), event.getNodeB(), pco.getCollisionGroup()});

                if (applyMask(layerMask, pco.getCollisionGroup())) {
                    Spatial userObj = (Spatial) pco.getUserObject();
                    overlappingObjects.add(userObj);
                }
            }
        });

        System.out.println("numContacts: " + numContacts);
        return overlappingObjects;
    }
	
	/**
     * Check if a collisionGroup is in a layerMask
     *
     * @param layerMask
     * @param collisionGroup
     * @return
     */
    private static boolean applyMask(int layerMask, int collisionGroup) {
        return layerMask == (layerMask | collisionGroup);
    }

Usage:

Vector3f position = ... ;
float radius = 5f;
int layer = PhysicsCollisionObject.COLLISION_GROUP_02;

Set<Spatial> set = PhysxQuery.contactTest(position, radius, layer);
1 Like

Looks fine, but perhaps it can be improved.

  1. You don’t need to create a new ghost for each test. You could create one during init (or lazily) and reuse it for each test.

  2. The assumption that getUserObject() returns a Spatial is violated for DynamicAnimControl—and also for any collision object not created by a physics control.

  3. In a serious game, I wouldn’t treat spatials as game objects. Instead, I would use setApplicationData() to link collision objects to game objects. In that context, contactTest() should return a set of game objects.

Perhaps it could be generalized like this:

Method 1:

    public static Set<PhysicsCollisionObject> contactTest(Vector3f position, PhysicsGhostObject ghost, int layerMask) {
    	
    	Set<PhysicsCollisionObject> overlappingObjects = new HashSet<>(5);
        ghost.setPhysicsLocation(position);

        int numContacts = PhysicsSpace.getPhysicsSpace().contactTest(ghost, new PhysicsCollisionListener() {
            @Override
            public void collision(PhysicsCollisionEvent event) {
            	
            	if (event.getDistance1() > 0f) {
            		return;
            	}

                // ghost is not linked to any Spatial, so one of the two nodes A and B is null.
                PhysicsCollisionObject pco = event.getNodeA() != null ? event.getObjectA() : event.getObjectB();

                if (applyMask(layerMask, pco.getCollisionGroup())) {
                    overlappingObjects.add(pco);
                }
            }
        });

        return overlappingObjects;
    }

Or insert a function in the parameters to allow the user to filter objects in the way that best suits his needs.

Method 2:

    public static Set<PhysicsCollisionObject> contactTest(Vector3f position, PhysicsGhostObject ghost, Function<PhysicsCollisionObject, Boolean> func) {
    	
    	Set<PhysicsCollisionObject> overlappingObjects = new HashSet<>(5);
        ghost.setPhysicsLocation(position);

        int numContacts = PhysicsSpace.getPhysicsSpace().contactTest(ghost, new PhysicsCollisionListener() {
            @Override
            public void collision(PhysicsCollisionEvent event) {
            	
            	if (event.getDistance1() > 0f) {
            		return;
            	}

                // ghost is not linked to any Spatial, so one of the two nodes A and B is null.
                PhysicsCollisionObject pco = event.getNodeA() != null ? event.getObjectA() : event.getObjectB();

                if (func.apply(pco)) {
                    overlappingObjects.add(pco);
                }
            }
        });

        return overlappingObjects;
    }

I did not understand point 3, can you give me an example please?

Suppose the game is zombies versus tanks. In that case, there should be some Java class used to represent both tanks and zombies. Call that class GameObject.

Each zombie and tank might have “hit points” to keep track of partial damage. Each might be composed of multiple collision objects (to simulate bones and turrets). Each might be visualized by multiple spatials (one for tactical/plan view, one for the first-person view). All this information should be maintained in the game object, not in collision objects or spatials.

When the player selects a zombie with the mouse pointer, the ray collision determines a geometry. In order to quickly map the geometry to a game object, the model rootnode of each 3-D model should keep reference to its corresponding game object, as “user data”

When a zombie encounters an area effect, the contact test determines a collision object. In order to quickly map the collision object to a game object, each collision object should keep a reference to its corresponding game object, as “application-specific data”

got it, I’ll keep that in mind. thank you

1 Like