Getting started with DirectInput keyboard programming

Posted by Sekka on Sun, 20 Feb 2022 15:29:07 +0100

Getting started with DirectInput keyboard programming

Game programming is not only the development of graphics program, but also includes many aspects. What this paper wants to talk about is how to use DirectInput to program the keyboard.

In the DOS era, we are generally used to taking over keyboard interrupts to add our own processing code. However, this way of life is not feasible in the evil Windows society. We can only live on the relief fund of API or DirectInput.

In the API of Windows, there is a function of GetAsyncKeyState(), which can return whether the current state of a specified key is pressed or released. This function can also return whether the specified key has been pressed since the GetAsyncKeyState() function was called last time. Although this function sounds good, there are fewer and fewer programmers receiving such benefits. No reason, just because the benefits of DirectInput are richer than this and seem more professional?

In order to become a professional welfare user as soon as possible, let's start by learning the keyboard programming of DirectInput.

Initialization of DIRECTINPUT

When talking about DirectDraw earlier, it was mentioned that Microsoft designs DirectX according to COM, so there is a DIRECTINPUT object to represent the input device, and a specific device is represented by the DIRECTINPUTDEVICE object.

The actual establishment process is to create a DIRECTINPUT object first, and then create a DIRECTINPUTDEVICE object through the CreateDevice method of this object.

Examples are as follows:

#include <dinput.h>

#define DINPUT_BUFFERSIZE 16

LPDIRECTINPUT lpDirectInput; // DirectInput object
LPDIRECTINPUTDEVICE lpKeyboard; // DirectInput device

BOOL InitDInput(HWND hWnd)
{
HRESULT hr;

// Create a DIRECTINPUT object
hr = DirectInputCreate(hInstanceCopy, DIRECTINPUT_VERSION, &lpDirectInput, NULL);

if FAILED(hr)
{
    // fail
    return FALSE;
}

// Create a DIRECTINPUTDEVICE interface
hr = lpDirectInput->CreateDevice(GUID_SysKeyboard, &lpKeyboard, NULL);
if FAILED(hr)
{
    // fail
    return FALSE;
}

// Set to return the query status value through a 256 byte array
hr = lpKeyboard->SetDataFormat(&c_dfDIKeyboard);
if FAILED(hr)
{
    // fail
    return FALSE;
}

// Set collaboration mode
hr = lpKeyboard->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
if FAILED(hr)
{
    // fail
    return FALSE;
}

// Set buffer size
// If not set, the default value of buffer size is 0, and the program can only work in immediate mode
// If you want to work in buffer mode, you must make the buffer size exceed 0
DIPROPDWORD     property;

property.diph.dwSize = sizeof(DIPROPDWORD);
property.diph.dwHeaderSize = sizeof(DIPROPHEADER);
property.diph.dwObj = 0;
property.diph.dwHow = DIPH_DEVICE;
property.dwData = DINPUT_BUFFERSIZE;

hr = lpKeyboard->SetProperty(DIPROP_BUFFERSIZE, &property.diph);

if FAILED(hr)
{
    // fail
    return FALSE;
}


hr = lpKeyboard->Acquire();
if FAILED(hr)
{
    // fail
    return FALSE;
}

return TRUE;

}
In this code, we first define lpDirectInput and lpKeyboard pointers. The former is used to point to a DIRECTINPUT object and the latter points to a DIRECTINPUT device interface.

Through DirectInputCreate(), we created a DIRECTINPUT object for lpDirectInput. Then we call CreateDevice to create a DIRECTINPUTDEVICE interface. Parameter GUID_SysKeyboard indicates that the keyboard object is created.

Next, SetDataFormat sets the data format, SetCooperativeLevel sets the collaboration mode, and SetProperty sets the buffer mode. Because there are many parameters of these functions and methods, I won't explain their functions in detail one by one. Please directly check the help information of DirectX, which is very clear.

After completing these tasks, we call the Acquire method of the DIRECTINPUTDEVICE object to activate the access to the device. It should be noted here that any DIRECTINPUT device cannot be accessed without Acquire. In addition, when the system switches to another process, you must use the Unacquire method to release the access permission. When the system switches back to this process, you can call Acquire to regain the access permission.

Therefore, we usually do the following in WindowProc:

long FAR PASCAL WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_ACTIVATEAPP:
if(bActive)
{
if(lpKeyboard) lpKeyboard->Acquire();
}
else
{
if(lpKeyboard) lpKeyboard->Unacquire();
}
break;
...
}

