Working with the Mouse (and Joystick)


I'm not going to talk about basic topics like grabbing WM_MOUSEMOVE and pulling screen coordinates out of the LPARAM. Many books have been written to cover these programming techniques. If you need a primer on Win32 and GDI, I suggest you read Charles Petzold's book: Programming Windows: The Definitive Guide to the Win32 API. Besides, if you don't know how to read mouse input you're probably in the wrong class. The prerequisite for this class is "Beginning Windows Programming" down the hall. But don't forget to come back when you're done.

Instead, I'll focus on giving you some good advice about using mouse input in games, and show you a few of my special tricks. First, here is the advice you should follow when using the mouse.

  1. Stay with well known conventions. There are plenty of standard conventions for using the mouse control, from Microsoft Windows to Quake. When you sit down to write your interface code, don't be tempted to be a cowboy programmer and break out into new directions with the interface. You'll likely end up with a lot of arrows in your back—ouch! After all, before the shooter-style game was popular, how many games used the mouse as a model for a human neck? This idea worked well in a case like this for two reasons: It solved a new problem and the solution was intuitive.

    Best Practice

    If you're solving an interface problem that has a standard solution and you choose a radically different approach, you take a risk of annoying players. If you think their annoyance will transition into wonder and words of praise as they discover (and figure out) your novel solution, then by all means give it a try. Make sure you test your idea first with some colleagues you can trust. They'll tell you if your idea belongs on the garbage heap. Be careful with interfaces, however. A friend of mine once judged the many entrants into the Indie Games Festival (www.indiegames.com) and he said the biggest mistake he saw that killed promising entrants was poor interface controls. He was amazed to see entries with incredible 3D graphics not make the cut because they were simply too hard to control. In short, don't be afraid to use a good idea just because it's already been done.

  2. Avoid context sensitive controls. Context sensitivity in controls can be tough to deal with as a player. It's easy to make the mistake of loading too much control onto too little a device. The Ultima games generally went a little too far, I think, in how they used the mouse. A design goal for the games was to have every conceivable action be possible from the mouse, so every click and double click was used for something. In fact, the same command would do different things if you clicked on a person, a door, or a monster. I'm sometimes surprised that we never implemented a special action for the "shave and a haircut, two bits" click.

  3. Use the cursor for user feedback. One thing I think the Ultima games did well was how they used the pointer or cursor. The cursor would change shape to give the player feedback on what things were and whether they could be activated by a mouse command. This is especially useful when your screens are very densely populated. When the mouse pointer changes shape to signify that the player can perform an action, players immediately understand that they can use the pointer to explore the screen.

  4. Avoid pixel perfect accuracy. It's a serious mistake to assume that players of all ages can target a screen area with pixel perfect accuracy. An example of this might be a small click target on a draggable item or a small drop point on the screen. Anything that will change as a result of a mouse click should have a little buffer zone, widening the available target area. If the area is already pretty large, like the menu bar of a window, you can get away without using the buffer zone.

    Anyone who has attempted to cast spells in the original version of Ultima VIII will agree. The reagents that made some of the spells work had to be placed exactly. This requirement made spell casting frustrating and arbitrary. Both the mouse and the joystick are moved with bigger muscles in the arm and wrist, which are less accurate than the index finger.

Gotcha

The Ultima VII mouse code detected objects on the screen by performing pixel collision testing with the mouse (x,y) position and the sprites that made up the objects in the world. Most of these sprites were chroma-keyed, and therefore had spots of the transparent color all through them. This was especially true of things like jail cell bars and fences. Ultima VII's pixel collision code ignored the transparent color, allowing players to click through fences and jail cell bars to examine objects on the other side. That was a good feature, and it was used in many places to advance the story. The problem it created, however, was that sometimes the transparent colored pixels actually made it harder for players to click on an object. For example, double clicking the door of the jail cell was difficult. If you use an approach like this, take some care in designing which objects are active, and which are simply scenery and make sure you make this clear to your players.

This is an extremely important issue with casual games or kids games. Very young players or older gamers enjoy games that include buffer zones in the interface because they are easier to play.

A Tale from the Pixel Mines

