Java Games: Keyboard and Mouse

Published November 30, 2007 by Tim Wright, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement

For computer games, the keyboard and mouse are the primary methods of interacting with the computer. The problem is, while Java has great support for these input devices for GUI applications, computer games need to handle the input a little differently. Although there are no built-in classes that give us what we need, we can easily create our own, learn a little something in the process, and have a lot of fun doing it. So grab your favorite drink and get coding.

Input and Java

When learning how to program computer games in Java, the input aspect of the program can cause a lot of trouble. Most of the time, when writing a GUI application, the monitoring of the keyboard and the mouse are handled for you. By supplying a callback function, you can listen for mouse clicks or keyboard presses and respond to them when they occur. The following image shows what is going on:

The problem is, when the callback happens, you are now in some mystery thread. While this is not a big deal for a GUI application, it does not work well for a computer game. It would be nice if you could know the state of any input device right now! Other programming environments allow this kind of request, but at the time of this writing, there is no simple way to poll the keyboard and mouse. Due to the fact that user input is very important, we need to develop our own input polling classes so we can focus on what is really important: "Is the space bar down right now, 'cause if it is, I am going to fire a laser!"

Keyboard Input

I am going to show you the keyboard polling class, so those of you who just want to see the code do not have to read anymore. Then I will explain what is going on "under the hood" and show how the class is used to poll the keyboard.


import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class KeyboardInput implements KeyListener {
        
  private static final int KEY_COUNT = 256;
        
  private enum KeyState {
    RELEASED, // Not down
    PRESSED,  // Down, but not the first time
    ONCE      // Down for the first time
  }
        
  // Current state of the keyboard
  private boolean[] currentKeys = null;
        
  // Polled keyboard state
  private KeyState[] keys = null;
        
  public KeyboardInput() {
    currentKeys = new boolean[ KEY_COUNT ];
    keys = new KeyState[ KEY_COUNT ];
    for( int i = 0; i < KEY_COUNT; ++i ) {
      keys[ i ] = KeyState.RELEASED;
    }
  }
        
  public synchronized void poll() {
    for( int i = 0; i < KEY_COUNT; ++i ) {
      // Set the key state 
      if( currentKeys[ i ] ) {
        // If the key is down now, but was not
        // down last frame, set it to ONCE,
        // otherwise, set it to PRESSED
        if( keys[ i ] == KeyState.RELEASED )
          keys[ i ] = KeyState.ONCE;
        else
          keys[ i ] = KeyState.PRESSED;
      } else {
        keys[ i ] = KeyState.RELEASED;
      }
    }
  }
        
  public boolean keyDown( int keyCode ) {
    return keys[ keyCode ] == KeyState.ONCE ||
           keys[ keyCode ] == KeyState.PRESSED;
  }
        
  public boolean keyDownOnce( int keyCode ) {
    return keys[ keyCode ] == KeyState.ONCE;
  }
        
  public synchronized void keyPressed( KeyEvent e ) {
    int keyCode = e.getKeyCode();
    if( keyCode >= 0 && keyCode < KEY_COUNT ) {
      currentKeys[ keyCode ] = true;
    }
  }

  public synchronized void keyReleased( KeyEvent e ) {
    int keyCode = e.getKeyCode();
    if( keyCode >= 0 && keyCode < KEY_COUNT ) {
      currentKeys[ keyCode ] = false;
    }
  }

  public void keyTyped( KeyEvent e ) {
    // Not needed
  }
}

KeyboardInput: Under the hood

The first thing to notice about the KeyboardInput class is that it implements the KeyListener interface. This interface consists of three methods: keyTyped, keyPressed, and keyReleased. The keyTyped method is not needed. The other methods check to make sure that the keycode is between 0 - 255, and then update the current state of that key in the boolean array. If the key is down the boolean is set to true, otherwise it is set to false. There are way more than 256 keys, so make sure that all the keys needed for your game either fall into this range or that the array size has been adjusted to include the missing keys.


  public synchronized void keyPressed( KeyEvent e ) {
    int keyCode = e.getKeyCode();
    if( keyCode >= 0 && keyCode < KEY_COUNT ) {
      currentKeys[ keyCode ] = true;
    }
  }

  public synchronized void keyReleased( KeyEvent e ) {
    int keyCode = e.getKeyCode();
    if( keyCode >= 0 && keyCode < KEY_COUNT ) {
      currentKeys[ keyCode ] = false;
    }
  }

  public void keyTyped( KeyEvent e ) {
    // Not needed
  }
