DirectXTutorial.com
The Ultimate DirectX Tutorial
Lesson 1:  Using the Keyboard
Log In
Lesson Overview

Welcome to the DirectInput tutorial.  In this first lesson we will cover the basics of DirectInput, how it is similar to Direct3D and how it is different.  Then we will go into getting information from the keyboard and how that is done with DirectInput.

What is DirectInput?

Before we get into that, why DirectInput?  The GetAsyncKeyState() function works just fine!  Well, it may have worked in our simple demos so far, but it is very limited and depends on Windows to get information to you.  This can present all sorts of difficulties.

First of all, Windows does not send you data about actual keystrokes.  Instead, you get what the result of the keystroke should be in a typical application.  For instance, you can't tell which ENTER key was pressed, and you can't tell if the user is pressing numbers on the keypad or on the full keyboard.  DirectInput gives you raw data, meaning you get a signal when a specific button is down, regardless of that button's meaning.

Second, Windows filters most keystrokes with keyboard repeat delays.  If you want to override this, DirectInput is the simplest way.

Third, while the Windows mouse is fast enough, getting information about where the mouse is located can be slow, and can result in mouse lag.  If you have ever played a game with mouse lag, you'll know how bad that can be.  It usually is enough to make you stop playing for that reason alone.  DirectInput bypasses normal Windows functions and gets information from the mouse directly.  There's no input lag at all.

While all these things are possible with various techniques in Windows programming, DirectInput provides you with a simple way to access it all from one place.  It creates a separate thread to manage input while the rest of your program runs normally.  In a professional game, it sometimes would be preferable to use the Windows methods, as you have more direct control of how the input data is obtained, but for most games this is irrelevant and DirectInput will suffice.

The Basic DirectInput Program

Let's take a look at how to set up DirectInput.  We will do this by building a simple program to access a keyboard, and use the arrow keys to rotate a teapot.  For this we have five steps:

1. Define global variables and function prototypes
2.  Initialize DirectInput and its devices
3.  Get the keyboard information from DirectInput
4.  Check specific keys as needed
5.  Close DirectInput when done

Let's go over each part before we look at the whole program combined.


1.   Define global variables and function prototypes.

The following code is excerpt from the Direct3D Meshes tutorial's first program, but with the added DirectInput code in bold.

// include the necessary header files
#include <windows.h>
#include <windowsx.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <dinput.h>

// define the screen resolution and keyboard macros
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
// #define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
// #define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)

// include the DirectX Library files
#pragma comment (lib, "d3d9.lib")
#pragma comment (lib, "d3dx9.lib")
#pragma comment (lib, "dinput8.lib")
#pragma comment (lib, "dxguid.lib")


// global declarations
LPDIRECT3D9 d3d;
LPDIRECT3DDEVICE9 d3ddev;
LPD3DXMESH meshTeapot;
LPDIRECTINPUT8 din;    // the pointer to our DirectInput interface
LPDIRECTINPUTDEVICE8 dinkeyboard;    // the pointer to the keyboard device
BYTE keystate[256];    // the storage for the key-information


// function prototypes
void initD3D(HWND hWnd);
void render_frame(void);
void cleanD3D(void);
void init_graphics(void);
void init_light(void);
void initDInput(HINSTANCE hInstance, HWND hWnd);    // sets up and initializes DirectInput
void detect_input(void);    // gets the current input state
void cleanDInput(void);    // closes DirectInput and releases memory


// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

#include <dinput.h>

This includes the DirectInput header file.  This file contains various declarations to actual methods contained in the DirectInput library.

#pragma comment (lib, "dinput8.lib")
#pragma comment (lib, "dxguid.lib")

This includes the DirectInput 8 library files.  "dxguid.lib" is an additional library that can get the GUID for any device DirectInput used, and this will be needed in a bit.

