JME2 Starter 7 - Hello MousePick


« Previous: Starter Tutorial 6 - Hello ModelLoading
Next: Starter Tutorial 8 - Hello Intersection »


(Tip: Up-to-date source files for the tutorials are always in the repository)


This program introduces how to determine which item in the 3D world the user clicks (or "shoots" at). You will learn how to aim using Rays and Intersections. The tutorial explains the InputSystem and the difference between AbsoluteMouse versus RelativeMouse. You will also learn how to create your own mouse pointer.

Code Sample

import java.net.URL;
import java.util.logging.Logger;
 
import com.jme.app.AbstractGame;
import com.jme.app.SimpleGame;
import com.jme.app.AbstractGame.ConfigShowMode;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.AbsoluteMouse;
import com.jme.input.FirstPersonHandler;
import com.jme.input.MouseInput;
import com.jme.intersection.BoundingPickResults;
import com.jme.intersection.PickResults;
import com.jme.math.Ray;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Box;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.util.TextureManager;
import com.jme.scene.state.BlendState;
 
/**
 * Started Date: Jul 22, 2004 <br>
 * <br>
 *
 * Demonstrates picking with the mouse.
 *
 * @author Jack Lindamood
 */
public class HelloMousePick extends SimpleGame {
    private static final Logger logger = Logger.getLogger(HelloMousePick.class
            .getName());
 
	// This will be my mouse
	AbsoluteMouse am;
 
	// This will be he box in the middle
	Box b;
 
	PickResults pr;
 
	public static void main(String[] args) {
		HelloMousePick app = new HelloMousePick();
		app.setConfigShowMode(ConfigShowMode.AlwaysShow);
		app.start();
	}
 
	protected void simpleInitGame() {
		// Create a new mouse. Restrict its movements to the display screen.
		am = new AbsoluteMouse("The Mouse", display.getWidth(), display
				.getHeight());
 
		// Get a picture for my mouse.
		TextureState ts = display.getRenderer().createTextureState();
		URL cursorLoc = HelloMousePick.class.getClassLoader().getResource(
                "jmetest/data/cursor/cursor1.png" );
		Texture t = TextureManager.loadTexture(cursorLoc, Texture.MinificationFilter.BilinearNearestMipMap,
        		Texture.MagnificationFilter.Bilinear);
		ts.setTexture(t);
		am.setRenderState(ts);
 
		// Make the mouse's background blend with what's already there
		BlendState as = display.getRenderer().createBlendState();
		as.setBlendEnabled(true);
		as.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		as.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
		as.setTestEnabled(true);
		as.setTestFunction(BlendState.TestFunction.GreaterThan);
		am.setRenderState(as);
 
		// Get the mouse input device and assign it to the AbsoluteMouse
		// Move the mouse to the middle of the screen to start with
		am.setLocalTranslation(new Vector3f(display.getWidth() / 2, display
				.getHeight() / 2, 0));
		// Assign the mouse to an input handler
		am.registerWithInputHandler( input );
 
		// Create the box in the middle. Give it a bounds
		b = new Box("My Box", new Vector3f(-1, -1, -1), new Vector3f(1, 1, 1));
		b.setModelBound(new BoundingBox() );
		b.updateModelBound();
		// Attach Children
		rootNode.attachChild(b);
		rootNode.attachChild(am);
		// Remove all the lightstates so we can see the per-vertex colors
		lightState.detachAll();
		b.setLightCombineMode( LightCombineMode.Off );
		pr = new BoundingPickResults();
		(( FirstPersonHandler ) input ).getMouseLookHandler().setEnabled( false );
	}
 
	protected void simpleUpdate() {
		// Get the mouse input device from the jME mouse
		// Is button 0 down? Button 0 is left click
		if (MouseInput.get().isButtonDown(0)) {
			Vector2f screenPos = new Vector2f();
			// Get the position that the mouse is pointing to
			screenPos.set(am.getHotSpotPosition().x, am.getHotSpotPosition().y);
			// Get the world location of that X,Y value
			Vector3f worldCoords = display.getWorldCoordinates(screenPos, 0);
			Vector3f worldCoords2 = display.getWorldCoordinates(screenPos, 1);
			logger.info( worldCoords.toString() );
			// Create a ray starting from the camera, and going in the direction
			// of the mouse's location
			Ray mouseRay = new Ray(worldCoords, worldCoords2
					.subtractLocal(worldCoords).normalizeLocal());
			// Does the mouse's ray intersect the box's world bounds?
			pr.clear();
			rootNode.findPick(mouseRay, pr);
 
			for (int i = 0; i < pr.getNumber(); i++) {
				pr.getPickData(i).getTargetMesh().setRandomColors();
			}
		}
	}
}

Introducing AbsoluteMouse

The first new part here is when I declare my AbsoluteMouse (jME API):

// This will be my mouse
AbsoluteMouse am;

We use an AbsoluteMouse object to hold the absolute position of the mouse pointer. As you see when running the example code, an AbsoluteMouse allows the pointer to move freely on the screen – you recognize this as the typical mouse behaviour in RTS (Real-Time Strategy) games. JME also supports a RelativeMouse (jME API), which is like +mouselook in Quake, or in any other first-person shooter: It doesn’t care about the mouse’s position on the screen, just where you’re moving the mouse. You have to decide which mouse behaviour makes sense in your game, here we choose AbsoluteMouse.

The next new thing is when I create the AbsoluteMouse object:

