Node.collideWith() question

I’m trying to cast a ray outward from the player in the direction he’s looking and using Node.collideWith(other, result) and CollisionResults to sense collisions between the ray and Nodes contained in classes implementing Interactable and storing the closest collision in a CollisionResult. Basically, it’s like the HelloPicking tutorial.
I want to call a method from the class instance containing the Node with the closest collision, but I can’t because that particular class instance is “lost”.

Here is my picking code:

protected void interact(ArrayList<Interactable> interacts, Vector3f lookDirect) {
	CollisionResults results = new CollisionResults();
	Ray ray = new Ray(pos, lookDirect);
	for (Interactable i : interacts) i.getNode().collideWith(ray, results);
	if (results.size() > 0) {
		CollisionResult closest = results.getClosestCollision();
		// I need to access the class instance that has the Node with the closest collision result.
	}
}
1 Like

You can attach whatever objects you like into a Spatial, this is done like this:

    Geometry yourGeometry = render();
    yourGeometry.setUserData("owner", someObject);

Then later you can get that object back

    SomeClass owner = (SomeClass)closest.getGeometry().getUserData("owner")
1 Like

If I create my Geometry inside the class, will this work?

// in class weapon
Geometry geom = new Geometry("a box", aBox);
geom.setUserData("a weapon", this);
Weapon owner = (Weapon)closest.getGeometry().getUserData("a weapon");

But wait, I can’t just set “a weapon” as the name for every single weapon I make. I need a field in Weapon to define that, so how would I access that specific name field?

The first string is your key, in your case the key would always be “weapon”. I’m not using that as a placeholder for many different weapons, I literally mean those characters.

Assuming a Geom only ever has one weapon associated with it, then you’re done. You can ask the geometry for the specific weapon it was created with.

Possibly I’m not fully understanding, but you can have whatever fields you like in your Weapon class, including its name (or Weapon can be an interface with many potential weapon types)

Shouldn’t you have to have a unique key for each instance? Or am I missing something here?

Each Geometry has exactly one weapon associated with it right? (because it is the geometry of the weapon). And you have the geometry, but want the weapon.

When creating

Geometry geom = new Geometry("a box", aBox);
geom.setUserData("weapon", this);

Looking at your picking code again it looks different from what I’d expect. Usually you’d pick against the root node (i.e. the entire world at once) and see what you hit with your ray. So something like this:

protected void interact(Node rootNode, Vector3f startPosition, Vector3f lookDirection) {
    CollisionResults results = new CollisionResults();
    Ray ray = new Ray(startPosition, lookDirection);
    rootNode.collideWith(ray, results);
    if (results.size() > 0) {
        CollisionResult closest = results.getClosestCollision();
        Weapon weapon = (Weapon)closest.getGeometry().getUserData("weapon");

        if (weapon!= null) {
            //do whatever happens when a weapon gets picked
            weapon.kill();
        }
    }
}

Or, if you want to be able to pick not just the closest, but everything you are looking at

protected void interact(Node rootNode, Vector3f startPosition, Vector3f lookDirection) {
    CollisionResults results = new CollisionResults();
    Ray ray = new Ray(startPosition, lookDirection);
    rootNode.collideWith(ray, results);
    for(CollisionResult result : results){
        Weapon weapon = (Weapon)result.getGeometry().getUserData("weapon");

        if (weapon!= null) {
            //do whatever happens when a weapon gets picked
            weapon.kill();
        }
    }
}

Although I guess there’s nothing wrong with prepicking out what you can pick

protected void interact(ArrayList<Interactable> interacts, Vector3f lookDirect) {
    CollisionResults results = new CollisionResults();
    Ray ray = new Ray(pos, lookDirect);
    for (Interactable i : interacts) i.getNode().collideWith(ray, results);
    if (results.size() > 0) {
        CollisionResult closest = results.getClosestCollision();
        Weapon weapon = (Weapon)closest.getGeometry().getUserData("weapon");

        if (weapon!= null) {
            //do whatever happens when a weapon gets picked
            weapon.kill();
        }
    }
}