With Ultima VIII, the left mouse button serves as the "walk/run" button. As long as you hold it down, the Avatar character will run in the direction of the mouse pointer. Ultima games require a lot of running; your character will run across an entire continent only to discover that the thingamajig that will open the gate of whosiz is back in the city you just left, so you go running off again. By the time I'd played through the game the umpteenth time, my index finger was so tired of running I started using tape to hold the mouse button down. One thing people do in a lot of FPS games when playing online is set them to "always run" mode. I wish we'd had done that with Ultima VIII.

Capturing the Mouse

I'm always surprised that Win32 documentation doesn't make inside jokes about capturing the mouse. At least we can still laugh at it. If you've never programmed a user interface before, you probably don't know what capturing the mouse means or why any programmer in his right mind would want to do this. Catching a mouse isn't probably something that's high on your list.

To see what you've been missing, go to a Windows machine right now and bring up a dialog box. Move the mouse over a button, hopefully not one that will erase your hard drive, and click the left mouse button and hold it down. You should see the button graphic depress. Move the mouse pointer away from the button and you'll notice the button graphic pop back up again. Until you release the left mouse button, you can move the mouse all you want, but only the button on the dialog will get the messages. If you don't believe me, open up Microsoft Spy++ and see for yourself. Microsoft Spy++ is a tool that you use to figure out what Windows messages are going to which window, and it's a great debugging tool if you are coding a GUI application. Here's a quick tutorial:

  1. In Visual Studio, select Spy++ from the Tools menu.

  2. Close the open default window, and select "Find Window" from the main menu or hit Ctrl-F.

  3. You'll then see a little dialog box that looks like the one shown in Figure 5.1.

    click to expand
    Figure 5.1: The Find Window with Spy++.

  4. Click and drag the little finder tool to the window or button you are interested in, and then click the "Messages" radio button at the bottom of the dialog. You'll get a new window in Spy++ that shows you every message sent to the object.

Perform the previous experiment again, but this time use Spy++ to monitor the Windows messages sent to the button. You'll find that as soon as you click on the button, every mouse action will be displayed even if the pointer is far away from the button in question. That might be interesting, but why is it important? If a user interface uses the boundaries of an object like a button to determine whether it should receive mouse events, capturing the mouse is critical. Imagine a scenario where you can't capture mouse events:

  1. The mouse button goes down over an active button.

  2. The button receives the event and draws itself in the down position.

  3. The mouse moves away from the button, outside its border.

  4. The button stops receiving any events from the mouse since the mouse isn't directly over the button.

  5. The mouse button is released.

The result is that the button will still be drawn in the down position, awaiting a button release event that will never happen. If the mouse events are captured, the button will continue to receive mouse events until the button is released.

