Creating an Input Manager

Now that you've mastered key and mouse input, you'll put it all together and create an Input Manager. But first, let's point out some of the pitfalls of the previous code examples that you need to address in your Input Manager. One thing you might have noticed about the previous examples is all of the synchronized methods. Remember that all events are sent in from the AWT event dispatch thread, which is a different thread than the main thread. Obviously, you don't want to modify the game state (change sprite locations, for example) in the middle of a draw operation, so the methods are synchronized to make sure this doesn't happen. For future games, however, you will want to handle all input at a specific point in the game loop. You can do this easily by setting Boolean variables when a key is pressed. For example, a jumpIsPressed Boolean can be set in the keyPressed() method when the spacebar is pressed, and later in the game loop, you can check whether the jumpIsPressed variable is set. If it is, the code can make the player jump. Also, for some actions, such as jumping, you want to perform an action only on the initial key press. For other actions, such as moving, you want to move as long as the key is down. For jumping, if you want to detect initial key presses for jumping, whenever we see that jumpIsPressed is true in the game loop, you can just set it to false. That way, the player won't jump again until he or she presses the key again. But what if the user doesn't like pressing the spacebar to jump and instead wants to press Control? Simple: In that case, you just say, "Sorry, you can't do that." Okay, maybe you'll be nice and let the user configure the keyboard. To do this, you need to be able to map a generic action to a key and let the action be mapped to different keys on the fly. For now, put this on the wish list for your InputManager class. To sum it up, here's everything you want InputManager to do:

  • Handle all key and mouse events, including relative mouse movement.
  • Save the events so you can query them precisely when you want to, instead of changing game state in the AWT event dispatch thread.
  • Detect the initial press for some keys and detect whether the key is held down for others.
  • Map keys to generic game actions, such as mapping the spacebar to a jump action.
  • Change the key mapping on the fly so the user can configure the keyboard.

First, let's focus on the generic game actions. The GameAction class in Listing 3.5 is easy to use, with methods such as isPressed() for keys and getAmount() to see how much the mouse has moved. It handles both initial press and normal key behavior, too. Finally, it has methods for the Input Manager to call, such as press() and release().

Listing 3.5 GameAction.java
package com.brackeen.javagamebook.input;
/**
 The GameAction class is an abstract to a user-initiated
 action, like jumping or moving. GameActions can be mapped
 to keys or the mouse with the InputManager.
*/
public class GameAction {
 /**
 Normal behavior. The isPressed() method returns true
 as long as the key is held down.
 */
 public static final int NORMAL = 0;
 /**
 Initial press behavior. The isPressed() method returns
 true only after the key is first pressed, and not again
 until the key is released and pressed again.
 */
 public static final int DETECT_INITAL_PRESS_ONLY = 1;
 private static final int STATE_RELEASED = 0;
 private static final int STATE_PRESSED = 1;
 private static final int STATE_WAITING_FOR_RELEASE = 2;
 private String name;
 private int behavior;
 private int amount;
 private int state;
 /**
 Create a new GameAction with the NORMAL behavior.
 */
 public GameAction(String name) {
 this(name, NORMAL);
 }
 /**
 Create a new GameAction with the specified behavior.
 */
 public GameAction(String name, int behavior) {
 this.name = name;
 this.behavior = behavior;
 reset();
 }
 /**
 Gets the name of this GameAction.
 */
 public String getName() {
 return name;
 }
 /**
 Resets this GameAction so that it appears like it hasn't
 been pressed.
 */
 public void reset() {
 state = STATE_RELEASED;
 amount = 0;
 }
 /**
 Taps this GameAction. Same as calling press() followed
 by release().
 */
 public synchronized void tap() {
 press();
 release();
 }
 /**
 Signals that the key was pressed.
 */
 public synchronized void press() {
 press(1);
 }
 /**
 Signals that the key was pressed a specified number of
 times, or that the mouse moved a specified distance.
 */
 public synchronized void press(int amount) {
 if (state != STATE_WAITING_FOR_RELEASE) {
 this.amount+=amount;
 state = STATE_PRESSED;
 }
 }
 /**
 Signals that the key was released
 */
 public synchronized void release() {
 state = STATE_RELEASED;
 }
 /**
 Returns whether the key was pressed or not since last
 checked.
 */
 public synchronized boolean isPressed() {
 return (getAmount() != 0);
 }
 /**
 For keys, this is the number of times the key was
 pressed since it was last checked.
 For mouse movement, this is the distance moved.
 */
 public synchronized int getAmount() {
 int retVal = amount;
 if (retVal != 0) {
 if (state == STATE_RELEASED) {
 amount = 0;
 }
 else if (behavior == DETECT_INITAL_PRESS_ONLY) {
 state = STATE_WAITING_FOR_RELEASE;
 amount = 0;
 }
 }
 return retVal;
 }
}


