This page has the following sections:
- DirectInput Introduction
- Using the Keyboard Device
- DirectInput Mouse
- Further Reading
DirectInput is an API that comes with the DirectX SDK for interfacing with input devices (mouse, keyboard, joystick, etc.). Additionally it handles force-feedback (input/output) devices.
DirectInput has benefits over normal Win32 input events:
- It enables an application to retrieve data from input devices even when the application is in the background
- It provides full support for any type of input device, as well as for force feedback
- Through action mapping, applications can retrieve input data without needing to know what kind of device is being used to generate it.
Note: DirectInput has not changed since version 8, so the objects and functions are appended with an 8 rather than a 9.
DirectInput, like all the other DirectX APIs, uses the COM design. The programmer declares object pointers and then calls a function in order to instantiate an instance of the object. Each object provides a set of methods (its interface) that can be used for DirectInput tasks. There are three types of COM object that provide the interfaces for using DirectInput:
- Type is IDirectInput8
- The root DirectInput interface.
- Provides interfaces to create the other objects:
- Type is IDirectInputDevice8
- Object representing a keyboard, mouse, joystick, or other input device.
- One is needed per device.
Note: there is a third object called an effect object, this is required for force feedback devices. This object is beyond the scope of these notes, look in the DirectX help file if you require more information.
DirectInput is a new library so we need to link with its library file and include its header:
Library File: dinput8.lib (DirectInput has not been changed since DirectX 8.0 hence the 8)
Additional helper library file: dxguid.lib
Header File: dinput.h
Note: there is a bit of a gotcha here because DirectInput carries out a check in its header to make sure you are using the correct version. So you have to define the version you wish to use before including the header file - otherwise you will get warnings about the version. E.g. to set it to use the current version (8) you would do this:
#define DIRECTINPUT_VERSION 0x0800
It is likely you will create a new class for handling all DirectInput data and interfaces so you will need to declare pointers to the two objects as member variables, these notes assume you have the following declared:
// The main DirectInput object pointer
// A DirectInput device object for the keyboard. If you want to handle other devices you need one of these for each.
To setup DirectInput you will need to create the main DirectInput object and use its interface functions to create a device object to represent the keyboard.
HRESULT hr = DirectInput8Create(inst, DIRECTINPUT_VERSION,
IID_IDirectInput8, (void**)&m_diObject, NULL);
This function requires the instance of your application (inst). This is passed to your application in the WinMain.. You could pass it into your initialise function as a parameter. The second parameter is the version, always use the DIRECTINPUT_VERSION define, the third determines the type of object you want to create, it is an identifier used by COM and here we set it to IID_IDirectInput8. This is the reason we need to link with the dxguid.lib library. A GUID is a globally unique identifier used by the COM design model. The fourth parameter is your DirectInput object pointer. If successful the call will instantiate an object and set your pointer to point at it hence you need to pass the address of a pointer. The last parameter is for advanced COM usage and is normally NULL.
Now that we have instantiated our main DirectInput object we need to create a device object to represent our keyboard.
hr = m_diObject->CreateDevice(GUID_SysKeyboard, &m_diKeyboardDevice, NULL);
The above function takes a GUID that tells it what type of device we want to create. In this case we want to create a device object to handle keyboard input so we pass GUID_SysKeyboard. If we wanted to create a mouse device we would pass in GUID_SysMouse The second parameter is the address of our device pointer. If the call is successful it will be set to point to a newly instantiated device object. The third parameter is for advanced COM usage and is normally NULL.
The next step is to set the keyboard device data format. It is possible to define your own format but normally you will use c_ddDIKeyboard for the keyboard and c_dfDIMouse for the mouse. Note that this is set once and then cannot be changed.
hr=m_diKeyboardDevice->SetDataFormat( &c_dfDIKeyboard );
Next we need to set the keyboard behaviour. This determines how we get on (co-operate) with other applications using the system. There are two types of flag we can set, one determines if we want background or foreground access to the device. If we only want to use the device when our window is active we only require foreground access. The second type determines how we hold on to the device, if we want exclusive access we prevent all other applications from using the keyboard. When running in a window we will want to only read key input when the our application is in the foreground and we do not want to prevent other applications using the keyboard so we want non-exclusive foreground behaviour:
hr = m_diKeyboardDevice->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
As well as the flags this function requires your application window handle.
DirectInput is now set up and you have created a device to represent the keyboard. The next thing to look at is how to use the keyboard device itself during your game loop.
Using the Keyboard Device
In order to use the keyboard device we need to acquire it. We need to do this before we can retrieve data from it. This is simple:
Note: we do not need to Acquire each time we read the keys only once at the start and whenever the keyboard device has been lost (see important note below).
The simplest way to get data from the keyboard device is to use the GetDeviceState method. This fills a buffer with a snapshot of the keyboard at that moment in time. This function takes a pointer to a buffer and how many buffer entries there are. For a keyboard this is always 256. So to retrieve the data we need to declare a 256 array of chars then call the GetDeviceState method to fill it:
// Get the input's device state, and put the state in keys - zero first
ZeroMemory(keys, sizeof(keys) );
m_diKeyboardDevice->GetDeviceState( sizeof(keys), keys );
Our 256 array is now filled with the state of each key on the keyboard. If a key is pressed then and-ing it with 0x80 will return true. If it is 0 then it is not pressed. e.g. to see if the key at index 7 is pressed:
if (keys & 0x80)
// key 7 is pressed
Each of the array elements represents one key on the keyboard. Rather than try to remember all the numbers we can use some DirectInput defines. These are very similar to the virtual key codes we used previously in Win32. The main difference is that they begin with DIK_ instead of VK_ e.g. to determine if the right arrow is pressed down:
if (keys[DIK_RIGHT] & 0x80)
// right arrow is pressed
To determine if the x key is pressed:
if (keys[DIK_X] & 0x80)
// x key is pressed
These defines are held in the direct input header. A full list can be viewed here: MSDN Link
Note: the above method takes a snapshot of the keyboard at any one time. There is another way of retrieving data and that is to use buffered mode. In buffered mode the key data is held in a buffer ready for when you want to retrieve it. To use this method you would need to use the GetDeviceData function.
Note: after responding to a key press remember to zero the keys array.
Since we have set our keyboard device to be none exclusive if the user switches away from our application (ALT-TAB) or minimises it we will lose access to the device and therefore have to reacquire it. We can detect when this happens by checking the return code of the GetDeviceState, if it fails (returning a DIERR_INPUTLOST error code) we loop until we can acquire the keyboard again. Note also that we may fail for another reason, if so then you will need to exit the function and try again later.
HRESULT hr=m_diKeyboardDevice->GetDeviceState( sizeof(keys), keys );
// If input is lost then acquire and keep trying until we get it back
while( hr == DIERR_INPUTLOST )
hr = m_diKeyboardDevice->Acquire();
// Could be we failed for some other reason
// Now read the state again
m_diKeyboardDevice->GetDeviceState( sizeof(keys), keys );
Localisation and special needs issues
I would recommend using DirectInput for reading control key presses and mouse but not for entering text. Keyboards are mapped differently dependant on the language used. Also input can be from speech to text converters etc. To take this into account we would need to use the Win32 built in functions like GetKeyboardLayout, MapVirtualKeyEx and ToAsciiEx. However from experience I have found them tricky to use and not always good at handling all language issues. So for basic text input I would suggest using the Win32 WM_CHAR message instead - this handles all localisation and special needs issues (plus user defined key repeat settings etc.). It may be slower than direct input but when you are entering text you do not type fast anyway. So I would advise using WM_CHAR for text entry and direct input for in game key and controller input.
Remember to release the DirectInput objects before your application closes. Also you have to do one more thing and that is to unacquire the device before releasing it. Your clean up code for a keyboard device may look like this:
Creating a mouse device is carried out in exactly the same way as for the keyboard apart from the fact that we pass GUID_SysMouse into the create device function. We then need to set the data format and cooperation modes e.g
m_diObject->CreateDevice(GUID_SysMouse, &m_diMouseDevice, NULL);
m_diMouseDevice->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
As with the keyboard device we can either read immediate data from the device (a snap shot of its state at a particular time) or use buffering. Reading immediate data is the default and so if you want to use buffering you need to first fill in a DIPROPDWORD structure with the size of buffer to use and then call the device's SetProperty method with the DIPROP_BUFFERSIZE flag.
To read the state of the mouse we use the same function as before but this time pass it a mouse structure to fill:
ZeroMemory( &m_mouseState, sizeof(m_mouseState) );
HRESULT hr = m_diMouseDevice->GetDeviceState( sizeof(DIMOUSESTATE2), &m_mouseState );
As with the keyboard a failed return code here could be due to your application being minimised and hence you should trap the case where hr is DIERR_INPUTLOST and go into a loop where you continuously try to re-acquire the mouse device.
Note I use c_dfDIMouse2 and DIMOUSESTATE2 as this provides a structure with support for up to 8 buttons rather than the 4 used by the earlier versions.
The DIMOUSESTATE2 structure contains all the data read from the mouse:
- LONG lX; LONG lY; LONG lZ - the x,y and z change in position of the mouse in Mickeys (z is normally the wheel).
- BYTE rgbButtons - the state of each button. When pressed the high order bit is set so you need to and (&) with 0x80 to determine if it is pressed.
A Mickey is the smallest measurable movement of a device.
Relative and Absolute positions
It is important to note that the x,y and z values by default are changes in position (relative) of the mouse and therefore do not relate to an actual screen position. It is possible to set the mouse to return accumulated (absolute) values using the SetProperty method however this simply sums the changes since the mouse was first acquired, something you could easily do yourself.
The thing is DirectInput is not concerned with the cursor (mouse pointer) at all it is just relative values. Relative values are fine for first person control of a camera but how, in a game, could you determine clicks on buttons etc? You could not use DirectInput at all for this purpose and instead use GetCursorPos or trap WM_MOUSEMOVE messages but if you want to use DirectInput you will need to find the position of the cursor when you first acquire the DirectInput mouse (using GetCursorPos) and from then on maintain a screen co-ordinate by summing the changes returned by GetDeviceState.
There is more to DirectInput than I have been able to describe here. There is the action mapping system that allows controls to be mapped to actions and also force feedback capabilities.