In previous post I explained how to detect keyboard events (“key pressed”) and how to create a simple game controlled by keyboard. This time we will do a similar thing for mouse.
(And because this game turned out to be too simple, I have also added sound effects to make it more fun.)
Responding to mouse events
There are many types of mouse events. You will probably only need a few of them, but here is an overview:
mouse cursor has entered the game window (or its specific part);
mouse cursor was moved;
mouse cursor has left the game window (or its specific part);
mouse button was pressed (while the cursor was in the game window);
mouse button was released;
mouse button was clicked (or double-clicked, etc.);
mouse cursor was moved while a mouse button was held (i.e. dragging);
mouse wheel was rotated.
Today’s game will display a bubble at a random position on the screen, and the player can pop the bubble using the mouse cursor. The bubble is popped by moving the mouse cursor into it; no clicking is required. (This is a game for little kids who are learning how to move the mouse; clicking would already be too difficult for them.) Therefore, we will use a MouseMotionListener
and respond to the “mouse moved” events.
Mouse movement is reported in short time intervals determined by the operating system. Note that if the user moves the mouse very fast, it is possible to “jump over” the bubble - for example, at one moment the mouse cursor is to the left of the bubble, and at the next moment the mouse cursor is already to the right of the bubble, apparently going over the bubble, but never actually reported inside it. One possible approach is to simply ignore this. Another possible approach would be to interpolate the mouse movement; for example to calculate whether the line between two consecutive reported positions would intersect the bubble, and consider the bubble popped if it did. You do not have to worry about this too much, because when the mouse movement slows down, its location will be reported more precisely. Specifically, the location where the mouse stops will be reported as the last “mouse moved” event.
To avoid writing empty methods, we can use a MouseMotionAdapter
, which implements MouseMotionListener
. The code for detecting mouse movement could look like this:
canvas.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
int x = e.getX();
int y = e.getY();
if (...) { // cursor is at an important place
... // update the game state
}
}
});
Alternatively, we could use MouseAdapter
, which implements all three MouseListener
, MouseMotionListener
, and MouseWheelListener
interfaces. This would come handy if we needed to respond to multiple mouse events from different interfaces. However, we would need to assign the adapter into a variable, and register it using all the necessary “addMouseListener”, “addMouseMotionListener”, and “addMouseWheelListener” methods; otherwise we would only receive the events related to the registration method we used.
Similarly to key events, we need to make the canvas get focus within the game window:
canvas.
requestFocusInWindow
();
A few more notes about mouse events:
The relation between “mouse pressed”, “mouse released”, and “mouse clicked” is similar to the relation between “key pressed”, “key released”, and “key typed”. The former two represent the technical details; the last one provides a summary of what happened from the perspective of a typical application.
The mouse wheel is considered to be another mouse button with some extra functionality. Pressing and releasing the mouse wheel results in the usual “mouse pressed”, “mouse released”, and “mouse clicked” events. Only rotating the wheel results in a special “mouse wheel rotated” event.
Responding to window events
Instead of using a fixed-sized game window, in this game I decided to maximize the window. The question is how large will the canvas become? It will not occupy the entire screen, because there is still some place needed for a task bar and window borders, so the canvas will be a little smaller. We need to wait until the window fills the available space on the screen and arranges its contents, and then check how large the canvas has become.
The problem is that the window will not resize itself and its contents immediately after we asked it to maximize. Instead, it will schedule the maximization and content layout to happen “soon” - shortly after the method we used to create the window has completed. But how can we run the canvas measuring code after the frame creation code has entirely completed? To achieve this, we need another kind of events and listeners.
There are many types of window events:
window was opened (i.e. made visible for the first time);
window is going to be closed (but you could still cancel this);
window has closed already (time to release the resources it used);
window became active;
window stopped being active;
window was minimized;
window was un-minimized;
window was maximized (maybe only horizontally or only vertically), etc.;
window gained focus;
window lost focus.
(The difference between being “active” and having “focus” is that the former only applies to application windows, but the latter also to their dialogs. When your application opens a dialog, the main application window remains “active”, but now the dialog has the “focus”.)
In this example, we will need the “window opened” event. That is when the window became visible, and its contents were arranged, so we can measure the size of the canvas.
We will also use the “window closed” event, because we will allocate some system resources for playing the sound effects, and closing the window is the proper opportunity to release those no longer needed resources. (In this case, this is not strictly necessary, because when the only application window is closed, the entire application ends, and all its resources are released automatically. However, releasing the resources is good practice, and we are already using the window events anyway.)
Again, we have interfaces WindowListener
, WindowStateListener
, and WindowFocusListener
; and a WindowAdapter
class that implements them all. Unlike the key and mouse events, these events will not originate at the canvas, but at the application main window (JFrame
). Therefore, the code will look like this:
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowOpened(WindowEvent e) {
... // the window is visible and ready
}
@Override
public void windowClosed(WindowEvent e) {
... // release the resources
}
};
Playing a sound effect
The API for playing audio in Java is a bit complex, but our needs at this moment are simple, because we only need to play one sound effect repeatedly, and the sound is short enough to have the audio data entirely loaded in memory. This avoids many of the complications.
Clip
can load a short sound into memory and then play it. It has simple methods called “start” and “stop”; although it is probably better to think about them as unpause and pause, because if you stop the sound and start it again, it will continue playing from the moment it was stopped. To restart a Clip
that was already played, you need to call “setMicrosecondPosition” or “setFramePosition” with parameter 0.
To specify the sound, you need to provide an AudioInputStream
, which reads and decodes the sound data from an InputStream
. New Clip
and AudioInputStream
objects are obtained using static methods of the AudioSystem
class.
The sound can be stored in a AIFF, AU, or WAV file. All of these formats are supported out of the box. They are uncompressed, but for a short sound effect it is okay. (For longer audio, such as background music, you would probably want a compressed format like OGG or MP3, but that would require adding a library.)
A simple code to play a sound effect could look like this:
try (
InputStream is = …;
AudioInputStream ais = AudioSystem.getAudioInputStream(is);
Clip clip = AudioSystem.getClip()
) {
clip.open(ais);
clip.start();
… // wait for a moment before you stop the sound
clip.stop();
} catch (UnsupportedAudioFileException e) {
… // choose a different format or use a library
} catch (IOException|LineUnavailableException e) {
… // some other problem
}
There are a few things that could go wrong, and many of them are out of your control. Maybe your game was started on a computer that has no sound output. Maybe it can only play a limited number of sounds at the same time, and the user already has other applications playing. All you can do is carefully catch the exceptions, and make sure your program doesn’t crash. (It would be quite sad if a Java program crashed just because the user started an MP3 player in another window.)
Note that the sounds are played asynchronously. That is, when you call “clip.start()”, your program immediately continues at the next line, and the sound starts playing in the background. So if you want to make a simple program just to test the sound, you need to introduce some time delay between “clip.start()” and “clip.stop()”.
An example game
Do you remember how difficult it was to use the mouse for the first time? This game tries to make it more fun, by letting you pop bubbles by pointing at them. (The target audience is like 3 years old kids. But actually, it’s kinda addictive for the adults, too, when they are bored.) The source code is at gitlab.com/kittenlord/demo002.
The game opens a maximized window and shows you a circle. If you move the mouse into the circle, it will disappear with a sound effect, and appear at a different place, slightly smaller (until it reaches the minimum size, then it stops getting smaller).
Class Resources
provides the application icon, and the sound effect.
Class Sound
handles all technical details related to playing the sound effect. Methods “open” and “close” allocate and release the resources. Method “play” starts playing the sound effect. If any problem happens during initialization, the sounds will not be played, but all exceptions are handled here. The game is playable without the sounds, too.
Class GameState
remembers the position and the size of the bubble. Method “initialize” remembers the canvas size and places the first bubble; it can also return that the canvas is too small.
Method “drawTo” paints the game state. Method “getScore” returns the current score, i.e. the number of bubbles already popped.
Method “inBubble” detects whether a coordinate pair is inside the circle, using the Pythagorean theorem. Methods “placeBubble…” set the coordinates for a new bubble. Method “tryCollide” wraps the entire logic of moving a mouse cursor somewhere.
Finally, class Game
creates a window, maximizes it, sets the event listeners, and initializes the game state and sounds at the right moment.
This game turned out to be much easier than I expected. Having no previous experience with Java Sound API, I was a bit afraid of it, so I wanted to keep the rest of the game simple. It turned out that although the API is complex in general, we only need a tiny part of it in order to play a sound effect. By the way, the sound effect was generated by sfxr.me, you may find it useful for your own games, too.