Lesson 23
     
  Advanced Input with Direct Input and Windows

With the way things are nowadays, you must use the latest technology to compete with games such as Quake and Unreal. In this tutorial, I will teach you how to set up your compiler for Direct Input, how to use it, and how to use the mouse in Opengl w/ Windows. This tutorial is based on code from Lesson 10. So open the Lesson 10 source code and lets get started!

The Mouse

First we need to add in a variable to hold the mouse's X and Y position.

 
     
typedef struct tagSECTOR
{
        int numtriangles;
        TRIANGLE* triangle;
} SECTOR;

SECTOR sector1;                                                         // Our Model Goes Here:

POINT mpos;                                                             // Mouse Position ( Add )

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);                   // Declaration For WndProc
     
  Ok, as you can see, we have added in a new variable called mpos. (Mouse Position). mpos has two variables, x and y. We will use these variables to figure out how to rotate the scene. We will modify parts of CreateGLWindow() with the following code.  
     
ShowCursor(FALSE);                                                      // Hide Mouse Pointer ( Modified )
if (fullscreen)                                                         // Are We Still In Fullscreen Mode?
{
        dwExStyle=WS_EX_APPWINDOW;                                      // Window Extended Style
        dwStyle=WS_POPUP;                                               // Windows Style
}
     
  Above, we moved the ShowCursor() out of the if statement below it. Therefore, if we go fullscreen or windowed the cursor will never be shown. Now we need to get and set the mouse every time we render. So modify the following in WinMain():  
     
SwapBuffers(hDC);                                                       // Swap Buffers (Double Buffering)
GetCursorPos(&mpos);                                                    // Get The Current Mouse Position ( Add )
SetCursorPos(320,240);                                                  // Set Mouse Position To Center Of The Window ( Add )
heading += (float)(320 - mpos.x)/100 * 5;                               // Update The Direction For Movement ( Add )
yrot = heading;                                                         // Update The Y Rotation ( Add )
lookupdown -= (float)(240 - mpos.y)/100 * 5;                            // Update The X Rotation ( Add )
     
  Lots to talk about here. First we get the mouse position with GetCursorPos(POINT p). This tells us how much to rotate on the X and Y axis. After we've got the position, we set it up for the next rendering pass using SetCursorPos(int X, int Y).

Note: Do not set the mouse position to 0,0! If you do, you will not be able to move the mouse to the upper left because 0,0 is the upper left of the window. 320 is the middle of the window from left to right and 240 is the middle of the window from the top to the bottom in 640x480 mode. Just a reminder!

After we have taken care of the mouse we need to update some stuff for rendering and movement.

float = (P - CX) / U * S;

P    - The point we set the mouse to every time
CX - The current mouse position
U    - Units
S    - Mouse speed ( being a hardcore quaker I like it at 12 )

We also do this for the heading variable and the lookupdown variable.

There you have it! Mouse code worthy of the greats!

The KeyBoard (DirectX 7)

Now we can look around in our world. The next step is to read multiple keys. By adding this section of code, you will be able to walk forward, strafe, and crouch all at the same time! Enough chit-chat, let's code!

I will now explain the steps required to use DirectX 7. The first step will depend on your compiler. I will show you how to do it with Visual C++, although it shouldn't be much different with other compilers.
 

  1. First you must download, or order, the DirectX 7 Sdk ( 128 MB ).

  2. You can download or order by clicking here
    Or click here to download the necessary library and include files locally ( 1.32 MB ).
    Make sure you have DX7 installed on your computer.
     
  3. Next you must install the sdk or necessary on your computer. If you are using the dx7.zip file from this site, all you have to do is unzip the file, and move all of the include files into your Visual C++ include directory, and all of the library files into your Visual C++ library directory. The Visual C++ directories can usually be found at C:\Program Files\Microsoft Visual Studio\VC98. If you are not using Visual Studio, look for a directory called Visual C. Hopefully by now you know where the library files are, and where the include files are.

  4.  
  5. After you have installed the required files, open your project and go to Project->Settings.

  6.  
  7. Click on the Link tab, and move down to Object / Library Modules.

  8.  
  9. Type in the following at the beginning of the line, dinput.lib dxguid.lib winmm.lib. This links the Direct Input library, the DirectX GUI library, and the Windows Multimedia library (required for timing code) into our program.

  10. Now we have Direct Input setup for compiling in our project. Time for some coding!

    We need to include the Direct Input header file so that we can use some of its functions. Also, we need to add in Direct Input and the Direct Input Keyboard Device.

 
     
#include <stdio.h>                                                      // Header File For Standard Input/Output
#include <gl\gl.h>                                                      // Header File For The OpenGL32 Library
#include <gl\glu.h>                                                     // Header File For The GLu32 Library
#include <gl\glaux.h>                                                   // Header File For The Glaux Library
#include <dinput.h>                                                     // Direct Input Functions       ( Add )

LPDIRECTINPUT7          g_DI;                                           // Direct Input                 ( Add )
LPDIRECTINPUTDEVICE7    g_KDIDev;                                       // Keyboard Device              ( Add )
     
  The last two lines above set up Direct Input ( g_DI ) and the the keyboard device ( g_KDIDev ). The keyboard device receives the input and we translate and use it. Direct Input is not too far off from regular windows input as seen in the previous tutorials.

Windows       Direct Input
VK_LEFT        DIK_LEFT
VK_RIGHT      DIK_RIGHT
...etc

Basically all we do is change VK to DIK. Although I think that some keys have changed.

Now we need to add a new function to setup Direct Input and the keyboard device. Underneath CreateGLWindow(), add the following:

 
     
// Initializes Direct Input ( Add )
int DI_Init()
{
        // Create Direct Input
        if ( DirectInputCreateEx(       hInstance,                      // Window Instance
                                        DIRECTINPUT_VERSION,            // Direct Input Version
                                        IID_IDirectInput7,              // Version 7
                                        (void**)&g_DI,                  // Direct Input
                                        NULL ) )                        // NULL Parameter
        {
                return(false);                                          // Couldn't Initialize Direct Input
        }

        // Create The Keyboard Device
        if ( g_DI->CreateDeviceEx(      GUID_SysKeyboard,               // Define Which Device Tto Create (KeyBoard,Mouse,or Joystick)
                                        IID_IDirectInputDevice7,        // Version 7
                                        (void**)&g_KDIDev,              // KeyBoard Device
                                        NULL ) )                        // NULL Parameter
        {
                return(false);                                          // Couldn't Create The Keyboard Device
        }

        // Set The Keyboard Data Format
        if ( g_KDIDev->SetDataFormat(&c_dfDIKeyboard) )
        {
                return(false);                                          // Could Not Set The Data Format
        }

        // Set The Cooperative Level
        if ( g_KDIDev->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_EXCLUSIVE) )
        {
                return(false);                                          // Could Not Set The Cooperative Level
        }

        if (g_KDIDev)                                                   // Did We Create The Keyboard Device?
                g_KDIDev->Acquire();                                    // If So, Acquire It
        else                                                            // If Not
                return(false);                                          // Return False

        return(true);                                                   // Everything Ok, Return True
}

// Destroys DX ( Add )
void DX_End()
{
        if (g_DI)
        {
                if (g_KDIDev)
                {
                        g_KDIDev->Unacquire();
                        g_KDIDev->Release();
                        g_KDIDev = NULL;
                }

                g_DI->Release();
                g_DI = NULL;
        }
}
     
  I think the code above is pretty self explanatory. First we init Direct Input, then we create the Keyboard device, and finally we acquire it. Later on, I might talk about how you can also use the Mouse and Joystick with Direct Input ( although I don't suggest using Direct Input for the mouse ).

Now we need to change the code from Window's Input to Direct Input. Which means a whole lot of modifying! So, here we go!

 
     
In WndProc() The Following Code Was Removed

                case WM_KEYDOWN:                                        // Is A Key Being Held Down?
                {
                        keys[wParam] = TRUE;                            // If So, Mark It As TRUE
                        return 0;                                       // Jump Back
                }

                case WM_KEYUP:                                          // Has A Key Been Released?
                {
                        keys[wParam] = FALSE;                           // If So, Mark It As FALSE
                        return 0;                                       // Jump Back
                }

At The Top Of The Program, Make The Following Changes

BYTE    buffer[256];                                                    // New Key Buffer, Replaces Keys[] ( Modified )
bool    active=TRUE;                                                    // Window Active Flag Set To TRUE By Default
bool    fullscreen=TRUE;                                                // Fullscreen Flag Set To Fullscreen Mode By Default
bool    blend;                                                          // Blending ON/OFF
bool    bp;                                                             // Blend Button Pressed?
bool    fp;                                                             // F1 Key Pressed? ( Modified )

...

GLfloat lookupdown = 0.0f;
GLfloat z=0.0f;                                                         // Depth Into The Screen
GLuint  filter;                                                         // Which Filter To Use ( Removed )

GLuint texture[5];                                                      // Storage For 5 Textures ( Modified )

In The WinMain() Function

        // Create Our OpenGL Window
        if (!CreateGLWindow("Justin Eslinger's & NeHe's Advanced DirectInput Tutorial",640,480,16,fullscreen))  ( Modified )
        {
                return 0;                                               // Quit If Window Was Not Created
        }

        if (!DI_Init())                                                 // Initialize DirectInput ( Add )
        {
                return 0;
        }

        ...

                        // Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene()
                        if ((active && !DrawGLScene()))                 // Active? Was There A Quit Received? ( Modified )

                        ...

                                HRESULT hr = g_KDIDev->GetDeviceState(sizeof(buffer), &buffer); // Get The Keys That Have Been Pressed ( Add )

                                if ( buffer[DIK_ESCAPE] & 0x80 )        // Check For Escape Key ( Modified )
                                {
                                        done=TRUE;
                                }

                                if ( buffer[DIK_B] & 0x80)              // B Key Being Pressed? ( Modified )
                                {
                                        if (!bp)
                                        {
                                                bp = true;              // Is The Blend Button Down? ( Add )
                                                blend=!blend;
                                                if (!blend)
                                                {
                                                        glDisable(GL_BLEND);
                                                        glEnable(GL_DEPTH_TEST);
                                                }
                                                else
                                                {
                                                        glEnable(GL_BLEND);
                                                        glDisable(GL_DEPTH_TEST);
                                                }
                                        }
                                }
                                else
                                {
                                        bp = false;
                                }

                                if ( buffer[DIK_PRIOR] & 0x80 )         // Page Up? ( Modified )
                                {
                                        z-=0.02f;
                                }

                                if ( buffer[DIK_NEXT] & 0x80 )          // Page Down? ( Modified )
                                {
                                        z+=0.02f;
                                }

                                if ( buffer[DIK_UP] & 0x80 )            // Up Arrow? ( Modified )
                                {
                                        xpos -= (float)sin(heading*piover180) * 0.05f;
                                        zpos -= (float)cos(heading*piover180) * 0.05f;
                                        if (walkbiasangle >= 359.0f)
                                        {
                                                walkbiasangle = 0.0f;
                                        }
                                        else
                                        {
                                                walkbiasangle+= 10;
                                        }

                                        walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
                                }

                                if ( buffer[DIK_DOWN] & 0x80 )          // Down Arrow? ( Modified )
                                {
                                        xpos += (float)sin(heading*piover180) * 0.05f;
                                        zpos += (float)cos(heading*piover180) * 0.05f;
                                        if (walkbiasangle <= 1.0f)
                                        {
                                                walkbiasangle = 359.0f;
                                        }
                                        else
                                        {
                                                walkbiasangle-= 10;
                                        }

                                        walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
                                }

                                if ( buffer[DIK_LEFT] & 0x80 )          // Left Arrow? ( Modified )
                                {
                                        xpos += (float)sin((heading - 90)*piover180) * 0.05f;   ( Modified )
                                        zpos += (float)cos((heading - 90)*piover180) * 0.05f;   ( Modified )
                                        if (walkbiasangle <= 1.0f)
                                        {
                                                walkbiasangle = 359.0f;
                                        }
                                        else
                                        {
                                                walkbiasangle-= 10;
                                        }

                                        walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
                                }

                                if ( buffer[DIK_RIGHT] & 0x80 )         // Right Arrow? ( Modified )
                                {
                                        xpos += (float)sin((heading + 90)*piover180) * 0.05f;   ( Modified )
                                        zpos += (float)cos((heading + 90)*piover180) * 0.05f;   ( Modified )
                                        if (walkbiasangle <= 1.0f)
                                        {
                                                walkbiasangle = 359.0f;
                                        }
                                        else
                                        {
                                                walkbiasangle-= 10;
                                        }

                                        walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
                                }

                                if ( buffer[DIK_F1] & 0x80)             // Is F1 Being Pressed? ( Modified )
                                {
                                        if (!fp)                        // If F1 Isn't Being "Held" ( Add )
                                        {
                                                fp = true;              // Is The F1 Button Down? ( Add )
                                                KillGLWindow();         // Kill Our Current Window ( Modified )
                                                fullscreen=!fullscreen; // Toggle Fullscreen/Windowed Mode ( Modified )

                                                // Recreate Our OpenGL Window
                                                if (!CreateGLWindow("Justin Eslinger's & NeHe's Advanced Direct Input Tutorial",640,480,16,fullscreen)) ( Modified )
                                                {
                                                        return 0;       // Quit If Window Was Not Created ( Modified )
                                                }

                                                if (!DI_Init())         // ReInitialize DirectInput ( Add )
                                                {
                                                        return 0;       // Couldn't Initialize, Quit
                                                }
                                        }
                                }
                                else
                                {
                                        fp = false;                     // Set 'fp' To False
                                }
                        }
                }
        }

        // Shutdown
        DX_End();                                                       // Destroys DirectX ( Add )
        KillGLWindow();                                                 // Kill The Window
        return (msg.wParam);                                            // Exit The Program
}

In DrawGLScene() Modify From The First Line Below

        glTranslatef(xtrans, ytrans, ztrans);
        numtriangles = sector1.numtriangles;
        
        // Process Each Triangle
        for (int loop_m = 0; loop_m < numtriangles; loop_m++)
        {
                glBindTexture(GL_TEXTURE_2D, texture[sector1.triangle[loop_m].texture]);        ( Modified )

                glBegin(GL_TRIANGLES);
     
  Ok, I need to discuss some things here. First, I took out some stuff that we didn't need. Then I replaced the old Windows keyboard stuff. I also changed the left and right keys. Now, when you go left or right, it strafes instead of turns! All I did was add or minus 90 degrees from the heading direction! That's pretty much it! Everything else is commented. Now we can compile and run our game! Whoohooo! hehehe

Now we have Direct Input and Mouse support in our game, what next? Well, we need to add in a timing system to regulate the speed in our game. Without timing, we check the input every frame and that could make us zip through the level before we can even look at it! So let's get started!

First we need adjustment variables to slow down our game and a timer structure.

 
     
POINT   mpos;                                                           // Mouse Position
int     adjust = 5;                                                     // Speed Adjustment ( Add )

// Create A Structure For The Timer Information ( Add )
struct
{
        __int64         frequency;                                      // Timer Frequency
        float           resolution;                                     // Timer Resolution
        unsigned long   mm_timer_start;                                 // Multimedia Timer Start Value
        unsigned long   mm_timer_elapsed;                               // Multimedia Timer Elapsed Time
        bool            performance_timer;                              // Using The Performance Timer?
        __int64         performance_timer_start;                        // Performance Timer Start Value
        __int64         performance_timer_elapsed;                      // Performance Timer Elapsed Time
} timer;                                                                // Structure Is Named Timer

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);                   // Declaration For WndProc
     
  The above code was discussed in lesson 21 so I shouldn't have to explain it.

Just below the declaration for the wndproc(), we need to add in the timer functions.

 
     
// Initialize Our Timer (Get It Ready) ( Add )
void TimerInit(void)
{
        memset(&timer, 0, sizeof(timer));                               // Clear Our Timer Structure
        // Check To See If A Performance Counter Is Available
        // If One Is Available The Timer Frequency Will Be Updated
        if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))
        {
                // No Performace Counter Available
                timer.performance_timer = FALSE;                        // Set Performance Timer To FALSE
                timer.mm_timer_start = timeGetTime();                   // Use timeGetTime() To Get Current Time
                timer.resolution = 1.0f/1000.0f;                        // Set Our Timer Resolution To .001f
                timer.frequency = 1000;                                 // Set Our Timer Frequency To 1000
                timer.mm_timer_elapsed = timer.mm_timer_start;          // Set The Elapsed Time To The Current Time
        }
        else
        {
                // Performance Counter Is Available, Use It Instead Of The Multimedia Timer
                // Get The Current Time And Store It In performance_timer_start
                QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);
                timer.performance_timer = TRUE;                         // Set Performance Timer To TRUE

                // Calculate The Timer Resolution Using The Timer Frequency
                timer.resolution = (float) (((double)1.0f)/((double)timer.frequency));
                // Set The Elapsed Time To The Current Time
                timer.performance_timer_elapsed = timer.performance_timer_start;
        }
}

// Get Time In Milliseconds ( Add )
float TimerGetTime()
{
        __int64 time;                                                   // time Will Hold A 64 Bit Integer
        if (timer.performance_timer)                                    // Are We Using The Performance Timer?
        {
                QueryPerformanceCounter((LARGE_INTEGER *) &time);       // Grab The Current Performance Time

                // Return The Current Time Minus The Start Time Multiplied By The Resolution And 1000 (To Get MS)
                return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;
        }
        else
        {
                // Return The Current Time Minus The Start Time Multiplied By The Resolution And 1000 (To Get MS)
                return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;
        }
}
     
  The above was also in Lesson 21 so nothing to explain here. Just make sure you add the winmm.lib library file. Otherwise you will get errors when you compile.