Why the DirectInput 8 library instead of 9?  Well, the fact is, Microsoft did not update DirectInput when the new DirectX 9 was released.  There really was no need, as there were no real breakthroughs in keyboard and mouse technology.  Maybe a future DirectInput will include multi-touch input, but until then, we're just sticking with version 8.

LPDIRECTINPUT8 din;

This variable is a long pointer to DirectInput.  What this means is that we will create a class called iDirectInput8.  When COM makes this class, we will ignore it, and access it only indirectly using this pointer.  The class is created the same way Direct3D is created.

LPDIRECTINPUTDEVICE8 dinkeyboard;

This is a long pointer to the keyboard device.  This contains all the information having to do with the keyboard and how our program will relate to it.  Later, we will create additional devices, but we'll stick to the keyboard for now.

BYTE keystate[256];

This is an array of bytes which are used to store the state of the keyboard.  Each byte represents one key on the keyboard.  If a certain byte is equal to 1, the key is pressed, and if 0, the key is not pressed.  We'll look more into how this array is used in a bit.


2.  Initialize DirectInput and its devices

Before we use DirectInput, we have to initialize it and the devices it controls.  In this lesson, we'll initialize DirectInput and the keyboard device.  I didn't bold the new parts here, because the entire thing is new.

// this is the function that initializes DirectInput
void initDInput(HINSTANCE hInstance, HWND hWnd)
{
    // create the DirectInput interface
    DirectInput8Create(hInstance,    // the handle to the application
                       DIRECTINPUT_VERSION,    // the compatible version
                       IID_IDirectInput8,    // the DirectInput interface version
                       (void**)&din,    // the pointer to the interface
                       NULL);    // COM stuff, so we'll set it to NULL

    // create the keyboard device
    din->CreateDevice(GUID_SysKeyboard,    // the default keyboard ID being used
                      &dinkeyboard,    // the pointer to the device interface
                      NULL);    // COM stuff, so we'll set it to NULL

    // set the data format to keyboard format
    dinkeyboard->SetDataFormat(&c_dfDIKeyboard);

    // set the control you will have over the keyboard
    dinkeyboard->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
}

If the comments here are good enough for you, excellent.  Otherwise, I've described each of these commands below.

DirectInput8Create()

This function is probably bigger than it needs to be, but I think we'll be okay.  What this function does is create the DirectInput interface.  Let's take a look at the prototype, then define each of the parameters.