When the poll method is called, the current state of the keys are transfered to the array of KeyState objects. The reason for this is to have three states instead of two: Pressed, Released, and Once. Pressed means the key is down, Released means the key is not down, and Once means that the key is down for the first time. To clarify, if the key was not down last frame and is down this frame, keyDownOnce will return true. On the next frame, if the key is still down, keyDownOnce will return false. Also note that the poll method and the KeyListeners are synchronized. Because this class is used from the main game thread and the mystery keyboard input thread, it is important to protect the shared currentKeys array.

  public synchronized void poll() {
    for( int i = 0; i < KEY_COUNT; ++i ) {
      // Set the key state 
      if( currentKeys[ i ] ) {
        // If the key is down now, but was not
        // down last frame, set it to ONCE,
        // otherwise, set it to PRESSED
        if( keys[ i ] == KeyState.RELEASED )
          keys[ i ] = KeyState.ONCE;
        else
          keys[ i ] = KeyState.PRESSED;
      } else {
        keys[ i ] = KeyState.RELEASED;
      }
    }
  }
The rest of the class involves either initializing all the properties or getting the current key state. Again, the difference between keyDown and keyDownOnce is this: keyDownOnce will only return true the first time the key is down, while keyDown will return true the entire time the key is down.

  public boolean keyDown( int keyCode ) {
    return keys[ keyCode ] == KeyState.ONCE ||
           keys[ keyCode ] == KeyState.PRESSED;
  }
        
  public boolean keyDownOnce( int keyCode ) {
    return keys[ keyCode ] == KeyState.ONCE;
  }

Simple Keyboard Example

To poll the keyboard, add the KeyboardInput class as a key listener to the JFrame. If you are using a Canvas to adjust the window size, do not forget to add the listener to both the JFrame and the Canvas. Then call the poll method every frame, and you are in business. The following is an example of using the class to poll the keyboard input. If the other code in this example is unfamiliar, please read the Java Games: Active Rendering tutorial.


import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.JFrame;

public class SimpleKeyboardInput extends JFrame {

  static final int WIDTH = 640;
  static final int HEIGHT = 480;
  class Bob { int x, y, w, h, dx, dy; }
  KeyboardInput keyboard = new KeyboardInput(); // Keyboard polling
  Canvas canvas; // Our drawing component
  Vector< Point > circles = new Vector< Point >(); // Circles
  Bob bob = new Bob(); // Our rectangle
  Random rand = new Random(); // Used for random circle locations

  public SimpleKeyboardInput() {
  
    setIgnoreRepaint( true );
    setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    canvas = new Canvas();
    canvas.setIgnoreRepaint( true );
    canvas.setSize( WIDTH, HEIGHT );
    add( canvas );
    pack();
    
    // Hookup keyboard polling
    addKeyListener( keyboard );
    canvas.addKeyListener( keyboard );
    
    bob.x = bob.y = 0;
    bob.dx = bob.dy = 5;
    bob.w = bob.h = 25;
  }
        
