Author : Yordan Gyurchev
Page : 1 Next >>
What is the first thing the customer does after he has installed a brand new game? He changes the controls to the way he likes, of course. You will be totally amazed with the keys some people use. I myself have seen quite strange faces, gazing at gamer’s fingers forming incredible configurations. The most terrible thing a game developer can do (as far as input goes) is to fix the keys (controls) and force the player to use them.
So, what we need is a system that can report the controls’ state while keeping the opportunity for customization and, of course, remember this customization.
Recently I had to program such a system to deal with the controls for a 3D game, and I tried to determine what tasks it should perform. Take a look at my list:
1.Internal work with input devices (keyboard and mouse)
2.Controls implementation, system controls
3.Customization of the controls
4.Retrieval of the current control state
5.Controls persistence
Store controls configuration
Retrieve controls configuration
6.Misc
We can access the user input by tracking the application message queue but sometimes (almost every time) we need firsthand information for the devices we use.
The DirectInput subdivision of DirectX can acquire exclusive control of common input devices such as the keyboard, mouse and joystick or wheel (if present). The programmer instantiates (creates) a DirectInput object and acquires pointers to the desired interfaces. Lately these interfaces are used to acquire exclusive input from the devices and retrieve the device status (mouse dragged, keys pressed, etc). The DirectX documentation completely covers these topics therefore I won’t go in to the basics. You can take a look at the samples in the DirectX SDK as well.
However, a few words are necessary. DirectX is based on COM (Component Object Model). When you create DirectX objects you instantiate COM objects and obtain interfaces to them. Once acquired these interfaces are used to invoke methods that provide us with more direct access to the hardware devices, which is quite important for a game. Once used and no longer needed these interfaces must be released properly to prevent memory leaks and ensure proper execution.
We can encapsulate DirectInput objects (pointers) into our class and when it falls out of scope automatically release the interfaces in the destructor. The constructor however is left empty, there are only some pointer assignments to NULL in order to keep pointers clean and guarantee accurate clean up. This has to be done (the empty constructor) because in order to construct the DirectInput objects and interfaces we need handles of the application instance and main window (it is possible that at the moment of the construction the main window of the application has not been created). Therefore I added an additional Create() method which has two handle parameters and takes care of the DirectInput object creation and obtains mouse and keyboard device interfaces as shown in Listing 1.
Listing 1: DirectInput object creation
HRESULT wj_controls::Create(HINSTANCE hInst, HWND hWnd)
{
hInstance=hInst;
hWindow=hWnd;
HRESULT hr;
// Register with the DirectInput subsystem and get a pointer
// to a IDirectInput interface we can use.
hr = DirectInputCreate( hInst, DIRECTINPUT_VERSION, &m_pDI, NULL );
if ( FAILED(hr) )
return hr;
hr=InitMouse();
if ( FAILED(hr) )
return hr;
hr=InitKeyboard();
if ( FAILED(hr) )
return hr;
AddSysKeys();
return S_OK;
}
The DirectInputCreate() function is provided by the DirectX library and creates a DirectInput object that supports the IDirectInput COM interface for us. Of course we can do it the harder way by directly calling CoCreateInstance and then obtaining and interface pointer to the object. But there is not much sense in doing so unless you want to do some special work such as aggregation or something else. InitMouse() and InitKeyboard() are almost identical – they only use different unique interface identifiers and set different data formats for the obtained device. All these interfaces must be stored in data members of our class. Having all initialization done there is one more thing you should do. Set some properties. These could be four:
Axis mode (for the mouse, determines whether it reports its position absolutely or relatively)
Range of values reported for an axis
Granularity (smallest increment reported for an axis value)
Size of the buffer used for buffer input (keyboard)
It’s up to you to choose what method to use - relational or absolute mouse coordinates - but if you are building a 3D shooter there is much more sense in using relational ones and if it is a real time strategy game, absolute ones are perfect. Be sure to set properties at the initial setup because you cannot do that for a device that has been acquired. (Note: of course you can unacquire it and set the properties)
We should keep an eye on the application message queue and when we detect application deactivation / activation, properly "unacquire" / "acquire" input devices - otherwise we may obtain undesirable results. State retrieval also must be synchronized with the "active" flag of the class because querying the status of an unacquired device can result in strange device hang ups (or at least I get them). So we add a MsgProg() method to our class and call it from the main message routine.
It may look like this:
Listing 2: Message proc code
switch (uMsg) {
case WM_ACTIVATE: //sent when window changes active state
if ( WA_INACTIVE == wParam )
{
m_bactive = FALSE;
}
else
{
m_bactive = TRUE;
}
SetAcquire(); // Set exclusive mode access according to m_bactive member
break;
}
According to the DirectInput Keyboard Device data format we have a keyboard buffer (an array of 256 (bytes) keys), and relative (or absolute) coordinates for the mouse and mouse buttons buffer (4 bytes). We need to declare some data members in the class to store this device information. Of course we start with three long integers for the mouse axis ("there are only two of them" - someone will note - the third one, the z-axis, is the wheel rotation if any wheel is present). Next come the mouse buttons and the keyboard buffers. In fact I have implemented a little trick while I was dealing with the retrieval of data from device objects, namely to place the mouse buttons buffer at the end of the keyboard's array of bytes making it 260 bytes long (I use the default buffer size of 256 for the keyboard buffer – data format c_dfDIKeyboard - and a 4 byte buffer for mouse buttons – data format c_dfDIMouse). Thus I produced a common buffer for all game keys the player can use. No matter what the player does (pushes keyboard keys or clicks mouse buttons) I get the information from the same place.
Next I have implemented an array of strings to name the keys – the player wants to see how the controls are configured and it is better to visualize "Space" than just the code (57). And once again for the code of 256 we have a name "Left Mouse Button".
Controls are much more than just keys. A single control (for example forward) can be represented by more than one keyboard key or even by a combination of keys. There has to be a small number of controls that cannot be changed (customized) which I call system controls that allow a game to maintain its behavior and of course change other controls.
Here is my structure for the control:
Listing 3:
typedef struct {
WORD id;
BYTE flags;
BYTE key[4];
BYTE state;
BYTE oldstate;
BYTE *external;
} control;
We need an ID to identify the control among other controls. Flags – to assign some properties such as system control, exclusive keys (some controls require to have unique keys and others don’t - imagine that you have the same key for forward and backward) and logical operators for the keys (if you have alternate keys you have an OR operator but if you have a key combination you have an AND operator). The key array represents the keys for the control; of course I limited their number to 4 but you can extend it to however many you wish - although I’ve never seen somebody playing with six keys for the same action.
The state and the oldstate contain the state of the control according to the specified keys.
The following code (Listing 4) shows how the state is calculated:
Listing 4: Updating single control state
if (control.flags&WJ_CTRLS_OPAND2)
control.state = keys[control.key[0]] &
keys[control.key[1]];
else
control.state = keys[control.key[0]] |
Page : 1 Next >>