HRESULT WINAPI DirectInput8Create(HINSTANCE hinst,    // application handle
                                  DWORD dwVersion,    // DirectInput version
                                  REFIID riidltf,    // unicode or ANSI
                                  LPVOID *lplpDirectInput,    // pointer to DirectInput
                                  LPUNKNOWN punkOuter    // COM stuff, so we'll put NULL

HINSTANCE hinst,

This first one is simple.  We just plug in the handle to the application instance.  It is passed in the WinMain() parameters, so it is readily accessible.

DWORD dwVersion

This tells DirectInput what version it is.  If you studied the Direct3D tutorials, you'll recall that we had to tell Direct3D what version is was.  This is for compatibility reasons.  The version you are using may be different from the version your user is using.  This parameter helps DirectX overcome the confusion that creates.

The parameter to put here is DIRECTINPUT_VERSION.  It doesn't matter what the version is, just know that DirectX has taken care of it all for you.

REFIID riidltf,

There is more than one DirectInput interface.  There is one for ANSI and one for Unicode.  However, most programmers just use the default.  To get the default, simply put IID_IDirectInput8 into the parameter.

[Table 1.1 - riidltf Values]

LPVOID *lplpDirectInput,

The pointer 'din' is the pointer we are using for the DirectInput interface.  DirectInput 8 requires that we convert this to a void**.  We do this, and it gets filled with the address of the interface.  Just set it to (void**)&din.

LPUNKNOWN punkOuter

This parameter is COM related, so we won't get into it.  We'll just set it to NULL.

 

Now we have the next function to talk about.  Fortunately, it is significantly smaller and simpler.

CreateDevice()

This function only has three parameters, only two of which you will use.

What this function does is create an interface through which you can access a device.  In this case, we will use it to access the keyboard.  Let's look at the prototype:

HRESULT CreateDevice(REFGUID rguid,
                     LPDIRECTINPUTDEVICE8 *lplpDirectInputDevice,
                     LPUNKNOWN pUnkOuter);

REFGUID rguid,

Remember the GUIDs (Globally Unique Identifiers) I talked about before?  This is where we enter that in.  You do this by entering the flag GUID_SysKeyboard.  This flag contains the GUID for the current keyboard.  As there is only one keyboard, we don't have to worry about picking the right one.

LPDIRECTINPUTDEVICE8 *lplpDirectInputDevice,

This parameter is the address of the device.  Before, we named this dinkeyboard, so just put '&dinkeyboard' in this parameter.

LPUNKNOWN pUnkOuter

This parameter, just like the last pUnkOuter we saw, is COM related, and we'll set it to NULL.

Now let's cover the next function, which fortunately has only one parameter.

SetDataFormat()

Let's take a look at the prototype:

HRESULT SetDataFormat(LPCDIDATAFORMAT lpdf);

That one parameter can get rather complex, but we'll keep it simple.  It is a pointer to a struct that sets the format of a device.  We can get into this at great detail and I could spend weeks dreaming up a new tutorial covering data formats in all their detail.  Fortunately, we have some default, pre-set structs provided by DirectInput.  The one we want now is the format for a keyboard, so we'll load this with c_dfDIKeyboard.  This takes care of all the details.

SetCooperativeLevel()

This parameter is just a little more annoying than SetDataFormat().  It's first parameter is simple, but it's second one is more complex.  Here is the prototype:

HRESULT SetCooperativeLevel(HWND hwnd,
                            DWORD dwFlags);

HWND hwnd,

The handle to the window that will have control of the keyboard.  This was passed in earlier, so we'll use hWnd here.

DWORD dwFlags

This is the last little confusing part we have to talk about.  There are five flags we can place in this parameter:

[Table 1.2 - SetCooperativeLevel Values]

For these examples, we'll be using DISCL_FOREGROUND | DISCL_NONEXCLUSIVE, which is workable for most situations.

Now let's take one more look at the whole function that creates DirectInput and accesses the keyboard:

// this is the function that initializes DirectInput
void initDInput(HINSTANCE hInstance, HWND hWnd)
{
    // create the DirectInput interface
    DirectInput8Create(hInstance,    // the handle to the application
                       DIRECTINPUT_VERSION,    // the compatible version
                       IID_IDirectInput8,    // the DirectInput interface version
                       (void**)&din,    // the pointer to the interface
                       NULL);    // COM stuff, so we'll set it to NULL

    // create the keyboard device
    din->CreateDevice(GUID_SysKeyboard,    // the default keyboard ID being used
                      &dinkeyboard,    // the pointer to the device interface
                      NULL);    // COM stuff, so we'll set it to NULL

    // set the data format to keyboard format
    dinkeyboard->SetDataFormat(&c_dfDIKeyboard);

    // set the control we will have over the keyboard
    dinkeyboard->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
}

Read that code through a couple of times and you'll really 'get it'.


3.  Get the keyboard information from Windows

Although reading keys in DirectInput is not as simple as using the GetAsyncKeyState() function, we can easily set it up to be just as simple.  The following code is a function that gets keystroke information, then stores it in a 256-byte array.

// this is the function that gets the latest input data
void detect_input(void)
{
    // get access if we don't have it already
    dinkeyboard->Acquire();

    // get the input data
    dinkeyboard->GetDeviceState(256, (LPVOID)keystate);
}

Let's go over these commands briefly.

dinkeyboard->Acquire();

This is a simple function with no parameters which allows access the device (in this case, the keyboard).

The reason this is called every time we want to look at the keyboard, is that sometimes Windows will let other programs use the keyboard, and to do so, it must tell our program to "unaquire" the keyboard, or close further access until this function is called again.  To prevent this, we simply call the Aquire() function each time, and we'll get input whenever we're allowed to.

dinkeyboard->GetDeviceState(256, (LPVOID)keystate);

This function is actually quite simple.  All it does is get the keyboard information and stuff it into the memory location of our choice.  Let's look at the prototype:

HRESULT GetDeviceState(DWORD cbData,
                       LPVOID lpvData);

The first parameter is the size of the location we will place the keyboard information in.  For keyboard input, this is always 256 bytes.  Earlier, we created an array called 'keystate' for this exact purpose.

The second parameter is the location of the array.  We'll cast the 'keystate' array address into a void pointer to fill this parameter:  (LPVOID)keystate.


 4.  Check specific keys as needed

The last function got the keyboard information regarding the whole keyboard.  However, we only need specific keys, and we want an easy way to check which keys are pressed and which ones aren't.  Up until now, we've used KEY_DOWN() to do this.  In this step, we'll create a function that can be used the same way.

As explained before, each byte in the 'keystate' array is dedicated to the status of one key on the keyboard.  Therefore, if we want to know the status of a particular key, all we need to do is check the appropriate byte and the appropriate bit within that byte.

The code to do this can be represented in one line.  Where before we had:

if(KEY_DOWN(VK_LEFT))
    // do something...

we now have:

if(keystate[DIK_LEFT] & 0x80)
    // do something...

It's pretty obvious that this detects if the left arrow key has been pressed.  As you'll recall from GetAsyncKeyState(), you have to include the "& 0x80".  This is because only the high-order bit in the byte is relevant, and it is not guaranteed that the others will be set to 0.

DIK_LEFT is equal to 203.  The 203rd slot in the array is always the left arrow key.  Table 1.3 is a table of other key values that might be useful to you.

[Table 1.3 - Key Values]

In the example program for this lesson, we will be taking a teapot and rotating using the left and right arrow keys.  We place the above if() statements right where we previously put "index++".  This way, instead of index constantly increasing, it can be controlled by the keys.  See the finished program for the code.


5.  Close DirectInput when done

Well, we have one last part here and we're done.  Let's take a look at the code:

// this is the function that closes DirectInput
void cleanDInput(void)
{
    dinkeyboard->Unacquire();    // make sure the keyboard is unacquired
    din->Release();    // close DirectInput before exiting
}

Unacquire()

This function tells DirectInput to give up our access to the keyboard.  If we have exclusive control (which we will sometimes), this is vital, but it should still be called no matter what kind of control you have.

Release()

All this function does is close DirectInput.  Just as with Direct3D, DirectInput must be released, or it will continue to exist after the program has closed.

The Finished Program

Well, we did it!  We pieced together a program that will detect keys directly off the keyboard.  Now let's look at the whole program and see what happened.

This code is extended from Direct3D Meshes Lesson 1.  All the DirectInput code has been shown in bold.

What this code does is replace the automatic rotation from before and put it in control of the left and right mouse buttons.

[Show Code]

If you run this program, you should get something like this, only it will rotate with the arrow keys instead of rotating automatically.

Image 1.1 - DirectInput In Action

Image 1.1 - DirectInput In Action
Summary

In this lesson we've covered how to do basic DirectInput with the keyboard.  In the next lesson, we'll learn how to do the same thing with the mouse.  We'll also learn how to make a mouse pointer act like a standard Windows mouse pointer.

Before that, however, let's do some exercises and get to know the code we just learned.

1.  Make the teapot rotate on the z-axis using the up and down arrow keys.
2.  Change those keys from the arrow keys to the WASD keys.
3.  Experiment with the various cooperative level combinations and see what each one does.

Next Lesson:  Using the Mouse

GO! GO! GO!