Mouse and Joystick Input
Moving Your Game to Windows, Part II
By Peter Donnelly
Abstract
This article, the second in a series on converting MS-DOS® games to the
Microsoft® Win32® environment, covers joystick and mouse input. The emphasis is
on real-time games that require immediate data from input devices. The joystick
routines are based on DirectInput™ versions 1.0 through 3.0—really just the
extended services provided by the Windows® platform itself. For the mouse, I'll
look at the conventional Windows-based functions and then at the new services
offered by DirectInput 3.0.
It is assumed that you have a general understanding of game design, along
with MS-DOS programming skills with C or C++, and are familiar with the basics
of Windows programming.
"Moby Dick"—Revised
In the first article of this series I introduced a simple MS-DOS® game
called "Moby Dick" and took it through the first stages of
development for the Microsoft® Win32® environment. Now, I've added joystick and
mouse input routines to both versions of the game. The keyboard input routines
are unchanged. To keep the code as manageable as possible, I've stripped out
the priority-settings dialog that I used for experiments in the Windows®-based
version accompanying Part I.
As before, the MS-DOS version is mostly plain-vanilla C. It compiles without
revision under Turbo C++ 3.0. If you're not using the Turbo project files, be
sure to turn off register variables. The Windows version is a Microsoft Visual
C++ ® 4.0 project, but it can readily be adapted for other compilers. It uses
C++ calls to DirectInput methods but otherwise is class-free.
If you have a joystick attached to your computer, it is automatically
enabled in Moby Dick Windows except when mouse movement is turned on through
the Settings menu. There are actually two different mouse interfaces,
depending on whether or not USE_DIRECTINPUT was defined during compilation.
(The compiled version supplied with this article doesn't use DirectInput. If
you wish to recompile the code with the define, you need to have the DirectX™ 2
or DirectX 3 software development kit [SDK].) In the first interface variant,
using the standard Windows function GetCursorPos, Ahab (the ship
captain) always moves toward the cursor. In the DirectInput version, he moves
in response to mouse movements, regardless of the cursor's position.
DirectInput
In the past, game developers shunned Windows because it denied them the
performance and control they were used to in the MS-DOS environment. Animation
has been the biggest logjam—effectively cleared, one hopes, with DirectDraw®
and Direct3D™. There are, however, other issues, one of which is input.
Today's real-time games, with their ever-increasing frame rates, demand
instant response to a player's actions. DirectInput attempts to meet this need
for immediate access to the keyboard, mouse, and joystick by letting the
application read these devices at the lowest possible level consistent with the
device-independent nature of a Windows-based application.
The DirectX 3 SDK, which was released in September 1996, is the first
version of DirectX to contain DirectInput. DirectInput was not actually part of
the DirectX 2 SDK or its predecessor, the Windows 95 Game SDK. In those
versions, DirectInput consisted entirely of the extended joystick services
built into the standard Windows application programming interface (API),
together with the Windows calibration applet in Control Panel and a driver
model that enabled support for digital joysticks.
DirectX 3 has widened the scope of DirectInput to include a COM-based API
for the mouse and keyboard. (COM stands for Component Object Model, which is a
sort of protocol that lets software modules communicate. You should have at
least a smattering of COM theory before you start using DirectX.) A COM-based
API for the joystick is slated to be added in a future version of DirectX, but
for now you'll have to be content with the standard Windows functions, which do
pretty well for most purposes.
Incidentally, the new keyboard services provided by DirectInput provide some
advantages over GetAsyncKeyState, which I used in the first version of
Moby Dick Windows. New capabilities include reading all keys, not just those
represented by virtual key codes, and setting up event objects for
multithreading. But for now Moby Dick Windows will stick with GetAsyncKeyState,
which does an acceptable job of reading the keyboard in real time.
Programming the Joystick
In DirectInput lingo, the term joystick also encompasses game pads,
flight yokes, pedal systems, and similar devices. I'll use the term here with a
similar range of meaning.
You can read the joystick either through Windows messages or by polling. In
the first method, a window "captures" the joystick with joySetCapture
so that the window is notified whenever the joystick is moved or a button is
pressed. This makes it possible to turn the stick into a pointing device like
the mouse: you can move a pointer on the screen every time the angle of the
stick is altered. Moby Dick, however, is concerned with the position of the
joystick on each pass through the game loop rather than with the stick's actual
movements, so you want to use joyGetPosEx to do your own polling rather
than monitoring the message queue.
Another reason not to use joySetCapture is that joystick messages are
limited to three axes of movement (left-right, forward-back, and throttle, for
example) and four buttons. This obviously won't do for a modern input device
like the Microsoft Sidewinder 3D Pro, which has eight buttons, a point-of-view
hat, and four axes of movement (including a throttle slider and a twisting
motion on the stick itself). For the latest joysticks and game pads you
definitely need the power of joyGetPosEx, which returns the state of up
to 32 buttons, six axes, and the POV hat, besides transparently supporting both
digital and analog devices.
The API reference advises using the old joyGetPos function for
joystick devices that employ no more than three axes and four buttons. It's
true that joyGetPos is a bit simpler to implement, but joyGetPosEx
works with all joysticks, and I see no reason not to use it exclusively.
To include the extended joystick services in your program, you need to link
to WINMM.LIB and include MMSYSTEM.H.
Detecting the Joystick
To detect the primary joystick, simply call joyGetPosEx with the
JOYSTICKID1 argument:
JOYINFOEX joyInfoEx;
ZeroMemory(joyInfoEx, sizeof(joyInfoEx);
joyInfoEx.dwSize = sizeof(joyInfoEx);
BOOL JoyPresent = (joyGetPosEx(JOYSTICKID1, &joyInfoEx) == JOYERR_NOERROR);
The function returns an error (nonzero) if the joystick is not properly
installed in Windows or is simply unplugged.
Note that, as with many of the other DirectX functions, you have to let joyGetPosEx
know how big a structure it has to fill—annoying, but necessary for forward
compatibility. On the bright side, you don't have to worry about the dwFlags
field here, since you don't require any of the values returned in the
structure.
Calibrating the Joystick
You don't need to. (Hey, I'm starting to like this Windows stuff!) The user
is responsible for selecting and calibrating the joystick under Control Panel.
If you want to give the user a chance to do this from the game menu, just call
the applet:
WinExec("control joy.cpl", SW_NORMAL);
The "Dead Zone"
In Moby Dick MS-DOS, I did some arithmetic in order to create a "dead
zone" around the center of the X and Y axes. Without this zone it would be
almost impossible to move the ship along just one axis, as the slightest
deviation of the joystick on the other axis would cause the ship to move
diagonally.
At first glance it appears that joyGetPosEx is going to simplify life
by setting up an automatic dead zone. To quote the API reference, the JOY_USEDEADZONE
flag "expands the range for the neutral position of the joystick and calls
this range the dead zone. The joystick driver returns a constant value for all
positions in the dead zone."
The wording is a bit confusing, and using the dead zone is a bit more
complicated than the rather terse documentation suggests. First of all, it's
your responsibility to put the dead zone boundaries in the registry (under the
keys defined in REGSTR.H) and then notify the system by calling joyConfigChanged.
After you've done that, you still have to figure out the constant value
returned for the dead zone. (One way to do it is to set the dead zone to the
same size as the whole range of the joystick, read the axis positions, and then
reset the dead zone to the size you actually want.) And finally, when you close
your program you really should restore the original registry values, because
other applications may expect the defaults.
All in all, it's simpler for us to maintain your own dead zone. You'll do
the arithmetic in Moby Dick Windows just as in the MS-DOS version.
Using the Throttle
Just for fun, and to show you how easy it is to read any of the joystick
controls, I've added a throttle to Ahab's ship in Moby Dick Windows. This will
work with any device that has a third—or a Z axis—in addition to the X and Y
axes on the stick itself. Typically the third axis is controlled by a slider or
wheel on the base of the unit.
This is all you have to do to get the throttle information:
JOYCAPS joyCaps;
JOYINFOEX joyInfoEx;
ZeroMemory(joyInfoEx, sizeof(joyInfoEx);
// see whether a throttle is available
joyGetDevCaps(JOYSTICKID1, &joyCaps, sizeof(joyCaps));
BOOL JoyHasThrottle = (joyCaps.wCaps & JOYCAPS_HASZ);
// if so, read its position
if (JoyHasThrottle)
{
joyInfoEx.dwSize = sizeof(joyInfoEx);
joyInfoEx.dwFlags = JOY_RETURNZ;
joyGetPosEx(JOYSTICKID1, &joyInfoEx);
}
Now joyInfoEx.dwZpos gives you the position of the throttle. You can
calculate its relative position from the wZmin and wZmax fields
of joyCaps.
You can, of course, combine JOY_RETURNZ with other flags (using the
"or" operator) to get information on the stick position, button
states, and so on, with a single call.
Besides X, Y, and Z, three other axes can be monitored: R (for Rudder), U,
and V. These will accommodate foot pedals, twisting sticks, and various other
sliders, knobs, and dials, but not the POV hat, whose position is returned
instead in the dwPOV field of the JOYINFOEX structure. (I'm
looking forward to implementing the hat in the 3-D version of Moby Dick!)
Mouse
Cursor Visible by Default
Moby Dick MS-DOS doesn't actually use the mouse cursor, and by default it is
invisible when you switch to graphics mode. In Windows, of course, the opposite
is true. The cursor is always present unless you specifically hide it. Hiding
the cursor is not a good idea except in full-screen applications.
Custom Cursors
Implementing a custom cursor under MS-DOS requires that you assign a bitmap
to the cursor image using MS-DOS interrupt 33h, subfunction 09h. If the cursor
changes shape at different points on the screen, the cursor's position has to
be restored after each call to the interrupt. Naturally, the application is
responsible for keeping track of where the cursor is and changing the shape
accordingly.
You have to think a bit differently about cursors under Windows, but
implementing custom cursors is no more difficult. If you already have
hard-coded image data from your MS-DOS game that you want to keep, you can
generate a cursor on the fly with CreateCursor. In most cases, however,
it makes more sense to draw the image in a resource editor, link the resource
file to the application, and make the image available at runtime with LoadCursor.
(The standard cursors have to be loaded as well. Notice the use of LoadCursor
in the window definition section of Moby Dick's WinMain function. This
is where you set the default cursor shape that Windows will use when the cursor
is inside the application window.)
You can change the cursor to another shape at any time with SetCursor,
which takes the cursor handle as its only argument. Since LoadCursor
simply returns the existing handle if the cursor has already been loaded, the
following line of code can be executed inside a loop without any waste of
system resources:
SetCursor(LoadCursor(MAKEINTRESOURCE(IDC_MYCURSOR)));
If you want to use custom cursors, you have to set the window cursor to
NULL. From then on, you're responsible for cursor management within your
window. See the API reference for SetCursor.
One final note on cursor shapes. If you've written MS-DOS–based games for
SVGA modes, you know that for most cards even the basic pointer has to be
implemented in software. This is of course unnecessary under Windows, which
cheerfully adapts itself to any display mode supported by the graphics driver.
What to Do When the Mouse Escapes
Because MS-DOS-based games are full-screen by definition, you don't have to
worry about what happens when the mouse wanders outside the boundaries. In a
Windows-based game, however, it may be necessary to keep track of the mouse
cursor even when it is not over the application window.
Normally, Windows sends mouse messages to an application or dialog only when
the mouse activity takes place inside the application or dialog window. Also,
it sends different messages depending on whether the activity is within the
client area or the nonclient area (for example, on the menu bar or border).
Most applications, including simple point-and-click strategy games, are
concerned only with client-area messages. A game, however, may need to know
what the mouse is up to when it is outside the client area. A common example
would be a strategy game with a map that automatically scrolls when the cursor
is moved outside the client area. In the case of Moby Dick, my non-DirectInput
mouse interface requires that Ahab move toward the cursor whether or not the
cursor is within the game window. (Remember, this applies only if the program
has been compiled without USE_DIRECTINPUT defined.)
Under Windows 3.1, the Moby Dick application could have used the SetCapture
function to "capture the mouse," that is, direct all mouse messages
to the game regardless of the cursor location. This isn't possible under
Windows 95, because of asynchronous input processing—one of the ways Windows
95 keeps any one application from monopolizing the system. You can still
capture the mouse, but only when a button is depressed. Obviously this isn't
what I want in Moby Dick.
There are a couple of rather more devious ways of capturing the mouse, both
of which require the installation of a hook to intercept messages intended for
windows outside your application. (See the Marsh and Richter entries in the
bibliography.) However, for Moby Dick I don't need to jump through those hoops,
because all I care about is cursor position, not clicks. Fortunately there is
an API function, GetCursorPos, which returns the present position of the
cursor no matter where it is on screen. In other words, I don't need to rely on
messages delivered to my message pump; instead, I can poll the cursor whenever
I need to know where it is.
There is one small wrinkle. GetCursorPos returns the position of the
cursor in screen coordinates—naturally enough, since it tracks the cursor
anywhere it happens to wander. However, the graphics functions within my
program are based on window coordinates, because they are independent of the
window's position; in other words, the coordinates remain the same regardless
of where the window is on the screen at any given moment. That's where ClientToScreen
comes in. It converts a point within the window from its relative coordinates
to its absolute or screen coordinates, taking the current position of the
window into account.
The following snippet shows how to determine Ahab's bounding rectangle in
screen coordinates, for comparison with the cursor position.
RECT AhabRect;
// Calculate window coordinates of the ship's grid rectangle.
// The coordinates are derived from the row and column of the
// play grid multiplied by the size of a cell in pixels.
AhabRect.top = AhabY * SPRITE_HT;
AhabRect.bottom = AhabRect.top + SPRITE_HT;
AhabRect.left = AhabX * SPRITE_WD;
AhabRect.right = AhabRect.left + SPRITE_WD;
// Convert to screen coordinates. We treat a RECT as an array
// of two POINTs (left top and right bottom).
ClientToScreen(hMainWindow, (POINT*) &AhabRect);
ClientToScreen(hMainWindow, (POINT*) &AhabRect.right);
There's one other way of making sure your program never loses control of the
mouse: Maximize the game window at startup and don't provide a restore button.
This may seem a little high-handed at first. But when you think about it, not
many people are going to take part in a real-time dungeon crawl in one window
while continuing to work on a spreadsheet in another. It's not quite good
Windows manners for an application to elbow aside all others while it has the
foreground, but in this case you can rest assured that Emily Post is not
looking.
Mouse Clicks
Using only the standard Windows API, there are at least three ways to
monitor mouse clicks within your application's window:
- Mouse messages. This
is the most reliable way of catching every click. The disadvantage is that
rapid clicking (e.g. in a shoot-'em-up game) will pile up messages on the
queue, and they may not be dealt with in a timely way. If you're keeping
track of cursor movements by polling with GetCursorPos, you may
have trouble synchronizing responses to mouse clicks and responses to
cursor movements.
- GetKeyState. This
function can be used to check on the status of a mouse button, but just as
with keys, it does not return the actual present state of the hardware.
Rather, it tells you what state the button was in when the last associated
message was retrieved from the queue. For example, GetKeyState(VK_LBUTTON)
returns a negative value (high-order bit is 1) if the last mouse button
message retrieved from the queue was WM_LBUTTONDOWN.
The low-order bit of the return value for GetKeyState
is actually just a toggle that is reversed each time the button (or key) is
pressed. The following line of code, executed somewhere in the game loop,
allows the user to turn the music on and off with the right button:
BOOL MusicOn = (GetKeyState(VK_RBUTTON) & 1);
GetKeyState is really intended to
be used in response to a message that sets the button state. As Petzold (p.
296) points out, you can't wait for a mouse click with a statement like this:
while (GetKeyState(VK_LBUTTON) >= 0); // *** doesn't work! ***
Furthermore, polling a mouse button like
this:
if (GetKeyState(VK_LBUTTON) < 1) DoSomething();
is not going to catch every click, because
the user may have pressed and released the button since the mouse was
last polled.
- GetAsyncKeyState. If
you're not going to use DirectInput 3.0 to keep track of mouse clicks, and
if your game loop is not based on the message queue, GetAsyncKeyState
is probably the best way to go. The first article in this series showed
this function in action retrieving keystrokes in real time. It works
exactly the same way for the mouse, using the virtual keys VK_LBUTTON,
VK_RBUTTON, and (for three-button mice) VK_MBUTTON. Note that these
virtual keys are mapped to the actual buttons on the mouse, not the
primary and secondary buttons as defined by the user in Control Panel.
Following the Mouse with DirectInput
I've covered some of the conventional ways of reading the mouse under
Windows. Now, let's look at the alternative provided by DirectInput 3.0
DirectInput has one big advantage over standard Windows functions: speed.
Like the other components of DirectX, it works directly with the hardware,
bypassing the message queue and even the manipulation of data necessarily
performed by a function like GetCursorPos. Besides that, it allows you
to check the state of all axes and buttons at once, with a single call.
Hand in hand with this speed advantage comes one big disadvantage for some
kinds of game development. DirectInput is interested solely in the mouse;
it doesn't give a hoot about the cursor. Using it to track the position
of the standard Windows cursor would be an awkward business at best. So, if
your game uses the mouse as a joystick substitute, DirectInput is probably the
best tool for the job. But if the mouse is guiding a cursor around the screen,
you may want to stick to GetCursorPos. (You can still use DirectInput
for reading the buttons, though.)
Note The Scrawl sample application that
comes with DirectX 3 does implement a DirectInput-driven cursor. It is not a
standard Windows cursor, though, and does not pay any attention to Control
Panel mouse settings.
Setting up the mouse with DirectInput requires several steps.
- Create the DirectInput
object. This sets up the basic framework for handling all input.
- Enumerate the devices.
You can use EnumDevices to obtain the GUID (globally unique
identifier) for any input devices attached to the system. However, this
step can be skipped if you are interested only in the standard system
mouse, because DirectInput provides a default GUID for the mouse which can
be used in the next step.
- Create the mouse device
object. This step creates a code object for the mouse and attaches it
to the DirectInput object.
- Set the cooperative level.
You let the system know the circumstances under which you want to have
access to the mouse (for example, whether you want your application to
have access when it is in the background).
- Set the data format.
Tell DirectInput how to return the information you want about the mouse.
Remember, you're going to get all the data you want about axes and buttons
with a single call.
- Acquire the mouse.
Finally, tell the system to direct all mouse input to the application
window. This step has to be repeated whenever your window has
"lost" the mouse because it has been used for something else.
Here's how all this looks in the INPUT.CPP module of Moby Dick Windows.
Incidentally, the syntax here is C++, but DirectInput can be used with plain C,
as examples in the DirectX 3 SDK illustrate.
extern HINSTANCE hTheInstance; // program instance
extern HWND hMainWindow; // app window handle
extern BOOL MouseAcquired;// our own flag
static LPDIRECTINPUT lpdi; // DirectInput interface
static LPDIRECTINPUTDEVICE lpdiMouse; // mouse device interface
BOOL InitInput(void)
{
if(DirectInputCreate(hTheInstance, DIRECTINPUT_VERSION, &lpdi, NULL)
!= DI_OK)
return FALSE;
// We'll skip the enumeration step, since we care only about the
// standard system mouse.
if(lpdi->CreateDevice(GUID_SysMouse, &lpdiMouse, NULL)
!=DI_OK)
return FALSE;
if(lpdiMouse->SetCooperativeLevel(hMainWindow,
DISCL_NONEXCLUSIVE | DISCL_FOREGROUND) != DI_OK)
return FALSE;
// Note: c_dfDIMouse is an external DIDATAFORMAT structure supplied
// by DirectInput.
if (lpdiMouse->SetDataFormat(&c_dfDIMouse) != DI_OK)
return FALSE;
if (lpdiMouse->Acquire() != DI_OK)
return FALSE;
MouseAcquired = TRUE;
return TRUE;
} // InitInput()
Notice that DirectInput is kind enough to provide you with a default data
format for SetDataFormat so that you don't have to go through a lot of
rigmarole setting one up.
So much for the initialization. Now you want to reap the fruits of your labors
by polling the mouse. Here's a code fragment that checks for the current
position of the mouse and does something if the left button is pushed. Note
that MouseXY is tracking the absolute position of the mouse, whereas diMouseState
holds the relative coordinates. (More on this in a minute.)
POINT MouseXY;
BOOL MouseAcquired;
DIMOUSESTATE diMouseState;
if (MouseAcquired)
{
if (lpdiMouse->GetDeviceState(sizeof(diMouseState), &diMouseState)
== DI_OK)
{
MouseXY.x += diMouseState.lX;
MouseXY.y += diMouseState.lY;
if (diMouseState.rgbButtons[0]) DoSomething();
}
}
Remember, GetDeviceState does not report the position of the cursor
relative to either the screen or the window; it reports the travel of the mouse
(in "mickeys," a mickey being the smallest movement of the
mouse that can be recorded). In other words, if the cursor reaches the edge of
the screen but the user keeps pushing the mouse in that direction, the axis
values reported by GetDeviceState keep changing. If the user has mouse
acceleration turned on, moving the Windows cursor a given distance may take
more or fewer mickeys, depending on when acceleration kicks in. (By now you
should have some idea of the acrobatics that would be necessary for tracking
the Windows cursor position with GetDeviceState.)
By default, GetDeviceState returns the position of the mouse relative
to its position at the last call. This can be changed with SetProperty
so that absolute values are returned, but these values are really just the sum
of all the relative movements, and your application can just as easily do the
arithmetic itself, as in the example above. (If you do want to use SetProperty,
it has to be done just before acquiring the mouse, after all the other setup
has been done. See the commented-out example in INPUT.CPP.)
Before leaving the subject of DirectInput I'll mention one more function, GetDeviceData.
Rather than checking the current state of the hardware, GetDeviceData
reads from a buffer maintained by DirectInput. By using GetDeviceData
rather than GetDeviceState you can respond to button presses or small
movements that might otherwise be lost because they were immediately followed
by a counteraction (for example, a button press immediately followed by
release) before your game loop had a chance to check the state of the mouse.
Items in the buffer are time-stamped and given a sequence number so they can be
processed appropriately.
For more information on how to use DirectInput with the mouse and keyboard,
see my article "Programming
Mouse and Keyboard with DirectInput 3."
Bibliography
Edson, Dave. "Get World-Class Noise and Total Joy from Your Games with
DirectSound and DirectInput." Microsoft Systems Journal 11
(February 1996). (MSDN Library, Periodicals)
Marsh, Kyle. "Win32 Hooks." (MSDN Library, Technical Articles)
Microsoft Corporation. "Developing for the Microsoft SideWinder 3D
Pro." (MSDN Library, Technical Articles)
Microsoft Corporation. "Developing for the Microsoft SideWinder Game
Pad." (MSDN Library, Technical Articles)
Microsoft Corporation. "Extending DirectInput's Joystick Subsystem
Services." (MSDN Library, Technical Articles)
Petzold, Charles. Programming Windows 95. (Microsoft Press, 1996).
Richter, Jeffrey. "Win32 Questions and Answers." Microsoft
Systems Journal 10 (April 1995). (MSDN Library, Periodicals)
Peter Donnelly has been a game designer since 1972 and a programmer since 1984. He has authored four "paper" games with historical and fantasy themes, and his computer design credits include three interactive comic-book adventures and one full-scale adventure game. He has also developed shareware games and game editors for MS-DOS and Windows. |