Of course everything but the first example isn’t blocked by other geometry, its an “X ray pick” rather than a normal sight pick. Which may or may not be what you want

What if I pick a Wall instead of a Weapon?

Then that user data of Weapon will be null. And your game will have to do whatever is appropriate (probably nothing).

In my game I have an interface called ToolAffected. And that interface just has a method called shortInteractOnMe. And most of my pickable objects implement that interface and the object itself decides what to do if its picked

public interface ToolAffected {
    /**
     * Respond to a short interact (aka click)
     */
    void shortInteractOnMe(CollisionResult pickingResult);
}

So both your Wall and Weapon can implement this interface and decide what they do in response to being picked. Maybe a Barrel explodes on being picked, a Weapon is collected on being picked and a Wall does nothing

public class Barrel implements ToolAffected{

    @Override
    public void shortInteractOnMe(CollisionResult pickingResult) {
        explode();

    }

    public Geometry render(){
        Geometry geom = new Geometry("a box", new Box(1,1,1));
        geom.setUserData("ToolAffected", this);

        return geom;
    }

    private void explode() {
        //do whatever
    }

}

And then the picking code doesn’t care which particular ToolAffected gets picked

protected void interact(Node rootNode, Vector3f startPosition, Vector3f lookDirection) {
    CollisionResults results = new CollisionResults();
    Ray ray = new Ray(startPosition, lookDirection);
    rootNode.collideWith(ray, results);
    if (results.size() > 0) {
        CollisionResult closest = results.getClosestCollision();
        ToolAffected toolAffected = (ToolAffected)closest.getGeometry().getUserData("ToolAffected");

        if (toolAffected != null) {
            //let tool affected decide for itself what happens
            toolAffected.shortInteractOnMe(closest);
        }
    }
}
1 Like

Ok, that makes sense. I’m using an interface called Interactable, which does about what your ToolAffected does.

I’m still a little confused about the key. Does it point to a class and the geometry will “claim” the instance of that class where it was created?

You can think of it like this:

If the Geometry class had been created just for you, it would have looked like this

public class Geometry {

    //all the normal geometry stuff
    ToolAffected toolAffected;
    //all the normal geometry stuff

    public ToolAffected getToolAffected() {
        return toolAffected;
    }

    public void setToolAffected(ToolAffected toolAffected) {
        this.toolAffected = toolAffected;
    }
}

And then I would have used it like this

    public Geometry render(){
        Geometry geom = new Geometry("a box", new Box(1,1,1));
        geom.setToolAffected(this);

        return geom;
    }

And later I would have asked for it back

    ToolAffected toolAffected = geom.getToolAffected();

And that would have been nice and easy. But of course Geometry wasn’t created just for you and has no idea about your classes, so isn’t written like that. The user data simulates this. You can think of the key as a member variable on the Geometry object. But unlike a real member variable its added at run time, rather than compile time.

Or in other words, you give the geometry object an object to hold on to. And say when I pass this string I want it back (and of course you could give it several objects under several keys). And each geometry of course keeps its own separate set, just like normal objects keep their state separate from each other

1 Like

One thing I forgot to mention. Your version of ToolAffected will need to extend the interface Savable or else it will claim to be unsupported.

As long as you don’t actually want JME to handle saving and loading of geometries you can ignore those methods though.

(I personally find the idea of “saving” geometries weird and have state and graphics very separate. But if you have them intermingled you will want to care about the Savable’s write and read methods so your classes save and load correctly)

I’ve tried it out (finally), but every single time owner becomes null. I’ve tried it with and without Interactable extending Savable.

Is there anything I’m supposed to do with methods write and read from Savable?

“I get a collision but the thing I expect to be there isn’t there.”

99/100 times this is because you get a collision on a geometry but the thing you expect is on a parent node.