  public void run() {
  
    canvas.createBufferStrategy( 2 );
    BufferStrategy buffer = canvas.getBufferStrategy();
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gd = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gd.getDefaultConfiguration();
    BufferedImage bi = gc.createCompatibleImage( WIDTH, HEIGHT );
    
    Graphics graphics = null;
    Graphics2D g2d = null;
    Color background = Color.BLACK;
    
    while( true ) {
      try {
      
        // Poll the keyboard
        keyboard.poll();
        // Should we exit?
        if( keyboard.keyDownOnce( KeyEvent.VK_ESCAPE ) )
          break;
        
        // Clear the back buffer          
        g2d = bi.createGraphics();
        g2d.setColor( background );
        g2d.fillRect( 0, 0, WIDTH, HEIGHT );
        
        // Draw help
        g2d.setColor(  Color.GREEN );
        g2d.drawString( "Use arrow keys to move rect", 20, 20 );
        g2d.drawString( "Press SPACE to add circles", 20, 32 );
        g2d.drawString( "Press C to clear circles", 20, 44 );
        g2d.drawString( "Press ESC to exit", 20, 56 );
        
        // Move bob and add circles
        processInput();
        
        // Draw random circles
        g2d.setColor( Color.MAGENTA );
        for( Point p : circles ) {
          g2d.drawOval( p.x, p.y, 25, 25 );
        }
        
        // Draw bob
        g2d.setColor(  Color.GREEN );
        g2d.drawRect( bob.x, bob.y, bob.w, bob.h );
        
        // Blit image and flip...
        graphics = buffer.getDrawGraphics();
        graphics.drawImage( bi, 0, 0, null );
        if( !buffer.contentsLost() )
          buffer.show();
          
        // Let the OS have a little time...
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
        }
        
      } finally {
        // Release resources
        if( graphics != null ) 
          graphics.dispose();
        if( g2d != null ) 
          g2d.dispose();
      }
    }
  }
        
  protected void processInput() {
    // If moving down
    if( keyboard.keyDown( KeyEvent.VK_DOWN ) ) {
      bob.y += bob.dy;
      // Check collision with botton
      if( bob.y + bob.h > HEIGHT - 1 )
        bob.y = HEIGHT - bob.h - 1;
    }
    // If moving up
    if( keyboard.keyDown( KeyEvent.VK_UP ) ) {
      bob.y -= bob.dy;
      // Check collision with top
      if( bob.y < 0 )
        bob.y = 0;
    }
    // If moving left
    if( keyboard.keyDown( KeyEvent.VK_LEFT ) ) {
      bob.x -= bob.dx;
      // Check collision with left
      if( bob.x < 0 )
        bob.x = 0;
    }
    // If moving right
    if( keyboard.keyDown( KeyEvent.VK_RIGHT ) ) {
      bob.x += bob.dx;
      // Check collision with right
      if( bob.x + bob.w > WIDTH - 1 )
        bob.x = WIDTH - bob.w - 1;
    }
    // Add random circle if space bar is pressed
    if( keyboard.keyDownOnce( KeyEvent.VK_SPACE ) ) {
      int x = rand.nextInt( WIDTH );
      int y = rand.nextInt( HEIGHT );
      circles.add( new Point( x, y ) );
    }
    // Clear circles if they press C
    if( keyboard.keyDownOnce( KeyEvent.VK_C ) ) {
      circles.clear();
    }
  }
        
  public static void main( String[] args ) {
    SimpleKeyboardInput app = new SimpleKeyboardInput();
    app.setTitle( "Simple Keyboard Input" );
    app.setVisible( true );
    app.run();
    System.exit( 0 );
  }
}

Mouse Input - Part One

The mouse is actually two different input devices. One device is the buttons and the other device is the movement. We can start with a mouse input class that does the same thing with the mouse buttons as the keyboard class does with the keyboard buttons, and then we add the current mouse location. Here is the code for the mouse input class. Like before, the nuts and bolts are covered in the next "Under the Hood" section. By now, most of this code should look familiar.


import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

public class MouseInput1 implements MouseListener, MouseMotionListener {

  private static final int BUTTON_COUNT = 3;
  // Polled position of the mouse cursor
  private Point mousePos = null;
  // Current position of the mouse cursor
  private Point currentPos = null;
  // Current state of mouse buttons
  private boolean[] state = null;
  // Polled mouse buttons
  private MouseState[] poll = null;
        
  private enum MouseState {
    RELEASED, // Not down
    PRESSED,  // Down, but not the first time
    ONCE      // Down for the first time
  }
        
  public MouseInput1() {
    // Create default mouse positions
    mousePos = new Point( 0, 0 );
    currentPos = new Point( 0, 0 );
    // Setup initial button states
    state = new boolean[ BUTTON_COUNT ];
    poll = new MouseState[ BUTTON_COUNT ];
    for( int i = 0; i < BUTTON_COUNT; ++i ) {
      poll[ i ] = MouseState.RELEASED;
    }
  }
        
  public synchronized void poll() {
    // Save the current location
    mousePos = new Point( currentPos );
    // Check each mouse button
    for( int i = 0; i < BUTTON_COUNT; ++i ) {
      // If the button is down for the first
      // time, it is ONCE, otherwise it is
      // PRESSED.  
      if( state[ i ] ) {
        if( poll[ i ] == MouseState.RELEASED )
          poll[ i ] = MouseState.ONCE;
        else
          poll[ i ] = MouseState.PRESSED;
      } else {
          // button is not down
          poll[ i ] = MouseState.RELEASED;
      }
    }
  }

  public Point getPosition() {
    return mousePos;
  }

  public boolean buttonDownOnce( int button ) {
    return poll[ button-1 ] == MouseState.ONCE;
  }

  public boolean buttonDown( int button ) {
    return poll[ button-1 ] == MouseState.ONCE ||
           poll[ button-1 ] == MouseState.PRESSED;
  }
  
  public synchronized void mousePressed( MouseEvent e ) {
    state[ e.getButton()-1 ] = true;
  }