Oh, by the way, the immediate mode and buffer mode are also mentioned in the previous routine. In DirectINPUT, these two working modes are different.

If the immediate mode is used, when querying data, only the equipment status at the time of query can be returned. The buffer mode will record the state change process of all devices. As far as personal preference is concerned, the author prefers the latter, because in this way, generally no key information will be lost. Accordingly, if the frequency of query is too low when using the former, it is difficult to ensure the integrity of the collected data.

Data query of DIRECTINPUT

The data query of immediate mode is relatively simple. Please see the following example:

BYTE diks[256]; // DirectInput keyboard state buffer

HRESULT UpdateInputState(void)
{
If (lpKeyboard! = null) / / if the lpKeyboard object interface exists
{
HRESULT hr;

    hr = DIERR_INPUTLOST;   // Prepare for cycle detection

    // if input is lost then acquire and keep trying
    while(hr == DIERR_INPUTLOST)
    {
        // Read input device status value to status data buffer
        hr = lpKeyboard->GetDeviceState(sizeof(diks), &diks);

        if(hr == DIERR_INPUTLOST)
        {
            // DirectInput reports that the input stream is interrupted
            // You must call the Acquire method again and try again
            hr = lpKeyboard->Acquire();
            if(FAILED(hr))
                return hr;
        }
    }

    if(FAILED(hr))
        return hr;
}

return S_OK;

}

In the above example, the key is to use the GetDeviceState method to read the input device state value and deal with exceptions. By using the GetDeviceState method, we put the state value of the input device in a 256 byte array. If the highest bit of an array element in the array is 1, it indicates that the key of the corresponding encoding is being pressed at this time. For example, if diks [1] &0x80 > 0, it means that the ESC key is being pressed.

After learning the data query of immediate mode, let's start to study the buffer mode:

HRESULT UpdateInputState(void)
{
DWORD i;

if(lpKeyboard != NULL)
{
    DIDEVICEOBJECTDATA  didod[DINPUT_BUFFERSIZE];  // Receives buffered data
    DWORD               dwElements;
    HRESULT             hr;

    hr = DIERR_INPUTLOST;

    while(hr != DI_OK)
    {
        dwElements = DINPUT_BUFFERSIZE;
        hr = lpKeyboard->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), didod, &dwElements, 0);
        if (hr != DI_OK)
        {
            // An error has occurred
            // This error may be di_ Buffer overflow error
            // But no matter what kind of error, it means that the contact with the input device has been lost

            // The most serious consequence of this error is if you press a key and haven't released it yet
            // If an error occurs, the message of releasing the key later will be lost. In this way, your program
            // You may think that the key has not been released, resulting in some unexpected situations

            // This code does not handle the error at this time

            // One way to solve this problem is to call it once when this error occurs
            // GetDeviceState(), and then compare the result with the last recorded state of the program
            // Compare to correct possible errors

            hr = lpKeyboard->Acquire();
            if(FAILED(hr))
            return hr;
        }
    }

    if(FAILED(hr))
        return hr;
}

// GetDeviceData() is different from GetDeviceState(). After calling it,
// dwElements will indicate how many buffer records were read in this call

// We use another loop to process each record

for(int i=0; i<dwElements; i++)
{
    // Put processing code here
    // didod[i].dwOfs means that key is pressed or released
    // didod[i].dwData records the status of this key. The highest bit of the low byte is 1, which means pressed and 0 means released
    // Generally use didod [i] Dwdata & 0x80 to test
}
return S_OK;

}

In fact, each record also has dwTimeStamp and dwSequence fields to record the time and sequence number of the message for more complex processing. This article is written for beginners, so I don't intend to talk about these contents.

End processing of DIRECTINPUT

When using DIRECTINPUT, we should also pay attention to one thing: when the program ends, it must be released. The demonstration code is as follows:

void ReleaseDInput(void)
{
if (lpDirectInput)
{
if(lpKeyboard)
{
// Always unacquire the device before calling Release().
lpKeyboard->Unacquire();
lpKeyboard->Release();
lpKeyboard = NULL;
}
lpDirectInput->Release();
lpDirectInput = NULL;
}
}

This code is very simple, which is to call the Release method on each object of DIRECTINPUT to Release resources. This process is basically the same as when using other parts of DIRECTX.

Topics: C++