My brain makes things explode.
C++/D3D Pong Tutorial 04: Adding Keyboard Input
Tutorial Description
In the last tutorial, we established a rendering engine and got scene objects working, with basic physics. In this tutorial, we are going to implement the initial Input manager, which will be focusing on the keyboard. I was planning to include mouse input, but it turns out that DirectInput only handles the mouse in relative positioning – which is perfect for three-dimensional first-person shooter games that need to track the movement of the mouse – but it’s poor for two-dimensional casual games. For those of you that do wish to use mouse input, I will be covering it a later tutorial – the methods of getting absolute positioning will require a bit of modification to our engine.
Downloads
Please note that I’ve switched to using 7-zip in place of WinRAR for compression
Binary Only (20.4KB)
Full VS2010 Project Dir (26.8KB)
SVN Repository
As of this tutorial, I have created and added a Subversion repository on Assembla to store the code for the tutorial series. I will continue to upload the source for each tutorial – the SVN will merely contain the code as I write it, so you can feel free to play around with the code that I’m working on before I release a tutorial. Mostly, this is to help people play around with things if there’s a point where I’m slow to create the tutorial following the code (as was the case with this tutorial).
SVN URL: http://svn2.assembla.com/svn/d3dpong
Starting Words
Notice: This tutorial continues from where “C++/D3D Pong Tutorial 03” article left off. Please read it if you have not already.
Libraries
You will need to add a single library to the project to build with the code in this tutorial:
dinput.lib
Code Listing 1: CInputManager.h
#pragma once
// Global Header Files
#include <Windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <dinput.h>
// Local Header Files
#include "CGameEngine.h"
#include "CKeyboardState.h"
namespace D3DPong
{
class CInputManager
{
private:
/** Constructor & Destructor **/
CInputManager(void); // ctor
CInputManager(CInputManager const&) { } // copy ctor
~CInputManager(void); // dtor
static CInputManager* _instance;
// DirectInput Devices
LPDIRECTINPUT8 _device; // Core DirectInput Object
LPDIRECTINPUTDEVICE8 _keyboard; // Keyboard
// Keyboard-related Variables
CKeyboardState* _preKeyboardState;
CKeyboardState* _curKeyboardState;
// Device Initialization
bool initKeyboard(void);
public:
/** Singleton **/
static CInputManager* Instance(void) { return CInputManager::_instance; }
/** Public Functions **/
bool Init(void);
void Update(void);
/** Keyboard Functions **/
bool IsKeyDown(int);
bool IsKeyUp(int);
bool WasKeyPressed(int);
bool WasKeyReleased(int);
/** Accessors & Mutators **/
LPDIRECTINPUT8 GetDevice(void) { return this->_device; }
CKeyboardState* GetCurKeyboardState(void) { return this->_curKeyboardState; }
CKeyboardState* GetPreKeyboardState(void) { return this->_preKeyboardState; }
};
};
Here is the framework for our Input Manager. Again, we’re back down to lower source sizes – because we’re not trying to implement the world here. As we did with our Game Engine class, we’ve turned our Input Manager into a singleton – we only want one of them to be created throughout the application.
// DirectInput Devices LPDIRECTINPUT8 _device; // Core DirectInput Object LPDIRECTINPUTDEVICE8 _keyboard; // Keyboard // Keyboard-related Variables CKeyboardState* _preKeyboardState; CKeyboardState* _curKeyboardState; // Device Initialization bool initKeyboard(void);
The first two variables are input devices – the first, as with our Direct3D object, we have an object that we will use to access the DirectInput system. The second is the actual object that we will use to access the keyboard device – when adding a Mouse or a Gamepad/Joystick, you will add another LPDIRECTINPUTDEVICE8. If you’ve noticed the 8, you’ll notice that this is something from DirectX 8 – DirectInput hasn’t changed since then. In all honesty, the use of DirectInput isn’t recommended by Microsoft, but it will suffice for our purposes. We then add two variables to store the keyboard state – the current keyboard state, which will contain the state as of the current frame, and the previous, which will (if you haven’t guessed) store the state of the last frame. We do this so that we can not only check whether a key is currently being pressed, but also whether it was pressed or released between the current and last frame. This makes things easier on us, in the long run, because we can wait for (for example) the escape key to be released before we end the game, and it will make managing our game menus much easier when we want to handle selection of menu options game console style. Finally, we define a private function that will initialize our keyboard. It will help keep our Init() function tidy for the future addition of controllers, mice, and other input devices.
/** Public Functions **/ bool Init(void); void Update(void);
These two functions are pretty obvious in what they do. Init() will initialize all of our input devices, and update will update the current/previous states of our devices. I’ll explain them more when we actually flesh them out.
/** Keyboard Functions **/ bool IsKeyDown(int); bool IsKeyUp(int); bool WasKeyPressed(int); bool WasKeyReleased(int);
Last on the list of things to look at is our key-checking functions. The first two will return whether the key is currently pressed or released, which can be useful for continuous motion checking, such as what we will be doing with our pong paddles. The latter two check if they key’s state has changed between this frame and the last (if the key has been pressed or released).
Code Listing 2: CInputManager.cpp
#include "CInputManager.h"
#include "CKeyboardState.h"
namespace D3DPong
{
CInputManager* CInputManager::_instance = new CInputManager();
CInputManager::CInputManager(void)
: _device(NULL), _keyboard(NULL),
_curKeyboardState(NULL), _preKeyboardState(NULL)
{
}
CInputManager::~CInputManager(void)
{
if(this->_keyboard != NULL)
{
this->_keyboard->Unacquire();
this->_keyboard->Release();
this->_keyboard = NULL;
}
if(this->_device != NULL)
{
this->_device->Release();
this->_device = NULL;
}
}
bool CInputManager::Init(void)
{
// Initialize the DirectInput Device
if(FAILED(DirectInput8Create(GetModuleHandle(NULL), // Instance Handle
DIRECTINPUT_VERSION, // DirectInput Version
IID_IDirectInput8, // Device Type
(LPVOID*)&this->_device, // Pointer to device storage
NULL)))
{
MessageBox(NULL, L"Error initializing Input Manager", L"Error", MB_OK);
return false;
}
// Initialize the Keyboard
if(!this->initKeyboard())
return false;
// Call update once to populate the input states
this->Update();
return true;
}
bool CInputManager::initKeyboard(void)
{
if(FAILED(this->_device->CreateDevice(GUID_SysKeyboard,
&this->_keyboard,
NULL)))
{
MessageBox(NULL, L"Error initializing Keyboard", L"Error", MB_OK);
return false;
}
if(FAILED(this->_keyboard->SetDataFormat(&c_dfDIKeyboard)))
{
MessageBox(NULL, L"Error setting keyboard data format", L"Error", MB_OK);
return false;
}
if(FAILED(this->_keyboard->SetCooperativeLevel(CGameEngine::Instance()->GetWindowHandle(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
{
MessageBox(NULL, L"Error setting keyboard cooperative level", L"Error", MB_OK);
return false;
}
if(FAILED(this->_keyboard->Acquire()))
{
MessageBox(NULL, L"Error acquiring keyboard device", L"Error", MB_OK);
return false;
}
// Create the keyboard states
this->_curKeyboardState = new CKeyboardState();
this->_preKeyboardState = new CKeyboardState();
return true;
}
void CInputManager::Update(void)
{
// Update the keyboard
this->_preKeyboardState->CopyKeyBuffer(this->_curKeyboardState->GetKeyStates());
// Make sure we reacquire the keyboard if the device was lost
if(this->_keyboard->GetDeviceState(this->_curKeyboardState->GetBufferSize(), this->_curKeyboardState->GetKeyBuffer()) == DIERR_INPUTLOST)
this->_keyboard->Acquire();
}
bool CInputManager::IsKeyDown(int keyCode)
{
return this->_curKeyboardState->IsKeyDown(keyCode);
}
bool CInputManager::IsKeyUp(int keyCode)
{
return this->_curKeyboardState->IsKeyUp(keyCode);
}
bool CInputManager::WasKeyPressed(int keyCode)
{
if(this->_curKeyboardState->IsKeyDown(keyCode) && this->_preKeyboardState->IsKeyUp(keyCode))
return true;
else
return false;
}
bool CInputManager::WasKeyReleased(int keyCode)
{
if(this->_curKeyboardState->IsKeyUp(keyCode) && this->_preKeyboardState->IsKeyDown(keyCode))
return true;
else
return false;
}
};
Here are the guts of our system. Not everything will need covered, because it will be pretty obvious based on other parts, but here goes:
bool CInputManager::Init(void)
{
// Initialize the DirectInput Device
if(FAILED(DirectInput8Create(GetModuleHandle(NULL), // Instance Handle
DIRECTINPUT_VERSION, // DirectInput Version
IID_IDirectInput8, // Device Type
(LPVOID*)&this->_device, // Pointer to device storage
NULL)))
{
MessageBox(NULL, L"Error initializing Input Manager", L"Error", MB_OK);
return false;
}
// Initialize the Keyboard
if(!this->initKeyboard())
return false;
// Call update once to populate the input states
this->Update();
return true;
}
This method initializes our input management system. The first thing that we must do is initialize our DirectInput device, because it will be used to create any DirectInput-based input systems later (keyboard, mouse, and joypad, for example). We then call initKeyboard() to initialize our keyboard (when adding a Mouse or Joystick, you would also add an initMouse/initJoystick function, just to keep things clean), make sure it worked properly, then capture the current state of the input system.
bool CInputManager::initKeyboard(void)
{
if(FAILED(this->_device->CreateDevice(GUID_SysKeyboard,
&this->_keyboard,
NULL)))
{
MessageBox(NULL, L"Error initializing Keyboard", L"Error", MB_OK);
return false;
}
if(FAILED(this->_keyboard->SetDataFormat(&c_dfDIKeyboard)))
{
MessageBox(NULL, L"Error setting keyboard data format", L"Error", MB_OK);
return false;
}
if(FAILED(this->_keyboard->SetCooperativeLevel(CGameEngine::Instance()->GetWindowHandle(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
{
MessageBox(NULL, L"Error setting keyboard cooperative level", L"Error", MB_OK);
return false;
}
if(FAILED(this->_keyboard->Acquire()))
{
MessageBox(NULL, L"Error acquiring keyboard device", L"Error", MB_OK);
return false;
}
// Create the keyboard states
this->_curKeyboardState = new CKeyboardState();
this->_preKeyboardState = new CKeyboardState();
return true;
}
Not much to see here, in spite of the length – most of it’s error checking. First, we use the DirectInput device to create our Keyboard device, then we follow up by setting the data format and cooperative level. The cooperative level sets how our application works with the operating system and other applications running, when it comes to handling input on the device. Setting a Foreground cooperative level makes it so that our game only responds to input when the game window is our active window. This way, if the player were to open Notepad and press the escape key, it wouldn’t close our game. Setting this to background, on the other hand, allows this to happen. The second piece of our cooperative level determines whether other applications will be allowed to access the input device when the game is using them. Nonexclusive means that the input device can be shared with others, and that works best to keep our application on friendly terms with any software our players may be using. The only time you really want to use exclusive mode is when using a mouse in a three-dimensional shooter game, to prevent our mouse from slipping out the side of the window when we’re turning. Finally, we capture (acquire) our input device, and create two empty instances to store the state of the keyboard.
void CInputManager::Update(void)
{
// Update the keyboard
this->_preKeyboardState->CopyKeyBuffer(this->_curKeyboardState->GetKeyStates());
// Make sure we reacquire the keyboard if the device was lost
if(this->_keyboard->GetDeviceState(this->_curKeyboardState->GetBufferSize(), this->_curKeyboardState->GetKeyBuffer()) == DIERR_INPUTLOST)
this->_keyboard->Acquire();
}
The update method updates our keyboard states. First, we use the CopyKeyBuffer (which, as you will see when we design our Keyboard State class, simply copies the key buffer array from another array). We then check to make sure that the device was not lost (for example, if the window is switched). If it is, we re-acquire it. When we use the GetDeviceState() function within the if() statement, we are also capturing the keyboard state – first, we pass the size of the buffer, then we pass a pointer to the buffer array. GetDeviceState will copy the state of our keyboard to the key buffer array.
Because the IsKeyUp and IsKeyDown functions directly map to functions of the same name on the current keyboard state, we’ve only one last thing to cover:
bool CInputManager::WasKeyPressed(int keyCode)
{
if(this->_curKeyboardState->IsKeyDown(keyCode) && this->_preKeyboardState->IsKeyUp(keyCode))
return true;
else
return false;
}
This is pretty obvious, but I want to be completely clear on how we’re working this. If the state has been changed between the current frame and the last, we return true. In the case above, we are checking if the key has been pressed. We check the current state of the key against the previous state – if it was released (up) on the last frame, but is pressed (down) now, we return true – the key has been pressed between the current and last frames. Otherwise, it hasn’t, so we return false.
Code Listing 3: CKeyboardState.h
#pragma once
#include <Windows.h>
namespace D3DPong
{
class CKeyboardState
{
private:
char _keyStates[256];
public:
/** Constructor & Destructor **/
CKeyboardState(void);
~CKeyboardState(void);
/** Public Methods **/
bool IsKeyDown(int);
bool IsKeyUp(int);
void CopyKeyBuffer(char*);
/** Accessors & Mutators **/
char* GetKeyStates(void) { return this->_keyStates; }
LPVOID GetKeyBuffer(void) { return (LPVOID)&this->_keyStates; }
DWORD GetBufferSize(void) { return sizeof(this->_keyStates); }
};
};
The only member variable here is our key buffer – which is a simple char array, exactly what will be set by the keyboard device’s GetDeviceState function. I’ll cover the functions below:
Code Listing 4: CKeyboardState.cpp
#include "CKeyboardState.h"
namespace D3DPong
{
CKeyboardState::CKeyboardState(void)
{
}
CKeyboardState::~CKeyboardState(void)
{
}
bool CKeyboardState::IsKeyDown(int keyCode)
{
if(this->_keyStates[keyCode] & 0x80)
return true;
else
return false;
}
bool CKeyboardState::IsKeyUp(int keyCode)
{
return !this->IsKeyDown(keyCode);
}
void CKeyboardState::CopyKeyBuffer(char* buffer)
{
for(int i = 0; i < 256; i++)
this->_keyStates[i] = buffer[i];
}
};
Here we go!
bool CKeyboardState::IsKeyDown(int keyCode)
{
if(this->_keyStates[keyCode] & 0x80)
return true;
else
return false;
}
To check whether or not a key is up, we simply do a BITWISE AND operation against the key state (stored in our buffer) at the keycode in our array against 0×80. The keyCode integer will be a pre-defined DirectInput keycode, such as DIK_ESCAPE or DIK_HOME – you can find a list of DirectInput keycodes here. To find out if the key is up, we need only check if it is not down – so our IsKeyUp merely returns !IsKeyDown.
void CKeyboardState::CopyKeyBuffer(char* buffer)
{
for(int i = 0; i < 256; i++)
this->_keyStates[i] = buffer[i];
}
Here, we actually copy the key buffer. There are only 256 indexes in the key buffer array, so that’s all we need to copy. In case you’re surprised, yes, arrays are simply glorified pointers with fixed sizes – and you can easily use an array as a pointer.
Wrapping it into our engine
Now, we need to make some ties to bring our singleton into the application. Open up WinMain.cpp, and let’s get started.
#define InputManager D3DPong::CInputManager::Instance()
Just as we did with the instance of the game engine, we are going to create a Macro to access our Input Manager singleton. Now, we can just use InputManager->WasKeyPressed instead of going out of our way to type D3DPong::CInputManager::Instance()->WasKeyPressed.
// Initialize the Game Engine
GEInstance->SetWindowHandle(hWnd);
if(!GEInstance->Init(GEInstance->GetScreenWidth(), GEInstance->GetScreenHeight(), GEInstance->GetColorDepth()))
return 0;
if(!InputManager->Init())
return 0;
// Message Loop
while(GEInstance->GetGameRunning())
{
while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
InputManager->Update();
GEInstance->Update();
}
The only changes here are the lines regarding the InputManager – these parts are all we need to tie the input manager into our engine. They’re quite obvious in what they do – initialize and update the input manager – so I doubt that I need to give much explanation.
Adding input to our test game
I’m only going to cover checking for the escape key on this, but if you look at the tutorial files, I’ve added some simple platformer-esque input handling to our earlier rendering and physics tests.
void GameUpdate(float delta)
{
if(InputManager->WasKeyReleased(DIK_ESCAPE))
GEInstance->SetGameRunning(false);
// [...] The rest of our update method
}
Here, we check if the Escape key has been released – this will allow us to easily exit our window via a single keystroke. I choose to check for the release of the escape key out of personal preference, because I prefer acting on the release of most keystrokes, but you could achieve similar results by doing WasKeypressed – it would merely act when the key was pressed, instead of when it was released (</obvious>).
And there you have it, the (long awaited?) keyboard input tutorial. If I missed something between the downloadable source and what I’ve posted, please let me know so I can fix it – I wrote the code for this ages ago, and only finally got around to writing the tutorial portion recently.
| Print article | This entry was posted by Samantha on April 25, 2010 at 6:09 PM, and is filed under C++, Computers, Development, Languages, Pong (C++/DirectX), Programming, Series, Technology, Tutorials. Follow any responses to this post through RSS 2.0. You can leave a response or trackback from your own site. |
about 3 weeks ago
Thank you very much for these tutorials!
about 2 months ago
Hi,
you’re doing a very good job! Do you have any idea when you’ll finish the next part? I know the code is available on assembla, but some explanations would be very helpful!
I’m using Visual C++ 2010 Express, and there I had to add “dinput8.lib” instead of “dinput.lib”.
Greetings from Germany,
Felix
about 4 months ago
http://files.minalien.com/D3DPong/03/D3DPong-Src.7z
This archive is corrupted.
about 3 months ago
I’ll check it out – I’ve been gone throughout the past week or so. Sorry.
about 3 months ago
Ah, here’s the problem – I forgot to change the number on the link. Guess that’s what happens when you copy-paste for a template. :P
http://files.minalien.com/D3DPong/04/D3DPong-Src.7z
Updating the link in the post itself now.
about 4 months ago
And for people who hasn’t watched my Twitter, the next tutorial will be covering the creation of a Finite State Machine and we’ll be creating our Title Screen, Main Menu, and setting up for our game.
Code for the FSM and some testing code for it is set up in the SVN repo on Assembla.
about 4 months ago
Just found something I missed. Make sure you go into CGameEngine.h and add
to the headers section.