  public synchronized void mouseReleased( MouseEvent e ) {
    state[ e.getButton()-1 ] = false;
  }

  public synchronized void mouseEntered( MouseEvent e ) {
    mouseMoved( e );
  }
  
  public synchronized void mouseExited( MouseEvent e ) {
    mouseMoved( e );
  }
  
  public synchronized void mouseDragged( MouseEvent e ) {
    mouseMoved( e );
  }

  public synchronized void mouseMoved( MouseEvent e ) {
    currentPos = e.getPoint();
  }
  
  public void mouseClicked( MouseEvent e ) {
    // Not needed
  }
}

MouseInput: Under the hood (Part One)

The mouse input class works just like the keyboard class as far as the mouse buttons are concerned. The only weird part is that the mouse buttons are numbered 1 - 3, but we need to access the array with 0 - 2, so every time we do something with the mouse buttons we need to subtract one. Other than that, this class behaves just like the KeyboardInput class.


  public synchronized void mousePressed( MouseEvent e ) {
    state[ e.getButton()-1 ] = true;
  }

The getPosition method returns the current mouse cursor coordinates after the poll method has been called. The current mouse position is captured in all the different callbacks, such as mouseEntered, mouseExited, and mouseDragged by having all the different mouse callback methods call the mouseMoved method. This method just stores the current position of the mouse. All of these methods are synchronized because both the mystery mouse callback thread and the main game loop thread access the shared currentPos property.


  public Point getPosition() {
    return mousePos;
  }
  
  public synchronized void poll() {
    // Save the current location
    mousePos = new Point( currentPos );
    // Check each mouse button
    [...]
  }

  public synchronized void mouseMoved( MouseEvent e ) {
    currentPos = e.getPoint();
  }

Simple Mouse Example

Here is a simple example using the MouseInput class. The only thing going on in this example that might need some explanation is the array of points. When the mouse button is released, a null object is added to the array so that when the points are drawn as lines, the null object lets the program know to break up the line and start a new one. Other than that, everything should look familiar. As before, if the rendering code is unfamiliar, please see the Java Games: Active Rendering tutorial.


import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.Vector;
import javax.swing.JFrame;

public class SimpleMouseInput extends JFrame {
        
  static final int WIDTH = 640;
  static final int HEIGHT = 480;
        
  // The new mouse input class
  MouseInput1 mouse;
  // Keyboard polling
  KeyboardInput keyboard;
  // Adding a null into this list will start a new line
  Vector< Point > lines = new Vector< Point >();
  // Are we currently drawing a line?
  boolean drawingLine;
  // Our drawing component
  Canvas canvas;

  public SimpleMouseInput() {
                
    // Setup specific JFrame properties
    setIgnoreRepaint( true );
    setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

    // Create canvas to force the drawing
    // surface to the correct size...
    canvas = new Canvas();
    canvas.setIgnoreRepaint( true );
    canvas.setSize( WIDTH, HEIGHT );
    add( canvas );
    pack();
    
    // Add key listeners
    keyboard = new KeyboardInput();
    addKeyListener( keyboard );
    canvas.addKeyListener( keyboard );
                
    // Add mouse listeners
    mouse = new MouseInput1();
    addMouseListener( mouse );
    addMouseMotionListener( mouse );
    canvas.addMouseListener( mouse );
    canvas.addMouseMotionListener( mouse );
  }