Without seeing an example of your code, that’s the best we can say. You don’t need to implement write/read unless you plan to save the objects on disk.

My updated picking code:

protected void interact(ArrayList<Interactable> interacts, Vector3f lookDirect) {
	if (interact) {
		CollisionResults results = new CollisionResults();
		Ray ray = new Ray(pos, lookDirect);
		for (Interactable i : interacts) i.getNode().collideWith(ray, results);
		if (results.size() > 0) {
			CollisionResult closest = results.getClosestCollision();
			Interactable owner = (Interactable)closest.getGeometry().getUserData(Interactable.KEY);
			if (owner != null) owner.onPick(this);
		}
	}
}

Interactable interface:

public interface Interactable {
	
	public static final String KEY = "Interactable";
	
	abstract void onPick(Player p);
	abstract Node getNode();
	
}

Each node that is Interactable I do this:

node.setUserData(Interactable.KEY, this);

Note how it says “getGeometry()”

Note how it says “node”.

Probably your node is a parent of the geometry or a parent of its parent or a parent of that parent. Only you can say.

Ok, but if I instead set the user data for the geometries like this:

geom.setUserData(Interactable.KEY, this);

I get an exception: IllegalArgumentException: Unsupported type: Map.Wall
(I’m testing the picking with Map.Wall btw)

Edit: Is that what Savable is for?

In Java, don’t bother to tell us about exceptions without a stack trace. You might as well just say “I got an error” because you left out 99% of the useful information.

Your options are:

  1. add your object to every single geometry that might exist in all of the children under that node anywhere. (UGH)
  2. traverse up the parent hierarchy until you find a node that has what you are looking for.
  3. use a picking framework that already does all of this for you because your approach is a little strange.

Taking a step back… what is your ultimate goal with all of this? Why are you picking from a list instead of hitting a node with all of your pickable stuff? etc.

You’re right, I shouldn’t be going through a list to find collisions. I’ve changed up the code so now I’m going through Node scene, which is directly attached to rootNode (an improvement, at least).

Ultimate goal: Have a number of items implementing Interactable in the scene that will execute a particular method whenever it is the closest result of a pick. But, I do not want the player to be involved in the picking (so the player doesn’t turn out as the closest result every time).

Edit: here’s that stack trace:

com.jme3.app.LegacyApplication handleError
SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.IllegalArgumentException: Unsupported type: Map.Wall
	at com.jme3.scene.UserData.getObjectType(UserData.java:124)
	at com.jme3.scene.Spatial.setUserData(Spatial.java:1539)
	at Map.Wall.initPosiWall(Wall.java:112)
	at Map.Wall.build(Wall.java:158)
	at Map.Room.buildTop(Room.java:181)
	at Map.Room.buildRoom(Room.java:166)
	at Map.MansionMap.initMainRooms(MansionMap.java:49)
	at Map.MansionMap.create(MansionMap.java:31)
	at Map.MansionMap.<init>(MansionMap.java:23)
	at Map.MapState.initialize(MapState.java:54)
	at com.jme3.app.state.BaseAppState.initialize(BaseAppState.java:124)
	at com.jme3.app.state.AppStateManager.initializePending(AppStateManager.java:251)
	at com.jme3.app.state.AppStateManager.update(AppStateManager.java:281)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:236)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:197)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:748)

In case it matters and you are interested, Lemur has scene picking build in. You just add listeners to whatever node/geometry you like and you will get notified.

Deeper old example here: Lemur Gems #3 : Scene picking

…but the short version is:

MouseEventControl.addListenersToSpatial(node, new DefaultMouseListener() {
        protected void click( MouseButtonEvent event, Spatial target, Spatial capture ) {
            // do the stuff
        }
    });

Then whenever the mouse clicks on ‘node’, the click method is called.

Edit: also note that you can use the scene picking even if you don’t use the rest of Lemur for your UI or anything.

3 Likes