To better understand this, take a look at a code snippet that shows some code you can use to capture the mouse and draw lines:

 LRESULT APIENTRY MainWndProc(HWND hwndMain, UINT uMsg, WPARAM wParam,                              LPARAM lParam) {    static POINTS ptsBegin;        // beginning point    switch (uMsg)    {       case WM_LBUTTONDOWN:           // Capture mouse input.           SetCapture(hwndMain);           ptsBegin = MAKEPOINTS(lParam);           return 0;       case WM_MOUSEMOVE:           // When moving the mouse, the user must hold down           // the left mouse button to draw lines.           if (wParam & MK_LBUTTON)           {               // imaginary code - you write this function              yourcode::ErasePreviousLine();               // Convert the current cursor coordinates to a               // POINTS structure, and then draw a new line.               ptsEnd = MAKEPOINTS(lParam);               // also imaginary               yourcode::DrawLine(ptsEnd.x, ptsEnd.y);           }           break;       case WM_LBUTTONUP:         // The user has finished drawing the line. Reset the         // previous line flag, release the mouse cursor, and         // release the mouse capture.         fPrevLine = FALSE;         ReleaseCapture();         break;    }    return 0; } 

If you were to write functions for erasing and drawing lines, you'd have a nice rubber band line drawing mechanism, which you can thank mouse capturing for. By using it, your lines would stop following the mouse if you ever left the window's client area.

Making a Mouse Drag Work

You might wonder why a mouse drag is so important. Drags are important because they are prerequisites to much user interface code in a lot of games. When you select a group of combatants in Command and Conquer, for example, you drag out a rectangle. When you play Freecell in Windows, you use the mouse to drag cards around. In Sims Online, you drag objects around to arrange the furniture in your house or give objects to other players. Most assuredly, your game will use drags too.

Dragging the mouse adds a little complexity to the process of capturing it. Most user interface code distinguishes a single click, double click, and drag as three separate actions, and therefore will call different game code. Dragging also relates to the notion of legality; it's not always possible that anything in your game can be dragged to anywhere. If a drag fails, you'll need a way to set things back to the way they were. This issue might seem moot when you consider that dragging usually affects the look of the game—the dragged object needs to appear like it is really moving around, and it shouldn't leave a copy of itself in it's original location. That might confuse the player big time.

The code to support dragging requires three phases:

  • Detect and initiate a drag event

  • Handle the mouse movement and draw objects accordingly

  • Detect the release and finalize the drag

The actions that define a drag are typically a mouse press (button down) followed by a mouse movement, but life in the mouse drag game is not always that simple. Also, during a double click event, a slight amount of mouse movement might occur, perhaps only a single pixel coordinate. Your code must interpret these different cases.

In Windows, a drag event is only allowed on objects that are already selected, which is why drags usually follow on the second "click and hold" of the mouse button. The first click of the left mouse button always selects objects. Many games differ from that standard, but it's one of the easier actions to code since only selected objects are draggable.

Back to the task at hand—detecting a drag event. Since a drag event involves multiple trips around the main loop, you must assume that every mouse button down event could be the beginning of a drag event. I guess an event is assumed draggable until proven innocent. In your mouse button down handler you need to look at the mouse coordinates and determine if they are over a draggable object. If the object is draggable, you must create a temporary reference to it that you can find a few game loops later. Since this is the first button down event, you can't tell if it's a bona fide drag event just yet.

The only thing that will make the drag event real is the movement of the mouse, but only movement outside of a tiny buffer zone. On an 800x600 screen, a good choice is five pixels in either the x or y coordinate. This is large enough to indicate that the drag was real, but small enough that small shakes in the mouse during a double click won't unintentionally initiate a drag. Here's the code that performs this dirty work:

 // Place this code at the top of your mouse movement handler if (m_aboutToDrag) {        CPoint offset = currentPoint - dragStartingPoint;    if (abs(offset.x) > DRAG_THRESHOLD || abs(offset.y) > DRAG_THRESHOLD)    {       // We have a real drag event!       bool dragOK = yourcode::InitiateDrag(draggedObject, dragStartingPoint);       SetCapture( GetWindow()->m_hWnd );       m_dragging = TRUE;    } } 

The call to yourcode::InitiateDrag is something you write yourself. Its job is to set the game state to remove the original object from the display and draw the dragged object in some obvious form, such as a transparent sprite. The call to SetCapture is the same Win32 function I showed you in the previous section.

Until the mouse button is released, the mouse movement handler will continue to get mouse movement commands, even those that are outside the client area of your window! Make sure your draw routines don't freak out when they see these odd coordinates.

What must go down, must finally come up again. When the mouse button is released, your drag is complete, but it might not be legal:

 // Place this code at the top of your mouse button up handler if ( m_dragging )  {    ReleaseCapture();    m_bDragging = false;    if (!yourcode::FinishDrag(point))    {       yourcode::AbortDrag(dragStartingPoint);    } } 

This bit of code would exist in your handler for a mouse button up event. The call to ReleaseCapture() makes sure mouse events get sent to all their normal places again. yourcode::FinishDrag() is a function you'd write yourself. It should detect if the destination of the drag was legal, and perform the right game state manipulations to make it so. If the drag is illegal, the object has to snap back to its previous location as if the drag never occurred. This function can be trickier to write than you'd think, since you can't necessarily use game state information to send the object back to where it came from.

A Tale from the Pixel Mines

In Ultima VII and Ultima VIII, we created a complicated system to keep track of object movement, specifically whether or not an object could legally move from one place to another. It was possible for a game designer to use the all powerful game editor to force objects into any location, whether it was legal or not. If these objects were dragged to another illegal location by the player, the object had to be forced back into place. Otherwise the object would exist in limbo. What we learned is that the drag code could access the game state at a low enough level to run the abort code.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net