Finally, you'll create the InputManager class in Listing 3.6. The InputManager class has all the code from the previous input examples, including invisible cursors and relative mouse motion. Also, it has code for mapping keys and mouse events to GameActions. When a key is pressed, the code checks to see whether a GameAction is mapped to that key and, if so, calls the GameAction's press() method. How does the mapping work? An array of GameActions is created. Each index in the array corresponds to a virtual key code. Most of the virtual key codes have a value that is less than 600, so the array of GameActions has a length of 600. The mapping works similarly for mouse events. Because mouse events don't have codes, fake mouse codes are made up in InputManager. There are mouse codes for mouse movement (left, right, up, and down), mouse wheel scrolling, and mouse buttons.

Listing 3.6 InputManager.java
package com.brackeen.javagamebook.input;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;
import javax.swing.SwingUtilities;
/**
 The InputManager manages input of key and mouse events.
 Events are mapped to GameActions.
*/
public class InputManager implements KeyListener, MouseListener,
 MouseMotionListener, MouseWheelListener
{
 /**
 An invisible cursor.
 */
 public static final Cursor INVISIBLE_CURSOR =
 Toolkit.getDefaultToolkit().createCustomCursor(
 Toolkit.getDefaultToolkit().getImage(""),
 new Point(0,0),
 "invisible");
 // mouse codes
 public static final int MOUSE_MOVE_LEFT = 0;
 public static final int MOUSE_MOVE_RIGHT = 1;
 public static final int MOUSE_MOVE_UP = 2;
 public static final int MOUSE_MOVE_DOWN = 3;
 public static final int MOUSE_WHEEL_UP = 4;
 public static final int MOUSE_WHEEL_DOWN = 5;
 public static final int MOUSE_BUTTON_1 = 6;
 public static final int MOUSE_BUTTON_2 = 7;
 public static final int MOUSE_BUTTON_3 = 8;
 private static final int NUM_MOUSE_CODES = 9;
 // key codes are defined in java.awt.KeyEvent.
 // most of the codes (except for some rare ones like
 // "alt graph") are less than 600.
 private static final int NUM_KEY_CODES = 600;
 private GameAction[] keyActions =
 new GameAction[NUM_KEY_CODES];
 private GameAction[] mouseActions =
 new GameAction[NUM_MOUSE_CODES];
 private Point mouseLocation;
 private Point centerLocation;
 private Component comp;
 private Robot robot;
 private boolean isRecentering;
 /**
 Creates a new InputManager that listens to input from the
 specified component.
 */
 public InputManager(Component comp) {
 this.comp = comp;
 mouseLocation = new Point();
 centerLocation = new Point();
 // register key and mouse listeners
 comp.addKeyListener(this);
 comp.addMouseListener(this);
 comp.addMouseMotionListener(this);
 comp.addMouseWheelListener(this);
 // allow input of the TAB key and other keys normally
 // used for focus traversal
 comp.setFocusTraversalKeysEnabled(false);
 }
 /**
 Sets the cursor on this InputManager's input component.
 */
 public void setCursor(Cursor cursor) {
 comp.setCursor(cursor);
 }
 /**
 Sets whether relative mouse mode is on or not. For
 relative mouse mode, the mouse is "locked" in the center
 of the screen, and only the changed in mouse movement
 is measured. In normal mode, the mouse is free to move
 about the screen.
 */
 public void setRelativeMouseMode(boolean mode) {
 if (mode == isRelativeMouseMode()) {
 return;
 }
 if (mode) {
 try {
 robot = new Robot();
 recenterMouse();
 }
 catch (AWTException ex) {
 // couldn't create robot!
 robot = null;
 }
 }
 else {
 robot = null;
 }
 }
 /**
 Returns whether or not relative mouse mode is on.
 */
 public boolean isRelativeMouseMode() {
 return (robot != null);
 }
 /**
 Maps a GameAction to a specific key. The key codes are
 defined in java.awt.KeyEvent. If the key already has
 a GameAction mapped to it, the new GameAction overwrites
 it.
 */
 public void mapToKey(GameAction gameAction, int keyCode) {
 keyActions[keyCode] = gameAction;
 }
 /**
 Maps a GameAction to a specific mouse action. The mouse
 codes are defined here in InputManager (MOUSE_MOVE_LEFT,
 MOUSE_BUTTON_1, etc). If the mouse action already has
 a GameAction mapped to it, the new GameAction overwrites
 it.
 */
 public void mapToMouse(GameAction gameAction,
 int mouseCode)
 {
 mouseActions[mouseCode] = gameAction;
 }
 /**
 Clears all mapped keys and mouse actions to this
 GameAction.
 */
 public void clearMap(GameAction gameAction) {
 for (int i=0; i<keyActions.length; i++) {
 if (keyActions[i] == gameAction) {
 keyActions[i] = null;
 }
 }
 for (int i=0; i<mouseActions.length; i++) {
 if (mouseActions[i] == gameAction) {
 mouseActions[i] = null;
 }
 }
 gameAction.reset();
 }
 /**
 Gets a List of names of the keys and mouse actions mapped
 to this GameAction. Each entry in the List is a String.
 */
 public List getMaps(GameAction gameCode) {
 ArrayList list = new ArrayList();
 for (int i=0; i<keyActions.length; i++) {
 if (keyActions[i] == gameCode) {
 list.add(getKeyName(i));
 }
 }
 for (int i=0; i<mouseActions.length; i++) {
 if (mouseActions[i] == gameCode) {
 list.add(getMouseName(i));
 }
 }
 return list;
 }
 /**
 Resets all GameActions so they appear like they haven't
 been pressed.
 */
 public void resetAllGameActions() {
 for (int i=0; i<keyActions.length; i++) {
 if (keyActions[i] != null) {
 keyActions[i].reset();
 }
 }
 for (int i=0; i<mouseActions.length; i++) {
 if (mouseActions[i] != null) {
 mouseActions[i].reset();
 }
 }
 }
 /**
 Gets the name of a key code.
 */
 public static String getKeyName(int keyCode) {
 return KeyEvent.getKeyText(keyCode);
 }
 /**
 Gets the name of a mouse code.
 */
 public static String getMouseName(int mouseCode) {
 switch (mouseCode) {
 case MOUSE_MOVE_LEFT: return "Mouse Left";
 case MOUSE_MOVE_RIGHT: return "Mouse Right";
 case MOUSE_MOVE_UP: return "Mouse Up";
 case MOUSE_MOVE_DOWN: return "Mouse Down";
 case MOUSE_WHEEL_UP: return "Mouse Wheel Up";
 case MOUSE_WHEEL_DOWN: return "Mouse Wheel Down";
 case MOUSE_BUTTON_1: return "Mouse Button 1";
 case MOUSE_BUTTON_2: return "Mouse Button 2";
 case MOUSE_BUTTON_3: return "Mouse Button 3";
 default: return "Unknown mouse code " + mouseCode;
 }
 }
 /**
 Gets the x position of the mouse.
 */
 public int getMouseX() {
 return mouseLocation.x;
 }
 /**
 Gets the y position of the mouse.
 */
 public int getMouseY() {
 return mouseLocation.y;
 }
 /**
 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() {
 if (robot != null && comp.isShowing()) {
 centerLocation.x = comp.getWidth() / 2;
 centerLocation.y = comp.getHeight() / 2;
 SwingUtilities.convertPointToScreen(centerLocation,
 comp);
 isRecentering = true;
 robot.mouseMove(centerLocation.x, centerLocation.y);
 }
 }
 private GameAction getKeyAction(KeyEvent e) {
 int keyCode = e.getKeyCode();
 if (keyCode < keyActions.length) {
 return keyActions[keyCode];
 }
 else {
 return null;
 }
 }
 /**
 Gets the mouse code for the button specified in this
 MouseEvent.
 */
 public static int getMouseButtonCode(MouseEvent e) {
 switch (e.getButton()) {
 case MouseEvent.BUTTON1:
 return MOUSE_BUTTON_1;
 case MouseEvent.BUTTON2:
 return MOUSE_BUTTON_2;
 case MouseEvent.BUTTON3:
 return MOUSE_BUTTON_3;
 default:
 return -1;
 }
 }
 private GameAction getMouseButtonAction(MouseEvent e) {
 int mouseCode = getMouseButtonCode(e);
 if (mouseCode != -1) {
 return mouseActions[mouseCode];
 }
 else {
 return null;
 }
 }
 // from the KeyListener interface
 public void keyPressed(KeyEvent e) {
 GameAction gameAction = getKeyAction(e);
 if (gameAction != null) {
 gameAction.press();
 }
 // make sure the key isn't processed for anything else
 e.consume();
 }
 // from the KeyListener interface
 public void keyReleased(KeyEvent e) {
 GameAction gameAction = getKeyAction(e);
 if (gameAction != null) {
 gameAction.release();
 }
 // make sure the key isn't processed for anything else
 e.consume();
 }
 // from the KeyListener interface
 public void keyTyped(KeyEvent e) {
 // make sure the key isn't processed for anything else
 e.consume();
 }
 // from the MouseListener interface
 public void mousePressed(MouseEvent e) {
 GameAction gameAction = getMouseButtonAction(e);
 if (gameAction != null) {
 gameAction.press();
 }
 }
 // from the MouseListener interface
 public void mouseReleased(MouseEvent e) {
 GameAction gameAction = getMouseButtonAction(e);
 if (gameAction != null) {
 gameAction.release();
 }
 }
 // from the MouseListener interface
 public void mouseClicked(MouseEvent e) {
 // do nothing
 }
 // from the MouseListener interface
 public void mouseEntered(MouseEvent e) {
 mouseMoved(e);
 }
 // from the MouseListener interface
 public void mouseExited(MouseEvent e) {
 mouseMoved(e);
 }
 // 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;
 mouseHelper(MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, dx);
 mouseHelper(MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, dy);
 if (isRelativeMouseMode()) {
 recenterMouse();
 }
 }
 mouseLocation.x = e.getX();
 mouseLocation.y = e.getY();
 }
 // from the MouseWheelListener interface
 public void mouseWheelMoved(MouseWheelEvent e) {
 mouseHelper(MOUSE_WHEEL_UP, MOUSE_WHEEL_DOWN,
 e.getWheelRotation());
 }
 private void mouseHelper(int codeNeg, int codePos,
 int amount)
 {
 GameAction gameAction;
 if (amount < 0) {
 gameAction = mouseActions[codeNeg];
 }
 else {
 gameAction = mouseActions[codePos];
 }
 if (gameAction != null) {
 gameAction.press(Math.abs(amount));
 gameAction.release();
 }
 }
}


Whew! That's a lot of code. One additional feature that InputManager has is functionality to get the name of the key or mouse event. The KeyEvent class already has this functionality for keys, but you implement your own for mouse events. This feature, used in the getKeyName() and getMouseName() methods, is useful for telling the user which keys do what, and you'll use it later for letting the user configure the keyboard. Also, the InputManager has the resetAllGameActions() method, which resets any GameActions that are currently "pressed." This can be useful for situations such as switching from the main menu to the game. Without clearing the GameActions, pressing the spacebar in a menu could cause the player to jump later, as soon as the game starts.

Screenshot


   
Comments