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
- "Java Games: Active Rendering" by Tim Wright - My tutorial about Active Rendering
- "How to Write a Key Listener" - Java's tutorial for writing a key listener
- "How to Write a Mouse Listener" - Java's tutorial for Mouse listeners
- "How to Write a Mouse-Motion Listener" - Java's tutorial for Mouse Motion listeners
- "Killer Game Programming in Java" by Andrew Davison - Great book with lots of source code to download
- "JInput" - The JInput Project hosts an implementation of an API for game controller discovery and polled input
Article written by Tim Wright
Copyright(C) 2007 - All rights reserved
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!