  public void run() {
                
    canvas.createBufferStrategy( 2 );
    BufferStrategy buffer = canvas.getBufferStrategy();
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gd = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gd.getDefaultConfiguration();
    BufferedImage bi = gc.createCompatibleImage( WIDTH, HEIGHT );

    Graphics graphics = null;
    Graphics2D g2d = null;
    Color background = Color.BLACK;
                
    while( true ) {
      try {
        // Poll the keyboard
        keyboard.poll();
        // Poll the mouse
        mouse.poll();
                                
        // Exit the program on ESC key
        if( keyboard.keyDownOnce( KeyEvent.VK_ESCAPE ) )
          break;
                
        // Clear back buffer...
        g2d = bi.createGraphics();
        g2d.setColor( background );
        g2d.fillRect( 0, 0, WIDTH, HEIGHT );
                                
        // Display help
        g2d.setColor(  Color.GREEN );
        g2d.drawString( "Use mouse to draw lines", 20, 20 );
        g2d.drawString( "Press C to clear lines", 20, 32 );
        g2d.drawString( "Press ESC to exit", 20, 44 );
        g2d.drawString( mouse.getPosition().toString(), 20, 56 );
                                
        // Process mouse input
        processInput();

        // Set line color
        g2d.setColor(  Color.WHITE );

        // If just one line, draw a point
        if( lines.size() == 1 ) {
          Point p = lines.get( 0 );
          if( p != null )
            g2d.drawLine( p.x, p.y, p.x, p.y );
        } else {
          // Draw all the lines
          for( int i = 0; i < lines.size()-1; ++i ) {
            Point p1 = lines.get( i );
            Point p2 = lines.get( i+1 );
            // Adding a null into the list is used
            // for breaking up the lines when
            // there are two or more lines
            // that are not connected
            if( !(p1 == null || p2 == null) )
              g2d.drawLine( p1.x, p1.y, p2.x, p2.y );
          }
        }
                                
        // Blit image and flip...
        graphics = buffer.getDrawGraphics();
        graphics.drawImage( bi, 0, 0, null );
        if( !buffer.contentsLost() ) 
          buffer.show();
                                
        // Let the OS have a little time...
        try {
          Thread.sleep(10);
        } catch( InterruptedException ex ) {
                                        
        }
      } finally {
        // Release resources
        if( graphics != null ) 
          graphics.dispose();
        if( g2d != null ) 
          g2d.dispose();
      }
    }
  }
        
  protected void processInput() {
    // if button pressed for first time,
    // start drawing lines
    if( mouse.buttonDownOnce( 1 ) ) {
      drawingLine = true;
    }
    // if the button is down, add line point
    if( mouse.buttonDown( 1 ) ) {
      lines.add( mouse.getPosition() );
    // if the button is not down but we were drawing,
    // add a null to break up the lines
    } else if( drawingLine ){
      lines.add( null );
      drawingLine = false;
    }
    // if 'C' is down, clear the lines
    if( keyboard.keyDownOnce( KeyEvent.VK_C ) ) {
        lines.clear();
    }
  }
        
  public static void main( String[] args ) {
    SimpleMouseInput app = new SimpleMouseInput();
    app.setTitle( "Simple Mouse Example" );
    app.setVisible( true );
    app.run();
    System.exit( 0 );
  }
}

Mouse Input - Part Two

The last part of the MouseInput class that needs some attention is relative mouse movement. A naive attempt to make this work was to just save the last position of the mouse and compute the relative movement by subtracting the current position from the last. Sounds like a good idea. To quote Henry Louis Mencken, "[T]here is always an easy solution to every problem -- neat, plausible and wrong." As it turns out, this solution does not work because when the mouse cursor gets to the edge of the screen, you stop getting relative movement, even though the user is still moving the mouse. The solution to this problem is to re-center the mouse so it can never hit the edge of the screen.

IMPORTANT!!! - Because the solution to the problem involves constantly re-centering the mouse, make sure you add code to turn relative movement off, or you will wind up like me, the man who fired up the first test and had a really hard time stopping the program because there was no way to regain control of the mouse. You've been warned!


import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.SwingUtilities;

public class MouseInput2 implements MouseListener, MouseMotionListener {

  private static final int BUTTON_COUNT = 3;
  // Used for relative movement
  private int dx, dy;
  // Used to re-center the mouse
  private Robot robot = null;
  // Convert coordinates from component to screen
  private Component component;
  // The center of the component
  private Point center;
  // Is this relative or absolute
  private boolean relative;
  // Polled position of the mouse cursor
  private Point mousePos = null;
  // Current position of the mouse cursor
  private Point currentPos = null;
  // Current state of mouse buttons
  private boolean[] state = null;
  // Colled mouse buttons
  private MouseState[] poll = null;
        
  private enum MouseState {
    RELEASED, // Not down
    PRESSED,  // Down, but not the first time
    ONCE      // Down for the first time
  }
        
  public MouseInput2( Component component ) {
    // Need the component object to convert screen coordinates 
    this.component = component;
    // Calculate the component center
    int w = component.getBounds().width;
    int h = component.getBounds().height;
    center = new Point( w/2, h/2 );
    try {
      robot = new Robot();
    } catch( Exception e ) {
      // Handle exception [game specific]
    }
  
    // Create default mouse positions
    mousePos = new Point( 0, 0 );
    currentPos = new Point( 0, 0 );
    // Setup initial button states
    state = new boolean[ BUTTON_COUNT ];
    poll = new MouseState[ BUTTON_COUNT ];
    for( int i = 0; i < BUTTON_COUNT; ++i ) {
      poll[ i ] = MouseState.RELEASED;
    }
  }
        
