Jump to content

  • Log In with Google      Sign In   
  • Create Account

Like
1Likes
Dislike

Java Games: Keyboard and Mouse

By Tim Wright | Published Nov 29 2007 07:22 PM in General Programming

mouse public void keyboard bob new canvas synchronized
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource



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© 2007 - All rights reserved








Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS