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
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.
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.
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
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", §or1.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) * DOWNLOAD Visual
C++ Code For This Lesson.
|
|||
Back To NeHe Productions! |
|||