  public synchronized void poll() {
    // If relative, return only the delta movements,
    // otherwise return the current position...
    if( isRelative() ) {
      mousePos = new Point( dx, dy );
    } else {
      mousePos = new Point( currentPos );
    }
    // Since we have polled, need to reset the delta
    // so the values do not accumulate
    dx = dy = 0;
    // Check each mouse button
    for( int i = 0; i < BUTTON_COUNT; ++i ) {
      // If the button is down for the first
      // time, it is ONCE, otherwise it is
      // PRESSED.  
      if( state[ i ] ) {
        if( poll[ i ] == MouseState.RELEASED )
          poll[ i ] = MouseState.ONCE;
        else
          poll[ i ] = MouseState.PRESSED;
      } else {
          // Button is not down
          poll[ i ] = MouseState.RELEASED;
      }
    }
  }
  
  public boolean isRelative() {
    return relative;
  }
  
  public void setRelative( boolean relative ) {
    this.relative = relative;
    if( relative ) {
      centerMouse();
    }
  }

  public Point getPosition() {
    return mousePos;
  }

  public boolean buttonDownOnce( int button ) {
    return poll[ button-1 ] == MouseState.ONCE;
  }

  public boolean buttonDown( int button ) {
    return poll[ button-1 ] == MouseState.ONCE ||
           poll[ button-1 ] == MouseState.PRESSED;
  }
  
  public synchronized void mousePressed( MouseEvent e ) {
    state[ e.getButton()-1 ] = true;
  }

  public synchronized void mouseReleased( MouseEvent e ) {
    state[ e.getButton()-1 ] = false;
  }

  public synchronized void mouseEntered( MouseEvent e ) {
    mouseMoved( e );
  }
  
  public synchronized void mouseExited( MouseEvent e ) {
    mouseMoved( e );
  }
  
  public synchronized void mouseDragged( MouseEvent e ) {
    mouseMoved( e );
  }

  public synchronized void mouseMoved( MouseEvent e ) {
    if( isRelative() ) {
      Point p = e.getPoint();
      dx += p.x - center.x;
      dy += p.y - center.y;
      centerMouse();
    } else {
      currentPos = e.getPoint();
    }
  }
  
  public void mouseClicked( MouseEvent e ) {
    // Not needed
  }
  
  private void centerMouse() {
    if( robot != null && component.isShowing() ) {
    // Because the convertPointToScreen method 
    // changes the object, make a copy!
      Point copy = new Point( center.x, center.y );
      SwingUtilities.convertPointToScreen( copy, component );
      robot.mouseMove( copy.x, copy.y );
    }
  }
}

MouseInput: Under the hood (Part Two)

If the mouse is constantly re-centered, it never has a chance to hit the edge of the screen and stop moving. The Robot class is used to re-center the mouse, but because the robot class takes screen coordinates and not window coordinates, and because the method call needs a Component object, we must keep a reference. The good news is that the same Component can be used to calculate the screen center. Just remember that if your game allows resizing of the window, you will need to recalculate the center every frame instead of once in the constructor.


  public MouseInput2( Component component ) {
    // Need the component object to convert screen
    // coordinates to window coordinates
    this.component = component;
    // Calculate the component center
    int w = component.getBounds().width;
    int h = component.getBounds().height;
    center = new Point( w/2, h/2 );
    try {
      robot = new Robot();
    } catch( Exception e ) {
      // Handle exception [game specific]
    }
    [...]
  }
Two new methods are added to update the relative property. There may still be times that you want absolute movement, such as configuring game options. If relative is turned on, the mouse is re-centered right away so that the first delta calculations are set to zero.

  public boolean isRelative() {
    return relative;
  }
  
  public void setRelative( boolean relative ) {
    this.relative = relative;
    if( relative ) {
      centerMouse();
    }
  }
The poll method is updated to return absolute or relative positions. Notice that the delta position is reset each time the mouse is polled. This way if the mouse has been moved more that once, all the delta movements are accumulated until the program has a chance to poll again.

  public synchronized void poll() {
    // If relative, return only the delta movements,
    // otherwise return the current position...
    if( isRelative() ) {
      mousePos = new Point( dx, dy );
    } else {
      mousePos = new Point( currentPos );
    }
    // Since we have polled, need to reset the delta
    // so the values do not accumulate
    dx = dy = 0;
    // Check each mouse button
    [...]
  }