// Create a new mouse. Restrict its movements to the display screen.
am = new AbsoluteMouse("The Mouse", display.getWidth(), 
	display.getHeight());

AbsoluteMouse extends Mouse, Mouse extends Geometry, Geometry extends Spatial.

As we know, all Spatials must have a name. The name of this spatial is “The Mouse”. It makes sense. All things that are drawn are Geometry objects. So to draw the mouse on the screen, we make it a Geometry. The two numbers at the end restrict the movement of the mouse. If it was 40,40 then the mouse could only move in the lower 40,40 of the screen. We simply restrict it to the size of the display. The object display is created for us in SimpleGame.

Introducing the BlendState

Next we add our own partially transparent mouse pointer graphic. We create Blend state, this is jME's way of making things transparent:

// Make the mouse's background blend with what's already there
BlendState as = display.getRenderer().createBlendState();
as.setBlendEnabled(true);
as.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
as.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
as.setTestEnabled(true);
as.setTestFunction(BlendState.TestFunction.GreaterThan);
am.setRenderState(as);

That’s a lot of new and strange things there, don't worry. You should understand that, if I didn’t use a BlendState, my program would look like this:

That’s because my cursor is a square picture, and without the BlendsState, there is no transparency. Doesn’t look very nice, does it? What I need is to tell jME to blend the cursor’s transparent color with what’s already there. That’s what the BlendState (jME API) does.

Color blending for BlendStates is divided into a source function and destination function. jME’s blendstates work in a similar way to OpenGL alpha states. For more information, look up alpha states for OpenGL and how they are used. For now, just use what I told you.

Next, I move the mouse to the center of the screen for the user:

// Get the mouse input device and assign it to the AbsoluteMouse
// Move the mouse to the middle of the screen to start with
am.setLocalTranslation(new Vector3f(display.getWidth() / 2, display
	.getHeight() / 2, 0));

Again, I use display.getWidth() to figure out the width of the window. Notice the setLocalTranslation I use is just like the one I use for other Spatials.

Handeling Mouse Input

Next, I tie the mouse to an input handler:

// Assign the mouse to an input handler
am.registerWithInputHandler(input);

The object input is created in SimpleGame. By default, it’s the thing that lets us move around. I change it to work with my AbsoluteMouse instead of aiming. In your own game, you would use your own InputHandler. An InputHandler is a class that lets you handle your inputs separately from your main game code. After creating my box, I attach both my mouse and the box to rootNode. After I deactivate my LightState, you see a strange line by itself at the end of simpleInitGame()

pr = new BoundingPickResults();

This creates a container for our “pick results” which we will use during simpleUpdate.

simpleUpdate Catching Events

Next, you see simpleUpdate. Remember, it is called every frame.

// Is button 0 down? Button 0 is left click
if (MouseInput.get().isButtonDown(0)) {

We could have checked for isButtonDown(1) or isButtonDown(2) and so on if your mouse has that many buttons on it. Button 0 happens to be left click in windows. If the button is pressed, we check to see if they are pressing it on the box, and if so we change the box’s color. The first step in that is getting to where the mouse is on the screen:

Vector2f screenPos = new Vector2f();
// Get the position that the mouse is pointing to
screenPos.set(am.getHotSpotPosition().x, am.getHotSpotPosition().y);

We get the mouse’s absolute X and Y position. To understand why I use getHotSpotPosition, lets look at a picture of my mouse:

Notice that the actual cursor icon points to the top. Because of this, my cursor is actually pointing to its position plus whatever the icon’s height is. You can set your hotspot to wherever your mouse pointer actually is. But default, the hotspot is at x=0, y=ImageHeight so we’re ok with the default.

Know where the mouse is pointing

After getting the correct cursor position, I get the real world value for wherever it’s pointing:

// Get the world location of that X,Y value
Vector3f worldCoords = display.getWorldCoordinates(screenPos, 0);
Vector3f worldCoords2 = display.getWorldCoordinates(screenPos, 1);

This translates a position on the screen to a position in my world. The third float I pass in, 0, is the distance away from the screen’s position that this 3D position will take. 0 would be the screen position right in front of you. 10 would be the screen position 10 units away from you. Now that I know at which point my cursor is located in the real world, I need to create a ray using that point:

// Create a ray starting from the camera, and going in the direction
// of the mouse's location
Ray mouseRay = new Ray(worldCoords, worldCoords2.subtractLocal(
	worldCoords).normalizeLocal());

This creates a ray starting wherever my camera is and going in whichever direction I’m clicking. Rays in jME are defined with a starting location and a direction. I subtract my world coordinate from my camera’s location to get a direction relative to the camera’s location. There’s some math going on there. Sadly, you’ll need to use math to create any good 3D game. For more math information, check out jME’s math links.

Next, I check to see if the ray I have intersects the BoundingBox for my box. If so, I change colors. To do that, I first clear any previous pick results I may have had.

pr.clear();

Next, I have to initiate the “pick” call. I am going to see if any children of rootNode intersect with the ray I just created, and if so their results are stored in pr. For each item that intersected that ray, I’m changing its color.

rootNode.findPick(mouseRay, pr);
 
for (int i = 0; i < pr.getNumber(); i++) {
	pr.getPickData(i).getTargetMesh().setRandomColors();
}

Scene Graph

Here is a picture of what the scene graph would look like:


rootNode
b “My Box” am “The Mouse”
tiedtoo
mouseInput
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution 3.0 Unported