« 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.
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(); } } } }
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.
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.
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.
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.
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(); }
Here is a picture of what the scene graph would look like:
| rootNode | |
| b “My Box” | am “The Mouse” |
| tiedtoo | |
| mouseInput | |