While trying this class out with full-screen and windowed modes, I came across a weird situation. If you are using relative mouse input in a full-screen application, then you should pass in the JFrame to the mouse input class. However, if you have a windowed application and you have used a Canvas to force the window size, pass in the Canvas object. I had weird things happen when I did this with a windowed application and I passed in the JFrame. The reason for this is that whatever Component is passed into the constructor is used to calculate the coordinates for re-centering the mouse. If you are using the JFrame to re-center the mouse, but you have used a Canvas object to force the size of the window, then the mouse movements actually come from the Canvas, not the JFrame. Since the centers are not the same for both objects because of the title bar and window resizing borders, the delta calculations will be incorrect. Other than that, this wraps up the new methods in the MouseInput class.

  public synchronized void mouseMoved( MouseEvent e ) {
    if( isRelative() ) {
      Point p = e.getPoint();
      dx += p.x - center.x;
      dy += p.y - center.y;
      centerMouse();
    } else {
      currentPos = e.getPoint();
    }
  }
  
  private void centerMouse() {
    if( robot != null && component.isShowing() ) {
    // Because the convertPointToScreen method 
    // changes the object, make a copy!
      Point copy = new Point( center.x, center.y );
      SwingUtilities.convertPointToScreen( copy, component );
      robot.mouseMove( copy.x, copy.y );
    }
  }
While trying this class out with full-screen and windowed modes, I came across a weird situation. If you are using relative mouse input in a full-screen application, then you should pass in the JFrame to the mouse input class. However, if you have a windowed application and you have used a Canvas to force the window size, pass in the Canvas object. I had weird things happen when I did this with a windowed application and I passed in the JFrame. The reason for this is that whatever Component is passed into the constructor is used to calculate the coordinates for re-centering the mouse. If you are using the JFrame to re-center the mouse, but you have used a Canvas object to force the size of the window, then the mouse movements actually come from the Canvas, not the JFrame. Since the centers are not the same for both objects because of the title bar and window resizing borders, the delta calculations will be incorrect. Other than that, this wraps up the new methods in the MouseInput class.

  // For full-screen apps...
  MouseInput mouse = new MouseInput( jFrame );
  
  // For windowed apps...
  MouseInput mouse = new MouseInput( canvas );

Mouse Cursor

After all that work to get the mouse cursor behaving the way we want, it looks really bad when we can see the cursor jumping around back to the center all the time. The follow is an example of a method that creates an empty cursor. Replacing the default cursor with this empty cursor makes it go away.


  private void disableCursor() {
    Toolkit tk = Toolkit.getDefaultToolkit();
    Image image = tk.createImage( "" );
    Point point = new Point( 0, 0 );
    String name = "CanBeAnything";
    Cursor cursor = tk.createCustomCursor( image, point, name ); 
    jframe.setCursor( cursor );
  }

Relative Mouse Example

Here is a relative mouse movement example using the MouseInput2 class. Pressing the space bar will toggle between relative and absolute mouse movement. You can also enable and disable the mouse cursor with the C key. As before, if the rendering code is unfamiliar, please see the Java Games: Active Rendering tutorial.


import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;

public class RelativeMouseInput extends JFrame {
        
  static final int WIDTH = 640;
  static final int HEIGHT = 480;

  // Used for drawing rectangle
  Point point = new Point(0,0);
  // Used to toggle relative/absolute
  boolean relative = false;
  // Used to toggle the cursor
  boolean disableCursor = false;
  // Relative mouse input class
  MouseInput2 mouse;
  // Keyboard polling
  KeyboardInput keyboard;
  // Our drawing component
  Canvas canvas;

  public RelativeMouseInput() {
                
    // Setup specific JFrame properties
    setIgnoreRepaint( true );
    setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

    // Create canvas to force the drawing
    // surface to the correct size...
    canvas = new Canvas();
    canvas.setIgnoreRepaint( true );
    canvas.setSize( WIDTH, HEIGHT );
    add( canvas );
    pack();
    
    // Add key listeners
    keyboard = new KeyboardInput();
    addKeyListener( keyboard );
    canvas.addKeyListener( keyboard );
                
    // Add mouse listeners
    // For full screen : mouse = new MouseInput( this );
    mouse = new MouseInput2( canvas );
    addMouseListener( mouse );
    addMouseMotionListener( mouse );
    canvas.addMouseListener( mouse );
    canvas.addMouseMotionListener( mouse );
  }