Now we must add some stuff in the WinMain() function.

 
     
        if (!DI_Init())                                                 // Initialize DirectInput  ( Add )
        {
                return 0;
        }

        TimerInit();                                                    // Init Our Timer  ( Add )

        ...

                        float start=TimerGetTime();
                        // Grab Timer Value Before We Draw  ( Add )

                        // Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()
                        if ((active && !DrawGLScene()))                 // Active?  Was There A Quit Received?  ( Modified )
                        {
                                done=TRUE;                              // ESC or DrawGLScene Signalled A Quit
                        }
                        else                                            // Not Time To Quit, Update Screen
                        {
                                while(TimerGetTime()<start+float(adjust*2.0f)) {}       // Waste Cycles On Fast Systems ( Add )
     
  Now the game will run at the adjusted speed. The following segment will be dedicated to some graphical adjustments I made to the Lesson 10 Level.

If you've already downloaded the code for this lesson, then you've already seen that I've added multiple textures to the scene. The new textures are in the DATA directory. Here's how I added the textures:

 
     
Modify the tagTriangle structure

typedef struct tagTRIANGLE
{
        int     texture;        ( Add )
        VERTEX  vertex[3];
} TRIANGLE;

Modify the SetupWorld code

for (int loop = 0; loop < numtriangles; loop++)
{
        readstr(filein,oneline);        ( Add )
        sscanf(oneline, "%i\n", &sector1.triangle[loop].texture);       ( Add )
        for (int vert = 0; vert < 3; vert++)
        {

Modify the DrawGLScene code

// Process Each Triangle
        for (int loop_m = 0; loop_m < numtriangles; loop_m++)
        {
                glBindTexture(GL_TEXTURE_2D, texture[sector1.triangle[loop_m].texture]);        ( Modified )

                glBegin(GL_TRIANGLES);

In the LoadGLTextures Code We Add More Textures

int LoadGLTextures()                                                    // Load Bitmaps And Convert To Textures
{
        int Status=FALSE;                                               // Status Indicator
        AUX_RGBImageRec *TextureImage[5];                               // Create Storage Space For The Textures
        memset(TextureImage,0,sizeof(void *)*2);                        // Set The Pointer To NULL
        if (    (TextureImage[0]=LoadBMP("Data/floor1.bmp")) &&         // Load The Floor Texture
                (TextureImage[1]=LoadBMP("Data/light1.bmp")) &&         // Load The Light Texture
                (TextureImage[2]=LoadBMP("Data/rustyblue.bmp")) &&      // Load the Wall Texture
                (TextureImage[3]=LoadBMP("Data/crate.bmp")) &&          // Load The Crate Texture
                (TextureImage[4]=LoadBMP("Data/weirdbrick.bmp")))       // Load the Ceiling Texture
        {
                Status=TRUE;                                            // Set The Status To TRUE
                glGenTextures(5, &texture[0]);                          // Create The Texture
                for (int loop1=0; loop1<5; loop1++)                     // Loop Through 5 Textures
                {
                        glBindTexture(GL_TEXTURE_2D, texture[loop1]);
                        glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX, TextureImage[loop1]->sizeY, 0,
                                GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);
                        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
                        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
                }
                for (loop1=0; loop1<5; loop1++)                         // Loop Through 5 Textures
                {
                        if (TextureImage[loop1])                        // If Texture Exists
                        {
                                if (TextureImage[loop1]->data)          // If Texture Image Exists
                                {
                                        free(TextureImage[loop1]->data);// Free The Texture Image Memory
                                }
                                free(TextureImage[loop1]);              // Free The Image Structure
                        }
                }
        }
        return Status;                                                  // Return The Status
}

     
  So now you're able to harness the awesome power of Direct Input. I spent a lot of time writing and revising this tutorial to make it very easy to understand and also error free. I hope this tutorial is helpful to those that wanted to learn this. I felt that since this site gave me enough knowledge to create the engine I have now, that I should give back to the community. Thanks for taking the time to read this!

Justin Eslinger (BlackScar)

blackscar@ticz.com

* DOWNLOAD Visual C++ Code For This Lesson.
* DOWNLOAD Visual C++ / OpenIL Code For This Lesson. ( Conversion by Denton Woods )
* DOWNLOAD Mac OS Code For This Lesson. ( Conversion by Morgan Aldridge )
* DOWNLOAD Irix / GLUT Code For This Lesson. ( Conversion by Rob Fletcher )
* DOWNLOAD Delphi Code For This Lesson. ( Conversion by Marc Aarts )

 
     
 

Back To NeHe Productions!