Mouselook-Style Mouse Movement
If you've ever played a first-person shooter such as Quake III, you've probably used the mouselook feature. The mouselook feature allows your player to look around by moving the mouse. For example, if you move the mouse left, your player looks left. The cool thing about this feature is that you can keep moving the mouse as much as you want because the mouse isn't constrained to the screen. You can move the mouse until you get dizzy or run out of mousepad. In the previous example, you could detect the location of the mouse on the screen, but when you moved the mouse too far left, the mouse would stop because it hits the edge of the screen. This obviously wouldn't work in a first-person shooter because you don't want to limit how much the player can look. Instead of detecting the absolute location of the mouse, you need to detect the relative motion, but how can you do this? The Java API doesn't directly provide methods to detect the relative motion of the mouse, but there are ways to trick the mouse to do what you want. To do this, you just need to make sure the mouse never hits the edge of the screen. Every time the mouse moves, you'll just reposition the mouse to the center of the screen. That way, the mouse will never be stopped by the edge of the screen, and you can always calculate how much the mouse has moved based on its previous location. Here's a breakdown of how it works:
- The mouse starts at the center of the screen.
- The user moves the mouse, and you calculate how much it moved.
- You send an event to reposition the mouse to the center of the screen.
- When you detect that that the mouse was recentered, you ignore the event.
You can reposition the mouse using the Robot class. No, the Robot class doesn't cause a real robot to come to your desk and move the mouse for you. The Robot class was designed to automate testing of graphical apps. Besides being capable of programmatically moving the mouse, it has all sorts of functions for doing things such as emulating key presses and making screen captures. Moving the mouse is as simple as you'd like it to be:
robot.mouseMove(x, y);
One note of caution: The Robot class might not work on some rare systems. Some systems don't allow you to programmatically move the mouse or emulate key presses. However, it will work just fine on modern Windows and Linux systems. For the Robot privileged, let's try it out. You're not ready to create a first-person shooter just yet, so you'll create a simple background pattern that can be scrolled indefinitely using mouselook. The MouselookTest class in Listing 3.4 does just that. For the curious, you can turn off mouselook by pressing the spacebar. As usual, press Escape to exit.
Listing 3.4 MouselookTest.java
import java.awt.*; import java.awt.event.*; import javax.swing.SwingUtilities; import com.brackeen.javagamebook.graphics.*; import com.brackeen.javagamebook.test.GameCore; /** A simple mouselook test. Using mouselook, the user can virtually move the mouse in any direction indefinitely. Without mouselook, the mouse stops when it hits the edge of the screen. <p>Mouselook works by recentering the mouse whenever it is moved, so it can always measure the relative mouse movement, and the mouse never hits the edge of the screen. */ public class MouselookTest extends GameCore implements MouseMotionListener, KeyListener { public static void main(String[] args) { new MouselookTest().run(); } private Image bgImage; private Robot robot; private Point mouseLocation; private Point centerLocation; private Point imageLocation; private boolean relativeMouseMode; private boolean isRecentering; public void init() { super.init(); mouseLocation = new Point(); centerLocation = new Point(); imageLocation = new Point(); relativeMouseMode = true; isRecentering = false; try { robot = new Robot(); recenterMouse(); mouseLocation.x = centerLocation.x; mouseLocation.y = centerLocation.y; } catch (AWTException ex) { System.out.println("Couldn't create Robot!"); } Window window = screen.getFullScreenWindow(); window.addMouseMotionListener(this); window.addKeyListener(this); bgImage = loadImage("../images/background.jpg"); } public synchronized void draw(Graphics2D g) { int w = screen.getWidth(); int h = screen.getHeight(); // make sure position is correct imageLocation.x %= w; imageLocation.y %= screen.getHeight(); if (imageLocation.x < 0) { imageLocation.x += w; } if (imageLocation.y < 0) { imageLocation.y += screen.getHeight(); } // draw the image in four places to cover the screen int x = imageLocation.x; int y = imageLocation.y; g.drawImage(bgImage, x, y, null); g.drawImage(bgImage, x-w, y, null); g.drawImage(bgImage, x, y-h, null); g.drawImage(bgImage, x-w, y-h, null); // draw instructions g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.drawString("Press Space to change mouse modes.", 5, FONT_SIZE); g.drawString("Press Escape to exit.", 5, FONT_SIZE*2); } /** Uses the Robot class to try to position the mouse in the center of the screen. <p>Note that use of the Robot class may not be available on all platforms. */ private synchronized void recenterMouse() { Window window = screen.getFullScreenWindow(); if (robot != null && window.isShowing()) { centerLocation.x = window.getWidth() / 2; centerLocation.y = window.getHeight() / 2; SwingUtilities.convertPointToScreen(centerLocation, window); isRecentering = true; robot.mouseMove(centerLocation.x, centerLocation.y); } } // from the MouseMotionListener interface public void mouseDragged(MouseEvent e) { mouseMoved(e); } // from the MouseMotionListener interface public synchronized void mouseMoved(MouseEvent e) { // this event is from re-centering the mouse - ignore it if (isRecentering && centerLocation.x == e.getX() && centerLocation.y == e.getY()) { isRecentering = false; } else { int dx = e.getX() - mouseLocation.x; int dy = e.getY() - mouseLocation.y; imageLocation.x += dx; imageLocation.y += dy; // recenter the mouse if (relativeMouseMode) { recenterMouse(); } } mouseLocation.x = e.getX(); mouseLocation.y = e.getY(); } // from the KeyListener interface public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { // exit the program stop(); } else if (e.getKeyCode() == KeyEvent.VK_SPACE) { // change relative mouse mode relativeMouseMode = !relativeMouseMode; } } // from the KeyListener interface public void keyReleased(KeyEvent e) { // do nothing } // from the KeyListener interface public void keyTyped(KeyEvent e) { // do nothing } }
In the code, if a Robot object can't be created, it throws an AWTException. Here, you just ignore the exception and print a message to the console. When you tell the Robot class to move the mouse, the mouse move event might not immediately occur. Some normal mouse move events could be sent before the recentering event. Therefore, the code checks whether the mouse move event results in the mouse being located in the center of the screen. If it is, it's treated as a recentering event, and the event is ignored. Otherwise, the event is treated as a normal mouse move event.
Hiding the Cursor
In the MouselookTest example, you might notice that the mouse cursor in the middle of the screen doesn't look too attractive. When it's recentered, it might flicker or you might see the mouse move for a brief moment. What you need to do next is just hide the mouse cursor. Luckily, the Java API has methods to change the mouse cursor. Unluckily, the Java API doesn't define an invisible cursor. Some of the cursors it does define are listed here:
- CROSSHAIR_CURSOR A cursor in the shape of a plus sign
- DEFAULT_CURSOR The normal arrow cursor
- HAND_CURSOR The hand cursor that you normally see when you mouse over hyperlinks on web pages
- TEXT_CURSOR The text cursor, normally I-shaped
- WAIT_CURSOR The wait cursor, normally an hourglass
All is not lost, however. The Java API enables you to create your own cursors using custom images, so you'll just create a cursor that has a blank image. You do this by calling the createCustomCursor() method of the Toolkit class:
Cursor invisibleCursor = Toolkit.getDefaultToolkit().createCustomCursor( Toolkit.getDefaultToolkit().getImage(""), new Point(0,0), "invisible");
Here you just create an "invalid" image, which the Toolkit interprets as being an invisible cursor. You can change the cursor for your games by calling the setCursor() method:
Window window = screen.getFullScreenWindow(); window.setCursor(invisibleCursor);
Later, you can get the default cursor by calling Cursor's getPredefinedCursor() method:
Cursor normalCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
That's it! Now you can use this code so you don't have to look at that annoying cursor anymore.