  public void run() {
                
    canvas.createBufferStrategy( 2 );
    BufferStrategy buffer = canvas.getBufferStrategy();
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gd = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gd.getDefaultConfiguration();
    BufferedImage bi = gc.createCompatibleImage( WIDTH, HEIGHT );

    Graphics graphics = null;
    Graphics2D g2d = null;
    Color background = Color.BLACK;
                
    while( true ) {
      try {
        // Poll the keyboard
        keyboard.poll();
        // Poll the mouse
        mouse.poll();
                                
        // Exit the program on ESC key
        if( keyboard.keyDownOnce( KeyEvent.VK_ESCAPE ) )
          break;
                
        // Clear back buffer...
        g2d = bi.createGraphics();
        g2d.setColor( background );
        g2d.fillRect( 0, 0, WIDTH, HEIGHT );
                                
        // Display help
        g2d.setColor(  Color.GREEN );
        g2d.drawString( "Position: " + mouse.getPosition().toString(), 20, 20 );
        g2d.drawString( "Press Space to switch mouse modes", 20, 32 );
        g2d.drawString( "Press C to toggle cursor", 20, 44 );
        g2d.drawString( "Press ESC to exit", 20, 56 );

        // Process mouse input
        processInput();

        // Draw the rectangle
        g2d.setColor( Color.WHITE );
        g2d.drawRect( point.x, point.y, 25, 25 );
        
        // Blit image and flip...
        graphics = buffer.getDrawGraphics();
        graphics.drawImage( bi, 0, 0, null );
        if( !buffer.contentsLost() ) 
          buffer.show();
                                
        // Let the OS have a little time...
        try {
          Thread.sleep(10);
        } catch( InterruptedException ex ) {
                                        
        }
      } finally {
        // Release resources
        if( graphics != null ) 
          graphics.dispose();
        if( g2d != null ) 
          g2d.dispose();
      }
    }
  }
        
  protected void processInput() {
    // If relative, move the rectangle
    if( mouse.isRelative() ) {
      Point p = mouse.getPosition();
      point.translate( p.x, p.y );
      // Wrap rectangle around the screen
      if( point.x + 25 < 0 ) 
        point.x = WIDTH - 1;
      else if( point.x > WIDTH - 1 ) 
        point.x = -25;
      if( point.y + 25 < 0 ) 
        point.y = HEIGHT - 1;
      else if( point.y > HEIGHT - 1 ) 
        point.y = -25;
    } 
    // Toggle relative
    if( keyboard.keyDownOnce( KeyEvent.VK_SPACE ) ) {
      relative = !relative;
      mouse.setRelative( relative );
      setTitle( "Relative: " + relative );
    }
    // Toggle cursor
    if( keyboard.keyDownOnce( KeyEvent.VK_C ) ) {
        disableCursor = !disableCursor;
      if( disableCursor ) {
          disableCursor(); 
      } else {
          // setCoursor( Cursor.DEFAULT_CURSOR ) is deprecated
          setCursor( new Cursor( Cursor.DEFAULT_CURSOR ) );
      }
    }
  }
  
  private void disableCursor() {
    Toolkit tk = Toolkit.getDefaultToolkit();
    Image image = tk.createImage( "" );
    Point point = new Point( 0, 0 );
    String name = "CanBeAnything";
    Cursor cursor = tk.createCustomCursor( image, point, name ); 
    setCursor( cursor );
  }
        
  public static void main( String[] args ) {
    RelativeMouseInput app = new RelativeMouseInput();
    app.setTitle( "Simple Mouse Example" );
    app.setVisible( true );
    app.run();
    System.exit( 0 );
  }
}

Now What?

That about wraps up the Keyboard and Mouse input needed for learning game programming with Java. Please see the references section for more web sites and tutorials if you want to dig deeper and understand more about this topic. Also be aware that the JInput project is attempting to provide all of these behaviors, including joystick support, so it might be worth while to check it out.

References

Article written by Tim Wright
Copyright(C) 2007 - All rights reserved

Cancel Save
0 Likes 2 Comments

Comments

fernandomoorie

Man, this guide is a lifesaver! I’ve been stuck trying to get the keyboard input right in my Java game for weeks. It’s awesome how you broke it down, especially the mouse event part. Super helpful!

September 10, 2024 10:53 AM
johnjacksoon2000

Dude, this Java input tutorial is solid, but I gotta ask, have you tried adding controller support as well? I mean, keyboard/mouse is cool, but controllers are kinda standard now, right? Like, for game dev nowadays, players want both. Maybe you could dive into that or check out crytech. They go pretty deep into input handling, might give you some fresh ideas to expand on this. But honestly, killer job here, just think it could go further with more input options, y'know?

September 10, 2024 11:24 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement