The flight simulator we developed in the previous article brought up the need for a serious control of the computer input devices. This is not so obvious for the keyboard but for the mouse it is a real problem. When the cursor reaches the edge of the screen it stops posting mouse move messages in that direction. In our game this translated as if the control has reached its physical limit and although we keep moving the mouse the game does not respond.
So we need a better method of reading the mouse movements and if possible use a joystick as a control device to make the game a little more realistic. We will use Direct Input to read devices like joystick, mouse and keyboard. This means that our game is getting some close relationship with Windows. We will try to keep this bonding as loose as possible so that it will be easy to move to any other platform we might need.
DirectInput is part of the DirectX programming API for Windows. Although DirectX covers graphics programming as well, we will not modify our drawing code to use it. We will only use the parts we need and this time we need its interface to the user input devices. So go to Micfrosoft and download the latest version of the DirectX SDK, install it and get on reading this article.
In the 'core' folder of our flight simulator project you will notice two new files. These are 'clf_input.cpp' and 'clf_input.h'. They contain all the code required to control the airplane. The code in these files was actually assembled from parts taken from older DirectX samples dated some years back. The good thing about DirectX is its backwards compatibility. The code compiles with the latest version and runs without any problems. You can use the code as is for simple games like our flight simulator, after all this is only an introduction to Direct Input.
Let's go through the code and see how it works.
The first step is to initialize Direct Input. This is handled by the 'InitDirectInput' function. The application core has been modified to use Direct Input all the time instead of relying on the default system messaging mechanism. So it calls the 'InitDirectInput' function to initialize the system and request for keyboard initialization. The initialization parameters structure 'sInitialSettings' has been enhanced with two variables which can be set to true if you want to capture mouse and/or joystick. Direct Input initializes mouse and/or keyboard according to your request.
The next thing we have to do is request the state of the devices we are interested in at every iteration of the main loop of our program. This is done internally in the main function before the call to the 'frame_move' function. So in our 'frame_move' we can read the status of our devices and control our game.
Finally we must unlock the devices before we terminate our game so that the system will continue to function normally.
Here is the initialization function which is called automatically from the main function. This function calls the functions which initialize the keyboard, the mouse and the joystic.
////////////////////////////////////////////////////////////////////////// // initialize int InitDirectInput( HWND hDlg, bool bImm, bool bKbd, bool bMouse, bool bJoy) { HRESULT hr; // Register with the DirectInput subsystem and get a pointer // to a IDirectInput interface we can use. // Create a DInput object if( FAILED( hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&g_pDI, NULL ) ) ) return 0; bImmediate = bImm; if (bJoy) CreateJoystickDevice( hDlg ); if (bKbd) CreateKeyboardDevice( hDlg ); if (bMouse) CreateMouseDevice( hDlg ); return 1; }
Here is the function that initializes the joystick. Similar functions perform the initializations for the other devices.
////////////////////////////////////////////////////////////////////// // joystick static HRESULT CreateJoystickDevice( HWND hDlg ) { HRESULT hr; DIJOYCONFIG PreferredJoyCfg = {0}; DI_ENUM_CONTEXT enumContext; enumContext.pPreferredJoyCfg = &PreferredJoyCfg; enumContext.bPreferredJoyCfgValid = false; IDirectInputJoyConfig8* pJoyConfig = NULL; if( FAILED( hr = g_pDI->QueryInterface( IID_IDirectInputJoyConfig8, (void **) &pJoyConfig ) ) ) return hr; PreferredJoyCfg.dwSize = sizeof(PreferredJoyCfg); // This function is expected to fail if no joystick is attached if( SUCCEEDED( pJoyConfig->GetConfig(0, &PreferredJoyCfg, DIJC_GUIDINSTANCE ) ) ) enumContext.bPreferredJoyCfgValid = true; SAFE_RELEASE( pJoyConfig ); // Look for a simple joystick we can use for this sample program. if( FAILED( hr = g_pDI->EnumDevices( DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback, &enumContext, DIEDFL_ATTACHEDONLY ) ) ) return hr; // Make sure we got a joystick if( NULL == g_pJoystick ) return S_OK; // Set the data format to "simple joystick" - a predefined data format // // A data format specifies which controls on a device we are interested in, // and how they should be reported. This tells DInput that we will be // passing a DIJOYSTATE2 structure to IDirectInputDevice::GetDeviceState(). if( FAILED( hr = g_pJoystick->SetDataFormat( &c_dfDIJoystick2 ) ) ) return hr; // Set the cooperative level to let DInput know how this device should // interact with the system and with other DInput applications. if( FAILED( hr = g_pJoystick->SetCooperativeLevel( hDlg, DISCL_EXCLUSIVE | DISCL_FOREGROUND ) ) ) return hr; // Enumerate the joystick objects. The callback function enabled user // interface elements for objects that are found, and sets the min/max // values property for discovered axes. if( FAILED( hr = g_pJoystick->EnumObjects( EnumObjectsCallback, (VOID*)hDlg, DIDFT_ALL ) ) ) return hr; return S_OK; }
The next step is to request the state of the devices and user actions at every loop of our program. This is done automatically in the main function which calls the 'UpdateInput' function. Here is the code of the 'UpdateJoystick' function which is called in order to update the state of the joystick.
static HRESULT UpdateJoystick( HWND hDlg ) { HRESULT hr; if( NULL == g_pJoystick ) return S_OK; // Poll the device to read the current state hr = g_pJoystick->Poll(); if( FAILED(hr) ) { // DInput is telling us that the input stream has been // interrupted. We aren't tracking any state between polls, so // we don't have any special reset that needs to be done. We // just re-acquire and try again. hr = g_pJoystick->Acquire(); while( hr == DIERR_INPUTLOST ) hr = g_pJoystick->Acquire(); // hr may be DIERR_OTHERAPPHASPRIO or other errors. This // may occur when the app is minimized or in the process of // switching, so just try again later return S_OK; } // Get the input's device state if( FAILED( hr = g_pJoystick->GetDeviceState( sizeof(DIJOYSTATE2), &js ) ) ) return hr; // The device should have been acquired during the Poll() return S_OK; }
Finally we must inform the system that our game is about to end and return control of the devices. The main function calls the 'FreeDirectInput' function. This calls the specific functions for each device captured. Here is the code for the 'FreeDirectInput' function.
////////////////////////////////////////////////////////////////////////// // terminate void FreeDirectInput() { if( g_pJoystick ) g_pJoystick->Unacquire(); if( g_pKeyboard ) g_pKeyboard->Unacquire(); if( g_pMouse ) g_pMouse->Unacquire(); SAFE_RELEASE( g_pJoystick ); SAFE_RELEASE( g_pKeyboard ); SAFE_RELEASE( g_pMouse ); SAFE_RELEASE( g_pDI ); }
One good advice I got from one of my teachers is always spend time thoroughly examining your tools and learning their operating details. In our case the tools is DirectX and the way to approach its inner workings is by reading the documentation.
In our game now we update the state of the virtual flight controls using the state of the DirectInput devices. First we create a structure to hold the state of the stick that controls the airplane. For the purpose of the sample code we hold the X-Y position of the joystick for the roll and pitch control and, the position of the first slider as the thrust position.
struct controlState{ controlState(){ } float pitch; float roll; float yoke; float flaps; float thrust; }ctrlState; // constant value used to convert joystick axis values // in the range -1 to 1, required by the airplane simulator code // instead of -1000 to 1000, returned by the joystick const float ctrlLimit = 1000.f; static void update_user_input(float fElapsed) { static float tsx = 0; static float tsy = 0; static float tst = 0; ///////////////////////////////////////////////////////// DIMOUSESTATE2* ms = getMouseStateImm(); if (ms) { if (ms->lX != 0) { tsx += ms->lX; if (tsx < -ctrlLimit) tsx = -ctrlLimit; if (tsx > ctrlLimit) tsx = ctrlLimit; ctrlState.roll = (float)tsx / ctrlLimit; } if (ms->lY != 0) { tsy += ms->lY; if (tsy < -ctrlLimit) tsy = -ctrlLimit; if (tsy > ctrlLimit) tsy = ctrlLimit; ctrlState.pitch = (float)tsy / ctrlLimit; } if (isKeyPressed(DIK_Z)) { tst -= fElapsed*400.f; } if (isKeyPressed(DIK_A)) { tst += fElapsed*400.f; } if (tst < 0.f) tst = 0.f; if (tst > 1000.f) tst = 1000.f; ctrlState.thrust = tst / ctrlLimit; } ///////////////////////////////////////////////////////// DIJOYSTATE2* js = getJoystickState(); if (js) { ctrlState.roll = (float)js->lX / ctrlLimit; ctrlState.pitch = (float)js->lY / ctrlLimit; ctrlState.thrust = (float)(-js->rglSlider[0] + ctrlLimit)/(2*ctrlLimit); } ///////////////////////////////////////////////////////// }
And after we update the state of our virtual device within the 'frame_move' function we can take appropriate actions.
float lr = 0.f; float ud = 0.f; update_user_input(fElapsed); lr = ctrlState.roll; if (lr < 0) ship.RollLeft(-lr); else if (lr > 0) ship.RollRight(lr); ///////////////////////////////////////////////////////// ud = ctrlState.pitch; if (ud < 0) ship.PitchDown(-ud); else if (ud > 0) ship.PitchUp(ud); ///////////////////////////////////////////////////////// ship.SetThrust(ctrlState.thrust);
So now we can fly our airplane either by using the joystick or the mouse. It was not so hard after all and our little game is lot more fun to play.
You can download the updated code from here.