Picking, Alpha Blending, Alpha Testing, SortingWelcome to Tutorial 32. This tutorial is probably the largest tutorial I have written to date. Over 1000 lines of Code and more than 1540 lines of HTML. This is also the first tutorial to use my new NeHeGL basecode. This tutorial took a long time to write, but I think it was worth the wait. Some of the topics I cover in this tutorial are: Alpha Blending, Alpha Testing, Reading The Mouse, Using Both Ortho And Perspective At The Same Time, Displaying A Custom Cursor, Manually Sorting Objects By Depth, Animating Frames From A Single Texture and most important, you will learn all about PICKING! The original version of this tutorial displayed three objects on the screen that would change color when you clicked on them. How exciting is that!?! Not exciting at all! As always, I wanted to impress you guys with a super cool tutorial. I wanted the tutorial to be exciting, packed full of information and of course... nice to look at. So, after weeks of coding, the tutorial is done! Even if you don't code you might enjoy this tutorial. It's a complete game. The object of the game is to shoot as many targets as you can before your morale hits rock bottom or your hand cramps up and you can no longer click the mouse button. I'm sure there will be critics, but I'm very happy with this tutorial! I've taken dull topics such as picking and sorting object by depth and turned them into something fun! Some quick notes about the code. I will only discuss the code in lesson32.cpp. There have been a few minor changes in the NeHeGL code. The most important change is that I have added mouse support to WindowProc(). I also added int mouse_x, mouse_y to store mouse movement. In NeHeGL.h the following two lines of code were added: extern int mouse_x; & extern int mouse_y; The textures used in this tutorial were made in Adobe Photoshop. Each .TGA file is a 32 bit image with an alpha channel. If you are not sure how to add an alpha channel to an image buy yourself a good book, browse the net or read the built in help in Adobe Photoshop. The entire process is very similar to the way I created masks in the masking tutorial. Load your object into Adobe Photoshop (or some other art program that supports the alpha channel). Use select by color range to select the area around your object. Copy that area. Create a new image. Paste the selection into the new image. Negate the image so the area where your image should be is black. Make the area around it white. Select the entire image and copy it. Go back to the original image and create an alpha channel. Paste the black and white mask that you just created into the alpha channel. Save the image as a 32 bit .TGA file. Make sure preserve transparency is checked, and make sure you save it uncompressed! As always I hope you enjoy the tutorial. I'm interested to hear what you think of it. If you have any questions or you find any mistakes, let me know. I rushed through parts of the tutorial, so if you find any part really hard to understand, send me some email and I'll try to explain things differently or in more detail! #include <windows.h> // Header File For Windows #include <stdio.h> // Header File For Standard Input / Output #include <stdarg.h> // Header File For Variable Argument Routines #include <gl\gl.h> // Header File For The OpenGL32 Library #include <gl\glu.h> // Header File For The GLu32 Library #include <time.h> // For Random Seed #include "NeHeGL.h" // Header File For NeHeGL In lesson 1, I preached about the proper way to link to the OpenGL libraries. In Visual C++ click on project, settings and then the link tab. Move down to object/library modules and add OpenGL32.lib, GLu32.lib and GLaux.lib. Failing to include a required library will cause the compiler to spout out error after error. Something you don't want happening! To make matters worse, if you only include the libaries in debug mode, and someone tries to build your code in release mode... more errors. There are alot of people looking for code. Most of them are new to programming. They grab your code, and try to compile it. They get errors, delete the code and move on. The code below tells the compiler to link to the required libraries. A little more typing, but alot less headache in the long run. For this tutorial, we will link to the OpenGL32 library, the GLu32 library and the WinMM library (for playing sound). In this tutorial we will be loading .TGA files so we don't need the GLaux library. #pragma comment( lib, "opengl32.lib" ) // Search For OpenGL32.lib While Linking #pragma comment( lib, "glu32.lib" ) // Search For GLu32.lib While Linking #pragma comment( lib, "winmm.lib" ) // Search For WinMM Library While Linking The 3 lines below check to see if CDS_FULLSCREEN has been defined by your compiler. If it has not been defined, we manually give CDS_FULLSCREEN a value of 4. For those of you that are completely lost right now... Some compilers do not give CDS_FULLSCREEN a value and will return an error message if CDS_FULLSCREEN is used! To prevent an error message, we check to see if CDS_FULLSCREEN has been defined and if not, we manually define it. Makes life easier for everyone. We then declare DrawTargets, and set up variables for our window and keyboard handling. If you don't understand declarations, read through the MSDN glossary. Keep in mind, I'm not teaching C/C++, buy a good book if you need help with the NON GL code! #ifndef CDS_FULLSCREEN // CDS_FULLSCREEN Is Not Defined By Some #define CDS_FULLSCREEN 4 // Compilers. By Defining It This Way, #endif // We Can Avoid Errors void DrawTargets(); // Declaration GL_Window* g_window; Keys* g_keys; The following section of code sets up our user defined variables. base will be used for our font display lists. roll will be used to move the ground and create the illusion of rolling clouds. level should be pretty straight forward (we start off on level 1). miss keeps track of how many objects were missed. It's also used to show the players morale (no misses means a high morale). kills keeps track of how many targets were hit each level. score will keep a running total of the number of objects hit, and game will be used to signal game over! The last line lets us pass structures to our compare function. The qsort routine expects the last parameter to be type type (const *void, const *void). // User Defined Variables GLuint base; // Font Display List GLfloat roll; // Rolling Clouds GLint level=1; // Current Level GLint miss; // Missed Targets GLint kills; // Level Kill Counter GLint score; // Current Score bool game; // Game Over? typedef int (*compfn)(const void*, const void*); // Typedef For Our Compare Function Now for our objects structure. This structure holds all the information about an object. The direction it's rotating, if it's been hit, it's location on the screen, etc. A quick rundown of the variables... rot specifies the direction we want to rotate the object. hit will be FALSE if the object has not yet been hit. If the object was hit or manually flagged as being hit, the value of hit will be TRUE. The variable frame is used to cycle through the frames of animation for our explosion. As frame is increased the explosion texture changes. More on this later in the tutorial. To keep track of which direction our object is moving, we have a variable called dir. dir can be one of 4 values: 0 - object is moving Left, 1 - object is moving right, 2 - object is moving up and finally 3 - object is moving down. texid can be any number from 0 to 4. Zero represents the BlueFace texture, 1 is the Bucket texture, 2 is the Target texture , 3 is the Coke can texture and 4 is the Vase texture. Later in the load texture code, you will see that the first 5 textures are the target images. Both x and y are used to position the object on the screen. x represents where the object is on the x-axis, and y the location of the object on the y-axis. The objects rotate on the z-axis based on the value of spin. Later in the code, we will increase or decrease spin based on the direction the object is travelling. Finally, distance keeps track of how far into the screen our object is. distance is an extremely important variable, we will use it to calculate the left and right sides of the screen, and to sort the objects so the objects in the distance are drawn before the objects up close. struct objects { GLuint rot; // Rotation (0-None, 1-Clockwise, 2-Counter Clockwise) bool hit; // Object Hit? GLuint frame; // Current Explosion Frame GLuint dir; // Object Direction (0-Left, 1-Right, 2-Up, 3-Down) GLuint texid; // Object Texture ID GLfloat x; // Object X Position GLfloat y; // Object Y Position GLfloat spin; // Object Spin GLfloat distance; // Object Distance }; No real reason to explain the code below. We are loading TGA images in this tutorial instead of bitmaps. The structure below is used to store image data, as well as information about the TGA image. Read the tutorial on loading TGA files if you need a detailed explanation of the code below. typedef struct // Create A Structure { GLubyte *imageData; // Image Data (Up To 32 Bits) GLuint bpp; // Image Color Depth In Bits Per Pixel. GLuint width; // Image Width GLuint height; // Image Height GLuint texID; // Texture ID Used To Select A Texture } TextureImage; // Structure Name The following code sets aside room for our 10 textures and 30 objects. If you plan to add more objects to the game make sure you increase the value from 30 to however many objects you want. TextureImage textures[10]; // Storage For 10 Textures objects object[30]; // Storage For 30 Objects I didn't want to limit the size of each object. I wanted the vase to be taller than the can, I wanted the bucket to be wider than the vase. To make life easy, I create a structure that holds the objects width (w) and height (h). I then set the width and height of each object in the last line of code. To get the coke cans width, I would check size[3].w. The Blueface is 0, the Bucket is 1, and the Target is 2, etc. The width is represented by w. Make sense? struct dimensions { // Object Dimensions GLfloat w; // Object Width GLfloat h; // Object Height }; // Size Of Each Object: Blueface, Bucket, Target, Coke, Vase dimensions size[5] = { {1.0f,1.0f}, {1.0f,1.0f}, {1.0f,1.0f}, {0.5f,1.0f}, {0.75f,1.5f} }; The following large section of code loads our TGA images and converts them to textures. It's the same code I used in lesson 25 so if you need a detailed description go back and read lesson 25. I use TGA images because they are capable of having an alpha channel. The alpha channel tells OpenGL which parts of the image are transparent and which parts are opaque. The alpha channel is created in an art program, and is saved inside the .TGA image. OpenGL loads the image, and uses the alpha channel to set the amount of transparency for each pixel in the image. bool LoadTGA(TextureImage *texture, char *filename) // Loads A TGA File Into Memory { GLubyte TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0}; // Uncompressed TGA Header GLubyte TGAcompare[12]; // Used To Compare TGA Header GLubyte header[6]; // First 6 Useful Bytes From The Header GLuint bytesPerPixel; // Holds Number Of Bytes Per Pixel Used In The TGA File GLuint imageSize; // Used To Store The Image Size When Setting Aside Ram GLuint temp; // Temporary Variable GLuint type=GL_RGBA; // Set The Default GL Mode To RBGA (32 BPP) FILE *file = fopen(filename, "rb"); // Open The TGA File if( file==NULL || // Does File Even Exist? fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) || // Are There 12 Bytes To Read? memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0 || // Does The Header Match What We Want? fread(header,1,sizeof(header),file)!=sizeof(header)) // If So Read Next 6 Header Bytes { if (file == NULL) // Does The File Even Exist? *Added Jim Strong* return FALSE; // Return False else // Otherwise { fclose(file); // If Anything Failed, Close The File return FALSE; // Return False } } texture->width = header[1] * 256 + header[0]; // Determine The TGA Width (highbyte*256+lowbyte) texture->height = header[3] * 256 + header[2]; // Determine The TGA Height (highbyte*256+lowbyte) if( texture->width <=0 || // Is The Width Less Than Or Equal To Zero texture->height <=0 || // Is The Height Less Than Or Equal To Zero (header[4]!=24 && header[4]!=32)) // Is The TGA 24 or 32 Bit? { fclose(file); // If Anything Failed, Close The File return FALSE; // Return False } texture->bpp = header[4]; // Grab The TGA's Bits Per Pixel (24 or 32) bytesPerPixel = texture->bpp/8; // Divide By 8 To Get The Bytes Per Pixel imageSize = texture->width*texture->height*bytesPerPixel; // Calculate The Memory Required For The TGA Data texture->imageData=(GLubyte *)malloc(imageSize); // Reserve Memory To Hold The TGA Data if( texture->imageData==NULL || // Does The Storage Memory Exist? fread(texture->imageData, 1, imageSize, file)!=imageSize) // Does The Image Size Match The Memory Reserved? { if(texture->imageData!=NULL) // Was Image Data Loaded free(texture->imageData); // If So, Release The Image Data fclose(file); // Close The File return FALSE; // Return False } for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel) // Loop Through The Image Data { // Swaps The 1st And 3rd Bytes ('R'ed and 'B'lue) temp=texture->imageData[i]; // Temporarily Store The Value At Image Data 'i' texture->imageData[i] = texture->imageData[i + 2]; // Set The 1st Byte To The Value Of The 3rd Byte texture->imageData[i + 2] = temp; // Set The 3rd Byte To The Value In 'temp' (1st Byte Value) } fclose (file); // Close The File // Build A Texture From The Data glGenTextures(1, &texture[0].texID); // Generate OpenGL texture IDs glBindTexture(GL_TEXTURE_2D, texture[0].texID); // Bind Our Texture glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // Linear Filtered glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Linear Filtered if (texture[0].bpp==24) // Was The TGA 24 Bits { type=GL_RGB; // If So Set The 'type' To GL_RGB } glTexImage2D(GL_TEXTURE_2D, 0, type, texture[0].width, texture[0].height, 0, type, GL_UNSIGNED_BYTE, texture[0].imageData); return true; // Texture Building Went Ok, Return True } The 2D texture font code is the same code I have used in previous tutorials. However, there are a few small changes. The thing you will notice is that we are only generating 95 display lists. If you look at the font texture, you will see there are only 95 characters counting the space at the top left of the image. The second thing you will notice is we divide by 16.0f for cx and we only divide by 8.0f for cy. The reason we do this is because the font texture is 256 pixels wide, but only half as tall (128 pixels). So to calculate cx we divide by 16.0f and to calculate cy we divide by half that (8.0f). If you do not understand the code below, go back and read through Lesson 17. The font building code is explained in detail in lesson 17! GLvoid BuildFont(GLvoid) // Build Our Font Display List { base=glGenLists(95); // Creating 95 Display Lists glBindTexture(GL_TEXTURE_2D, textures[9].texID); // Bind Our Font Texture for (int loop=0; loop<95; loop++) // Loop Through All 95 Lists { float cx=float(loop%16)/16.0f; // X Position Of Current Character float cy=float(loop/16)/8.0f; // Y Position Of Current Character glNewList(base+loop,GL_COMPILE); // Start Building A List glBegin(GL_QUADS); // Use A Quad For Each Character glTexCoord2f(cx, 1.0f-cy-0.120f); glVertex2i(0,0); // Texture / Vertex Coord (Bottom Left) glTexCoord2f(cx+0.0625f, 1.0f-cy-0.120f); glVertex2i(16,0); // Texutre / Vertex Coord (Bottom Right) glTexCoord2f(cx+0.0625f, 1.0f-cy); glVertex2i(16,16); // Texture / Vertex Coord (Top Right) glTexCoord2f(cx, 1.0f-cy); glVertex2i(0,16); // Texture / Vertex Coord (Top Left) glEnd(); // Done Building Our Quad (Character) glTranslated(10,0,0); // Move To The Right Of The Character glEndList(); // Done Building The Display List } // Loop Until All 256 Are Built } The printing code is the code is also from lesson 17, but has been modified to allow us to print the score, level and morale to the screen (variables that continually change). GLvoid glPrint(GLint x, GLint y, const char *string, ...) // Where The Printing Happens { char text[256]; // Holds Our String va_list ap; // Pointer To List Of Arguments if (string == NULL) // If There's No Text return; // Do Nothing va_start(ap, string); // Parses The String For Variables vsprintf(text, string, ap); // And Converts Symbols To Actual Numbers va_end(ap); // Results Are Stored In Text glBindTexture(GL_TEXTURE_2D, textures[9].texID); // Select Our Font Texture glPushMatrix(); // Store The Modelview Matrix glLoadIdentity(); // Reset The Modelview Matrix glTranslated(x,y,0); // Position The Text (0,0 - Bottom Left) glListBase(base-32); // Choose The Font Set glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // Draws The Display List Text glPopMatrix(); // Restore The Old Projection Matrix } This code will be called later in the program by qsort. It compares the distance in two structures and return -1 if the first structures distance was less than the seconds structures distance, 1 if the first structures distance is greater than the second structures distance and 0 if the distance is the same in both structures. int Compare(struct objects *elem1, struct objects *elem2) // Compare Function *** MSDN CODE MODIFIED FOR THIS TUT *** { if ( elem1->distance < elem2->distance) // If First Structure distance Is Less Than The Second return -1; // Return -1 else if (elem1->distance > elem2->distance) // If First Structure distance Is Greater Than The Second return 1; // Return 1 else // Otherwise (If The distance Is Equal) return 0; // Return 0 } The InitObject() code is where we set up each object. We start off by setting rot to 1. This gives the object clockwise rotation. Then we set the explosion animation to frame 0 (we don't want the explosion to start halfway through the animation). Next we set hit to FALSE, meaning the object has not yet been hit or set to self destruct. To select an object texture, texid is assigned a random value from 0 to 4. Zero is the blueface texture and 4 is the vase texture. This gives us one of 5 random objects. The variable distance will be a random number from -0.0f to -40.0f (4000/100 is 40). When we actually draw the object, we translate another 10 units into the screen. So when the objects are drawn, they will be drawn from -10.0f to -50.0f units into the screen (not to close, and not too far). I divide the random number by 100.0f to get a more accurate floating point value. After assigning a random distance, we then give the object a random y value. We don't want the object any lower than -1.5f, otherwise it will be under the ground, and we dont want the object any higher than 3.0f. So to stay in that range our random number can not be any higher than 4.5f (-1.5f+4.5f=3.0f). To calculate the x position, we use some tricky math. We take our distance and we subtract 15.0f from it. Then we divide the result by 2 and subtract 5*level. Finally, we subtract a random amount from 0.0f to 5 multiplied by the current level. We subtract the 5*level and the random amount from 0.0f to 5*level so that our object appears further off the screen on higher levels. If we didn't, the objects would appear one after another, making it even more difficult to hit all the targets than it already is. Finally we choose a random direction (dir) from 0 (left) to 1 (right). To make things easier to understand in regards to the x position, I'll write out a quick example. Say our distance is -30.0f and the current level is 1: object[num].x=((-30.0f-15.0f)/2.0f)-(5*1)-float(rand()%(5*1)); Now keeping in mind that we move 10 units into the screen before we draw our objects, and the distance in the example above is -30.0f. It's safe to say our actual distance into the screen will be -40.0f. Using the perspective code in the NeHeGL.cpp file, it's safe to assume that if the distance is -40.0f, the far left edge of the screen will be -20.0f and the far right will be +20.0f. In the code above our x value is -22.5f (which would be JUST off the left side of the screen). We then subtract 5 and our random value of 3 which guarantees the object will start off the screen (at -30.5f) which means the object would have to move roughly 8 units to the right before it even appeared on the screen. GLvoid InitObject(int num) // Initialize An Object { object[num].rot=1; // Clockwise Rotation object[num].frame=0; // Reset The Explosion Frame To Zero object[num].hit=FALSE; // Reset Object Has Been Hit Status To False object[num].texid=rand()%5; // Assign A New Texture object[num].distance=-(float(rand()%4001)/100.0f); // Random Distance object[num].y=-1.5f+(float(rand()%451)/100.0f); // Random Y Position // Random Starting X Position Based On Distance Of Object And Random Amount For A Delay (Positive Value) object[num].x=((object[num].distance-15.0f)/2.0f)-(5*level)-float(rand()%(5*level)); object[num].dir=(rand()%2); // Pick A Random Direction Now we check to see which direction the object is going to be travelling. The code below checks to see if the object is moving left. If it is, we have to change the rotation so that the object is spinning counter clockwise. We do this by changing the value of rot to 2. Our x value by default is going to be a negative number. However, the right side of the screen would be a positive value. So the last thing we do is negate the current x value. In english, we make the x value a positive value instead of a negative value. if (object[num].dir==0) // Is Random Direction Right { object[num].rot=2; // Counter Clockwise Rotation object[num].x=-object[num].x; // Start On The Left Side (Negative Value) } Now we check the texid to find out what object the computer has randomly picked. If texid is equal to 0, the computer has picked the Blueface object. The blueface guys always roll across the ground. To make sure they start off at ground level, we manually set the y value to -2.0f. if (object[num].texid==0) // Blue Face object[num].y=-2.0f; // Always Rolling On The Ground Next we check to see if texid is 1. If so, the computer has selected the Bucket. The bucket doesn't travel from left to right, it falls from the sky. The first thing we have to do is set dir to 3. This tells the computer that our bucket is falling or moving down. Our initial code assumes the object will be travelling from left to right. Because the bucket is falling down, we have to give it a new random x value. If we didn't, the bucket would never be visible. It would fall either far off the left side of the screen or far off the right side of the screen. To assign a new value we randomly choose a value based on the distance into the screen. Instead of subtracting 15, we only subtract 10. This gives us a little less range, and keeps the object ON the screen instead of off the side of the screen. Assuming our distance was -30.0f, we would end up with a random value from 0.0f to 40.0f. If you're asking yourself, why from 0.0f to 40.0f? Shouldn't it be from 0.0f to -40.0f? The answer is easy. The rand() function always returns a positive number. So whatever number we get back will be a positive value. Anyways... back to the story. So we have a positive number from 0.0f to 40.0f. We then add the distance (a negative value) minus 10.0f divided by 2. As an example... assuming the random value returned is say 15 and the distance is -30.0f: object[num].x=float(rand()%int(-30.0f-10.0f))+((-30.0f-10.0f)/2.0f); object[num].x=float(rand()%int(-40.0f)+(-40.0f)/2.0f); object[num].x=float(15 {assuming 15 was returned))+(-20.0f); object[num].x=15.0f-20.0f; object[num].x=-5.0f; The last thing we have to do is set the y value. We want the bucket to drop from the sky. We don't want it falling through the clouds though. So we set the y value to 4.5f. Just a little below the clouds. if (object[num].texid==1) // Bucket { object[num].dir=3; // Falling Down object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f); object[num].y=4.5f; // Random X, Start At Top Of The Screen } We want the target to pop out of the ground and up into the air. We check to make sure the object is indeed a target (texid is 2). If so, we set the direction (dir) to 2 (up). We use the exact same code as above to get a random x location. We don't want the target to start above ground. So we set it's initial y value to -3.0f (under the ground). We then subtract a random value from 0.0f to 5 multiplied by the current level. We do this so that the target doesn't INSTANTLY appear. On higher levels we want a delay before the target appears. Without a delay, the targets would pop out one after another, giving you very little time to hit them. if (object[num].texid==2) // Target { object[num].dir=2; // Start Off Flying Up object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f); object[num].y=-3.0f-float(rand()%(5*level)); // Random X, Start Under Ground + Random Value } All of the other objects travel from left to right, so there is no need to assign any values to the remaining objects. They should work just fine with the random values they were assigned. Now for the fun stuff! "For the alpha blending technique to work correctly, the transparent primitives must be drawn in back to front order and must not intersect". When drawing alpha blended objects, it is very important that objects in the distance are drawn first, and objects up close are drawn last. The reason is simple... The Z buffer prevents OpenGL from drawing pixels that are behind things that have already been drawn. So what ends up happening is objects drawn behind transparent objects do not show up. What you end up seeing is a square shape around overlapping objects... Not pretty! We already know the depth of each object. So after initializing a new object, we can get around this problem by sorting the objects using the qsort function (quick sort). By sorting the objects, we can be sure that the first object drawn is the object furthest away. That way when we draw the objects, starting at the first object, the objects in the distance will be drawn first. Objects that are closer (drawn later) will see the previously drawn objects behind them, and will blend properly! As noted in the line comments I found this code in the MSDN after searching the net for hours looking for a solution. It works good and allows you to sort entire structures. qsort takes 4 parameters. The first parameter points to the object array (the array to be sorted). The second parameter is the number of arrays we want to sort... of course we want to sort through all the object currently being displayed (which is level). The third parameter specifies the size of our objects structure and the fourth parameter points to our Compare() function. There is probably a better way to sort structures, but qsort() works... It's quick, convenient and easy to use! It's important to note, that if you wanted to use the glAlphaFunc() and glEnable(GL_ALPHA_TEST), sorting is not necessary. However, using the Alpha Function you are restricted to completely transparent or completely opaque blending, there is no in between. Sorting and using the Blendfunc() is a little more work, but it allows for semi-transparent objects. // Sort Objects By Distance: Beginning Address Of Our object Array *** MSDN CODE MODIFIED FOR THIS TUT *** // Number Of Elements To Sort // Size Of Each Element // Pointer To Our Compare Function qsort((void *) &object, level, sizeof(struct objects), (compfn)Compare ); } The init code is same as always. The first two lines grab information about our window and our keyboard handler. We then use srand() to create a more random game based on the time. After that we load our TGA images and convert them to textures using LoadTGA(). The first 5 images are objects that will streak across the screen. Explode is our explosion animation, ground and sky make up the background scene, crosshair is the crosshair you see on the screen representing your current mouse location, and finally, the font image is the font used to display the score, title, and morale. If any of the images fail to load FALSE is returned, and the program shuts down. It's important to note that this base code will not return an INIT FAILED error message. BOOL Initialize (GL_Window* window, Keys* keys) // Any OpenGL Initialization Goes Here { g_window = window; g_keys = keys; srand( (unsigned)time( NULL ) ); // Randomize Things if ((!LoadTGA(&textures[0],"Data/BlueFace.tga")) || // Load The BlueFace Texture (!LoadTGA(&textures[1],"Data/Bucket.tga")) || // Load The Bucket Texture (!LoadTGA(&textures[2],"Data/Target.tga")) || // Load The Target Texture (!LoadTGA(&textures[3],"Data/Coke.tga")) || // Load The Coke Texture (!LoadTGA(&textures[4],"Data/Vase.tga")) || // Load The Vase Texture (!LoadTGA(&textures[5],"Data/Explode.tga")) || // Load The Explosion Texture (!LoadTGA(&textures[6],"Data/Ground.tga")) || // Load The Ground Texture (!LoadTGA(&textures[7],"Data/Sky.tga")) || // Load The Sky Texture (!LoadTGA(&textures[8],"Data/Crosshair.tga")) || // Load The Crosshair Texture (!LoadTGA(&textures[9],"Data/Font.tga"))) // Load The Crosshair Texture { return FALSE; // If Loading Failed, Return False } If all of the images loaded and were successfully turned into textures, we can continue with initialization. The font texture is loaded, so it's safe to build our font. We do this by jumping to BuildFont(). We then set up OpenGL. The background color is set to black, the alpha is also set to 0.0f. The depth buffer is set up and enabled with less than or equal testing. The glBlendFunc() is a VERY important line of code. We set the blend function to (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). This blends the object with what's on the screen using the alpha values stored in the objects texture. After setting the blend mode, we enable blending. We then enable 2D texture mapping, and finally, we enable GL_CULL_FACE. This removes the back face from each object ( no point in wasting cycles drawing something we can't see ). We draw all of our quads with a counter clockwise winding so the proper face is culled. Earlier in the tutorial I talked about using the glAlphaFunc() instead of alpha blending. If you want to use the Alpha Function, comment out the 2 lines of blending code and uncomment the 2 lines under glEnable(GL_BLEND). You can also comment out the qsort() function in the InitObject() section of code. The program should run ok, but the sky texture will not be there. The reason is because the sky texture has an alpha value of 0.5f. When I was talking about the Alpha Function earlier on, I mentioned that it only works with alpha values of 0 or 1. You will have to modify the alpha channel for the sky texture if you want it to appear! Again, if you decide to use the Alpha Function instead, you don't have to sort the objects. Both methods have the good points! Below is a quick quote from the SGI site: "The alpha function discards fragments instead of drawing them into the frame buffer. Therefore sorting of the primitives is not necessary (unless some other mode like alpha blending is enabled). The disadvantage is that pixels must be completely opaque or completely transparent". BuildFont(); // Build Our Font Display List glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Black Background glClearDepth(1.0f); // Depth Buffer Setup glDepthFunc(GL_LEQUAL); // Type Of Depth Testing glEnable(GL_DEPTH_TEST); // Enable Depth Testing glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Enable Alpha Blending (disable alpha testing) glEnable(GL_BLEND); // Enable Blending (disable alpha testing) // glAlphaFunc(GL_GREATER,0.1f); // Set Alpha Testing (disable blending) // glEnable(GL_ALPHA_TEST); // Enable Alpha Testing (disable blending) glEnable(GL_TEXTURE_2D); // Enable Texture Mapping glEnable(GL_CULL_FACE); // Remove Back Face At this point in the program, none of the objects have been defined. So we loop through all thirty objects calling InitObject() for each object. for (int loop=0; loop<30; loop++) // Loop Through 30 Objects InitObject(loop); // Initialize Each Object return TRUE; // Return TRUE (Initialization Successful) } In our init code, we called BuildFont() which builds our 95 display lists. The following line of code deletes all 95 display lists before the program quits. void Deinitialize (void) // Any User DeInitialization Goes Here { glDeleteLists(base,95); // Delete All 95 Font Display Lists } Now for the tricky stuff... The code that does the actual selecting of the objects. The first line of code below allocates a buffer that we can use to store information about our selected objects into. The variable hits will hold the number of hits detected while in selection mode. void Selection(void) // This Is Where Selection Is Done { GLuint buffer[512]; // Set Up A Selection Buffer GLint hits; // The Number Of Objects That We Selected In the code below, we check to see if the game is over (FALSE). If it is, there is no point in selecting anything, so we return (exit). If the game is still active (TRUE), we play a gunshot sound using the Playsound() command. The only time Selection() is called is when the mouse button has been pressed, and every time the button is pressed, we want to play the gunshot sound. The sound is played in async mode so that it doesn't halt the program while the sound is playing. if (game) // Is Game Over? return; // If So, Don't Bother Checking For Hits PlaySound("data/shot.wav",NULL,SND_ASYNC); // Play Gun Shot Sound Now we set up a viewport. viewport[] will hold the current x, y, length and width of the current viewport (OpenGL Window). glGetIntegerv(GL_VIEWPORT, viewport) gets the current viewport boundries and stores them in viewport[]. Initially, the boundries are equal the the OpenGL window dimensions. glSelectBuffer(512, buffer) tells OpenGL to use buffer for it's selection buffer. // The Size Of The Viewport. [0] Is <x>, [1] Is <y>, [2] Is <length>, [3] Is <width> GLint viewport[4]; // This Sets The Array <viewport> To The Size And Location Of The Screen Relative To The Window glGetIntegerv(GL_VIEWPORT, viewport); glSelectBuffer(512, buffer); // Tell OpenGL To Use Our Array For Selection All of the code below is very important. The first line puts OpenGL in selection mode. In selection mode, nothing is drawn to the screen. Instead, information about objects rendered while in selection mode will be stored in the selection buffer. Next we initialize the name stack by calling glInitNames() and glPushName(0). It's important to note that if the program is not in selection mode, a call to glPushName() will be ignored. Of course we are in selection mode, but it's something to keep in mind. // Puts OpenGL In Selection Mode. Nothing Will Be Drawn. Object ID's and Extents Are Stored In The Buffer. (void) glRenderMode(GL_SELECT); glInitNames(); // Initializes The Name Stack glPushName(0); // Push 0 (At Least One Entry) Onto The Stack After preparing the name stack, we have to to restrict drawing to the area just under our crosshair. In order to do this we have to select the projection matrix. After selecting the projection matrix we push it onto the stack. We then reset the projection matrix using glLoadIdentity(). We restrict drawing using gluPickMatrix(). The first parameter is our current mouse position on the x-axis, the second parameter is the current mouse position on the y-axis, then the width and height of the picking region. Finally the current viewport[]. The viewport[] indicates the current viewport boundaries. mouse_x and mouse_y will be the center of the picking region. glMatrixMode(GL_PROJECTION); // Selects The Projection Matrix glPushMatrix(); // Push The Projection Matrix glLoadIdentity(); // Resets The Matrix // This Creates A Matrix That Will Zoom Up To A Small Portion Of The Screen, Where The Mouse Is. gluPickMatrix((GLdouble) mouse_x, (GLdouble) (viewport[3]-mouse_y), 1.0f, 1.0f, viewport); Calling gluPerspective() multiplies the perspective matrix by the pick matrix which restricts the drawing to the area requested by gluPickMatrix(). We then switch to the modelview matrix and draw our targets by calling DrawTargets(). We draw the targets in DrawTargets() and not in Draw() because we only want selection to check for hits with objects (targets) and not the sky, ground or crosshair. After drawing our targets, we switch back to the projection matrix and pop the stored matrix off the stack. We then switch back to the modelview matrix. The last line of code below switches back to render mode so that objects we draw actually appear on the screen. hits will hold the number of objects that were rendered in the viewing area requested by gluPickMatrix(). // Apply The Perspective Matrix gluPerspective(45.0f, (GLfloat) (viewport[2]-viewport[0])/(GLfloat) (viewport[3]-viewport[1]), 0.1f, 100.0f); glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix DrawTargets(); // Render The Targets To The Selection Buffer glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glPopMatrix(); // Pop The Projection Matrix glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix hits=glRenderMode(GL_RENDER); // Switch To Render Mode, Find Out How Many Now we check to see if there were more than 0 hits recorded. If so, we set choose to equal the name of the first object drawn into the picking area. depth holds how deep into the screen, the object is. Each hit takes 4 items in the buffer. The first item is the number of names on the name stack when the hit occured. The second item is the minimum z value of all the verticies that intersected the viewing area at the time of the hit. The third item is the maximum z value of all the vertices that intersected the viewing area at the time of the hit and the last item is the content of the name stack at the time of the hit (name of the object). We are only interested in the minimum z value and the object name in this tutorial. if (hits > 0) // If There Were More Than 0 Hits { int choose = buffer[3]; // Make Our Selection The First Object int depth = buffer[1]; // Store How Far Away It Is We then loop through all of the hits to make sure none of the objects are closer than the first object hit. If we didn't do this, and two objects were overlapping, the first object hit might behind another object, and clicking the mouse would take away the first object, even though it was behind another object. When you shoot at something, the closest object should be the object that gets hit. So, we check through all of the hits. Remember that each object takes 4 items in the buffer, so to search through each hit we have to multiply the current loop value by 4. We add 1 to get the depth of each object hit. If the depth is less than the the current selected objects depth, we store the name of the closer object in choose and we store the depth of the closer object in depth. After we have looped through all of our hits, choose will hold the name of the closest object hit, and depth will hold the depth of the closest object hit. for (int loop = 1; loop < hits; loop++) // Loop Through All The Detected Hits { // If This Object Is Closer To Us Than The One We Have Selected if (buffer[loop*4+1] < GLuint(depth)) { choose = buffer[loop*4+3]; // Select The Closer Object depth = buffer[loop*4+1]; // Store How Far Away It Is } } All we have to do is mark the object as being hit. We check to make sure the object has not already been hit. If it has not been hit, we mark it as being hit by setting hit to TRUE. We increase the players score by 1 point, and we increase the kills counter by 1. if (!object[choose].hit) // If The Object Hasn't Already Been Hit { object[choose].hit=TRUE; // Mark The Object As Being Hit score+=1; // Increase Score kills+=1; // Increase Level Kills I use kills to keep track of how many objects have been destroyed on each level. I wanted each level to have more objects (making it harder to get through the level). So I check to see if the players kills is greater than the current level multiplied by 5. On level 1, the player only has to kill 5 objects (1*5). On level 2 the player has to kill 10 objects (2*5), progressively getting harder each level. So, the first line of code checks to see if kills is higher than the level multiplied by 5. If so, we set miss to 0. This sets the player morale back to 10 out of 10 (the morale is 10-miss). We then set kills to 0 (which starts the counting process over again). Finally, we increase the value of level by 1 and check to see if we've hit the last level. I have set the maximum level to 30 for the following two reasons... Level 30 is insanely difficult. I am pretty sure no one will ever have that good of a game. The second reason... At the top of the code, we only set up 30 objects. If you want more objects, you have to increase the value accordingly. It is VERY important to note that you can have a maximum of 64 objects on the screen (0-63). If you try to render 65 or more objects, picking becomes confused, and odd things start to happen. Everything from objects randomly exploding to your computer crashing. It's a physical limit in OpenGL (just like the 8 lights limit). If by some chance you are a god, and you finish level 30, the level will no longer increase, but your score will. Your morale will also reset to 10 every time you finish the 30th level. if (kills>level*5) // New Level Yet? { miss=0; // Misses Reset Back To Zero kills=0; // Reset Level Kills level+=1; // Increase Level if (level>30) // Higher Than 30? level=30; // Set Level To 30 (Are You A God?) } } } } Update() is where I check for key presses, and update object movement. One of the nice things about Update() is the milliseconds timer. You can use the milliseconds timer to move objects based on the amount of time that has passed since Update() was last called. It's important to note that moving object based on time keeps the objects moving at the same speed on any processor... BUT there are drawbacks! Lets say you have an object moving 5 units in 10 seconds. On a fast system, the computer will move the object half a unit every second. On a slow system, it could be 2 seconds before the update procedure is even called. So when the object moves, it will appear to skip a spot. The animation will not be as smooth on a slower system. (Note: this is just an exaggerated example... computers update ALOT faster than once every two seconds). Anyways... with that out of the way... on to the code. The code below checks to see if the escape key is being pressed. If it is, we quit the application by calling TerminateApplication(). g_window holds the information about our window. void Update(DWORD milliseconds) // Perform Motion Updates Here { if (g_keys->keyDown[VK_ESCAPE]) // Is ESC Being Pressed? { TerminateApplication (g_window); // Terminate The Program } The code below checks to see if the space bar is pressed and the game is over. If both conditions are true, we initialize all 30 object (give them new directions, textures, etc). We set game to FALSE, telling the program the game is no longer over. We set the score back to 0, the level back to 1, the player kills to 0 and finally we set the miss variable back to zero. This restarts the game on the first level with full morale and a score of 0. if (g_keys->keyDown[' '] && game) // Space Bar Being Pressed After Game Has Ended? { for (int loop=0; loop<30; loop++) // Loop Through 30 Objects InitObject(loop); // Initialize Each Object game=FALSE; // Set game (Game Over) To False score=0; // Set score To 0 level=1; // Set level Back To 1 kills=0; // Zero Player Kills miss=0; // Set miss (Missed Shots) To 0 } The code below checks to see if the F1 key has been pressed. If F1 is being pressed, ToggleFullscreen will switch from windowed to fullscreen mode or fullscreen mode to windowed mode. if (g_keys->keyDown[VK_F1]) // Is F1 Being Pressed? { ToggleFullscreen (g_window); // Toggle Fullscreen Mode } To create the illusion of rolling clouds and moving ground, we decrease roll by .00005f multiplied by the number of milliseconds that have passed. This keeps the clouds moving at the same speed on all systems (fast or slow). We then set up a loop to loop through all of the objects on the screen. Level 1 has one object, level 10 has 10 objects, etc. roll-=milliseconds*0.00005f; // Roll The Clouds for (int loop=0; loop<level; loop++) // Loop Through The Objects { We need to find out which way the object should be spinning. We do this by checking the value of rot. If rot equals 1, we need to spin the object clockwise. To do this, we decrease the value of spin. We decrease spin by 0.2f multiplied by value of loop plus the number of milliseconds that have passed. By using milliseconds the objects will rotate the same speed on all systems. Adding loop makes each NEW object spin a little faster than the last object. So object 2 will spin faster than object 1 and object 3 will spin faster than object 2. if (object[loop].rot==1) // If Rotation Is Clockwise object[loop].spin-=0.2f*(float(loop+milliseconds)); // Spin Clockwise Next we check to see if rot equals 2. If rot equals 2, we need to spin counter clockwise. The only difference from the code above is that we are increasing the value of spin instead of decreasing it. This causes the object to spin in the opposite direction. if (object[loop].rot==2) // If Rotation Is Counter Clockwise object[loop].spin+=0.2f*(float(loop+milliseconds)); // Spin Counter Clockwise Now for the movement code. We check the value of dir if it's equal to 1, we increase the objects x value based on the milliseconds passed multiplied by 0.012f. This moves the object right. Because we use milliseconds the objects should move the same speed on all systems. if (object[loop].dir==1) // If Direction Is Right object[loop].x+=0.012f*float(milliseconds); // Move Right If dir equals 0, the object is moving left. We move the object left by decreasing the objects x value. Again we decrease x based on the amount of time that has passed in milliseconds multiplied by our fixed value of 0.012f. if (object[loop].dir==0) // If Direction Is Left object[loop].x-=0.012f*float(milliseconds); // Move Left Only two more directions to watch for. This time we check to see if dir equals 2. If so, we increase the objects y value. This causes the object to move UP the screen. Keep in mind the positive y axis is at the top of the screen and the negative y axis is at the bottom. So increasing y moves from the bottom to the top. Again movement is based on time passed. if (object[loop].dir==2) // If Direction Is Up object[loop].y+=0.012f*float(milliseconds); // Move Up The last direction our object can travel is down. If dir equals three, we want to move the object down the screen. We do this by increasing the objects y value based on the amount of time that has passed. Notice we move down slower than we move up. When an object is falling, our fixed falling rate is 0.0025f. When we move up, the fixed rate is 0.012f. if (object[loop].dir==3) // If Direction Is Down object[loop].y-=0.0025f*float(milliseconds); // Move Down After moving our objects we have to check if they are still in view. The code below first checks to see where our object is on the screen. We can roughly calculate how far left an object can travel by taking the objects distance into the screen minus 15.0f (to make sure it's a little past the screen) and dividing it by 2. For those of you that don't already know... If you are 20 units into the screen, depending on the way you set up the perspective, you have roughly 10 units from the left of the screen to the center and 10 from the center to the right. so -20.0f(distance)-15.0f(extra padding)=-35.0f... divide that by 2 and you get -17.5f. That's roughly 7.5 units off the left side of the screen. Meaning our object is completely out of view. Anyways... after making sure the object is far off the left side of the screen, we check to see if it was moving left (dir=0). If it's not moving left, we don't care if it's off the left side of the screen! Finally, we check to see if the object was hit. If the object is off the left of the screen, it's travelling left and it wasn't hit, it's too late for the player to hit it. So we increase the value of miss. This lowers morale and increases the number of missed targets. We set the objects hit value to TRUE so the computer thinks it's been hit. This forces the object to self destruct (allowing us to give the object a new texture, directions, spin, etc). // If We Are To Far Left, Direction Is Left And The Object Was Not Hit if ((object[loop].x<(object[loop].distance-15.0f)/2.0f) && (object[loop].dir==0) && !object[loop].hit) { miss+=1; // Increase miss (Missed Object) object[loop].hit=TRUE; // Set hit To True To Manually Blow Up The Object } The following code does the exact same thing as the code above, but instead of checking to see if we've gone off the left side of the screen, we check to see if it's gone off the right side of the screen. We also check to make sure the object is moving right and not some other direction. If the object is off the screen, we increase the value of miss and self destruct the object by telling our program it's been hit. // If We Are To Far Right, Direction Is Left And The Object Was Not Hit if ((object[loop].x>-(object[loop].distance-15.0f)/2.0f) && (object[loop].dir==1) && !object[loop].hit) { miss+=1; // Increase miss (Missed Object) object[loop].hit=TRUE; // Set hit To True To Manually Blow Up The Object } The falling code is pretty straight forward. We check to see if the object has just about hit the ground. We don't want it to fall through the ground which is at -3.0f. Instead, we check to see if the object is below -2.0f. We then check to make sure the object is indeed falling (dir=3) and that the object has not yet been hit. If the object is below -2.0f on the y axis, we increase miss and set the objects hit variable to TRUE (causing it to self destruct as it hits the ground... nice effect). // If We Are To Far Down, Direction Is Down And The Object Was Not Hit if ((object[loop].y<-2.0f) && (object[loop].dir==3) && !object[loop].hit) { miss+=1; // Increase miss (Missed Object) object[loop].hit=TRUE; // Set hit To True To Manually Blow Up The Object } Unlike the previous code, the going up code is a little different. We don't want the object to go through the clouds! We check to see if the objects y variable is greater than 4.5f (close to the clouds). We also make sure the object is travelling up (dir=2). If the objects y value is greater than 4.5f, instead of destroying the object, we change it's direction. That way the object will quickly pop out of the ground (remember, it goes up faster than it comes down) and once it gets to high we change its direction so it starts to fall toward the ground. There is no need to destroy the object, or increase the miss variable. If you miss the object as it's flying into the sky, there's always a chance to hit it as it falls. The falling code will handle the final destruction of the object. if ((object[loop].y>4.5f) && (object[loop].dir==2)) // If We Are To Far Up And The Direction Is Up object[loop].dir=3; // Change The Direction To Down } } Next we have the object drawing code. I wanted a quick and easy way to draw the game objects, along with the crosshair with as little code as possible. Object takes 3 parameters. First we have the width. The width controls how wide the object will be when it's drawn. Then we have the height. The height controls how tall the object will be when it's drawn. Finally, we have the texid. The texid selects the texture we want to use. If we wanted to draw a bucket, which is texture 1, we would pass a value of 1 for the texid. Pretty simple! A quick breakdown. We select the texture, and then draw a quad. We use standard texture coordinates so the entire textue is mapped to the face of the quad. The quad is drawn in a counter-clockwise direction (required for culling to work). void Object(float width,float height,GLuint texid) // Draw Object Using Requested Width, Height And Texture { glBindTexture(GL_TEXTURE_2D, textures[texid].texID); // Select The Correct Texture glBegin(GL_QUADS); // Start Drawing A Quad glTexCoord2f(0.0f,0.0f); glVertex3f(-width,-height,0.0f); // Bottom Left glTexCoord2f(1.0f,0.0f); glVertex3f( width,-height,0.0f); // Bottom Right glTexCoord2f(1.0f,1.0f); glVertex3f( width, height,0.0f); // Top Right glTexCoord2f(0.0f,1.0f); glVertex3f(-width, height,0.0f); // Top Left glEnd(); // Done Drawing Quad } The explosion code takes one parameter. num is the object identifier. In order to create the explosion we need to grab a portion of the explosion texture similar to the way we grab each letter from the font texture. The two lines below calculate the column (ex) and row (ey) from a single number (frame). The first line below grabs the current frame and divides it by 4. The division by 4 is to slow down the animation. %4 keeps the value in the 0-3 range. If the value is higher than 3 it would wrap around and become 0. If the value is 5 it would become 1. A value of 9 would be 0,1,2,3,0,1,2,3,0. We divide the final result by 4.0f because texture coordinates are in the 0.0f to 1.0f range. Our explosion texture has 4 explosion images from left to right and 4 up and down. Hopefully you're not completely confused. So if our number before division can only be 0,1,2 or 3 our number after we divide it by 4.0f can only be 0.0f, 0.25f (1/4), 0.50f (2/4) or 0.75f (3/4). This gives us our left to right texture coordinate (ex). Next we calculate the row (ey). We grab the current object frame and divide it by 4 to slow the animation down a little. We then divide by 4 again to eliminate an entire row. Finally we divide by 4 one last time to get our vertical texture coordinate. A quick example. If our current frame was 16. ey=((16/4)/4)/4 or 4/4/4 or 0.25f. One row down. If our current frame was 60. ey=((60/4)/4)/4 or 15/4/4 or 3/4 or 0.75f. The reason 15/4 isn't 3.75 is because we are working with integers up until we do the final division. With that in mind, the value of ey can only be one of 4 values... 0.0f, 0.25f, 0.50f or 0.75f. Assuming we stay inside our texture (prevent frame from going over a value of 63). Hope that made sense... it's simple, but intimidating math. void Explosion(int num) // Draws An Animated Explosion For Object "num" { float ex = (float)((object[num].frame/4)%4)/4.0f; // Calculate Explosion X Frame (0.0f - 0.75f) float ey = (float)((object[num].frame/4)/4)/4.0f; // Calculate Explosion Y Frame (0.0f - 0.75f) Now that we have the texture coordinates, all that's left to do is draw our textured quad. The vertex coordinates are fixed at -1.0f and 1.0f. You will notice we subract ey from 1.0f. If we didn't, the animation would be drawn in the reverse order... The explosion would get bigger, rather than fade out. The effect won't look right! We bind the explosion texture before we draw the textured quad. Again, the quad is drawn counter-clockwise. glBindTexture(GL_TEXTURE_2D, textures[5].texID); // Select The Explosion Texture glBegin(GL_QUADS); // Begin Drawing A Quad glTexCoord2f(ex ,1.0f-(ey )); glVertex3f(-1.0f,-1.0f,0.0f); // Bottom Left glTexCoord2f(ex+0.25f,1.0f-(ey )); glVertex3f( 1.0f,-1.0f,0.0f); // Bottom Right glTexCoord2f(ex+0.25f,1.0f-(ey+0.25f)); glVertex3f( 1.0f, 1.0f,0.0f); // Top Right glTexCoord2f(ex ,1.0f-(ey+0.25f)); glVertex3f(-1.0f, 1.0f,0.0f); // Top Left glEnd(); // Done Drawing Quad As I mentioned above, the value of frame should not exceed 63 otherwise the animation will start over again. So we increase the value of frame and then we check to see if the value is greater than 63. If it is, we call InitObject(num) which destroys the object and gives it new values to create an entirely new object. object[num].frame+=1; // Increase Current Explosion Frame if (object[num].frame>63) // Have We Gone Through All 16 Frames? { InitObject(num); // Init The Object (Assign New Values) } } This section of code draws all of the targets (objects) to the screen. We start off by resetting the modelview matrix. We then translate 10 units into the screen and set up a loop from 0 to the players current level. void DrawTargets(void) // Draws The Targets (Needs To Be Seperate) { glLoadIdentity(); // Reset The Modelview Matrix glTranslatef(0.0f,0.0f,-10.0f); // Move Into The Screen 20 Units for (int loop=0; loop<level; loop++) // Loop Through 9 Objects { The first line of code is the secret to picking individual objects. What it does is assigns a name (number) to each object. The first object drawn will be 0. The second object will be 1, etc... If the loop was to hit 29, the last object drawn would be given the name 29. After assigning a name to the object, we push the modelview matrix onto the stack. It's important to note the calls to glLoadName() are ignored if the program is not in selection mode. We then move to the location on the screen where we want our object to be drawn. We use object[loop].x to position on the x-axis, object[loop].y to position on the y-axis and object[loop].distance to position the object on the z-axis (depth into the screen). We have already translated 10 units into the screen, so the actual distance at which the object will be drawn is going to be object[loop].distance-10.0f. glLoadName(loop); // Assign Object A Name (ID) glPushMatrix(); // Push The Modelview Matrix glTranslatef(object[loop].x,object[loop].y,object[loop].distance); // Position The Object (x,y) Before we draw the object, we have to check if it's been hit or not. We do this by checking to see if object[loop].hit is TRUE. If it is, we jump to Explosion(loop) which will draw the explosion animation instead of the actual object. If the object was not hit, we spin the object on it's z-axis by object[loop].spin degrees before we call Object(). Object takes 3 parameters. The first one is the width, the second one is the height and the third one is the number of the texture to use. To get the width and height, we use the array size[object[loop].texid].w and size[object[loop].texid].h. This looks up the width and height from our predefined object size array at the beginning of this program. The reason we use object[loop].texid is because it represents the type of object we are drawing. A texid of 0 is always the blueface... a texid of 3 is always the coke can, etc. After drawing an object, we pop the matrix resetting the view, so our next object is drawn at the proper location on the screen. if (object[loop].hit) // If Object Has Been Hit { Explosion(loop); // Draw An Explosion } else // Otherwise { glRotatef(object[loop].spin,0.0f,0.0f,1.0f); // Rotate The Object Object(size[object[loop].texid].w,size[object[loop].texid].h,object[loop].texid); // Draw The Object } glPopMatrix(); // Pop The Modelview Matrix } } This is where the drawing occurs. We start off by clearing the screen, and resetting our modelview matrix. void Draw(void) // Draw Our Scene { glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer glLoadIdentity(); // Reset The Modelview Matrix Next we push the modelview matrix onto the stack and select the sky texture (texture 7). The sky is made up of 4 textured quads. The first 4 vertices draw the sky way in the distance from the ground straight up. The texture on this quad will roll fairly slowly. The next 4 vertices draw the sky again at the exact same location but the sky texture will roll faster. The two textures will blend together in alpha blending mode to create a neat multilayered effect. glPushMatrix(); // Push The Modelview Matrix glBindTexture(GL_TEXTURE_2D, textures[7].texID); // Select The Sky Texture glBegin(GL_QUADS); // Begin Drawing Quads glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f); // Top Right glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f); // Top Left glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f); // Bottom Left glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f); // Bottom Right glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f); // Top Right glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f); // Top Left glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f); // Bottom Left glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f); // Bottom Right To give the illusion that the sky is coming towards the viewer, we draw two more quads, but this time we draw them from way in the distance coming toward the viewer. The first 4 verticies draw slow rolling clouds and the remaining 4 draw faster moving clouds. The two layers will blend together in alpha blending mode to create a multilayered effect. The second layer of clouds is offset by 0.5f so that the two textures don't line up. Same with the two layers of clouds above. The second layer is offset by 0.5f. The final effect of all 4 quads is a sky that appears to move up way out in the distance and then toward the viewer up high. I could have used a textured half sphere for the sky, but I was too lazy, and the effect is still pretty good as is. glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,0.0f); // Top Right glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,0.0f); // Top Left glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f); // Bottom Left glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f); // Bottom Right glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,0.0f); // Top Right glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,0.0f); // Top Left glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f); // Bottom Left glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f); // Bottom Right glEnd(); // Done Drawing Quads With the sky out of the way, it's time to draw the ground. We draw the ground starting where the sky texture is the lowest coming towards the viewer. The ground texture rolls at the same speed as the fast moving clouds. The texture is repeated 7 times from left to right and 4 times from back to front to add a little more detail and to prevent the texture from getting all blocky looking. This is done by increasing the texture coordinates from 0.0f - 1.0f to 0.0f - 7.0f (left to right) and 0.0f - 4.0f (up and down). glBindTexture(GL_TEXTURE_2D, textures[6].texID); // Select The Ground Texture glBegin(GL_QUADS); // Draw A Quad glTexCoord2f(7.0f,4.0f-roll); glVertex3f( 27.0f,-3.0f,-50.0f); // Top Right glTexCoord2f(0.0f,4.0f-roll); glVertex3f(-27.0f,-3.0f,-50.0f); // Top Left glTexCoord2f(0.0f,0.0f-roll); glVertex3f(-27.0f,-3.0f,0.0f); // Bottom Left glTexCoord2f(7.0f,0.0f-roll); glVertex3f( 27.0f,-3.0f,0.0f); // Bottom Right glEnd(); // Done Drawing Quad After drawing the sky and the ground, we jump to the section of code that draws all of our targets (objects) called none other than DrawTargets(). After drawing out targets, we pop the modelview matrix off the stack (restoring it to it's previous state). DrawTargets(); // Draw Our Targets glPopMatrix(); // Pop The Modelview Matrix The code below draws the crosshair. We start off by grabbing our current window dimensions. We do this in case the window was resized in windowed mode. GetClientRect grabs the dimensions and stores them in window. We then select our projection matrix and push it onto the stack. We reset the view with glLoadIdentity() and then set the screen up in ortho mode instead of perspective. The window will go from 0 to window.right from left to right, and from 0 to window.bottom from the bottom to the top of the screen. The third parameter of glOrtho() is supposed to be the bottom value, instead I swapped the bottom and top values. I did this so that the crosshair would be rendered in a counter clockwise direction. With 0 at the top and window.bottom at the bottom, the winding would go the opposite direction and the crosshair and text would not appear. After setting up the ortho view, we select the modelview matrix, and position the crosshair. Because the screen is upside down, we have to invert the mouse as well. Otherwise our crosshair would move down if we moved the mouse up and up if we moved the mouse down. To do this we subtract the current mouse_y value from the bottom of the window (window.bottom). After translating to the current mouse position, we draw the crosshair. We do this by calling Object(). Instead of units, we are going to specify the width and height in pixels. The crosshair will be 16x16 pixels wide and tall and the texture used to draw the object is texture 8 (the crosshair texture). I decided to use a custom cursor for two reasons... first and most important, it looks cool, and it can be modified using any art program that supports the alpha channel. Secondly, some video cards do not display a cursor in fullscreen mode. Playing the game without a cursor in fullscreen mode is not easy :) // Crosshair (In Ortho View) RECT window; // Storage For Window Dimensions GetClientRect (g_window->hWnd,&window); // Get Window Dimensions glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glPushMatrix(); // Store The Projection Matrix glLoadIdentity(); // Reset The Projection Matrix glOrtho(0,window.right,0,window.bottom,-1,1); // Set Up An Ortho Screen glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix glTranslated(mouse_x,window.bottom-mouse_y,0.0f); // Move To The Current Mouse Position Object(16,16,8); // Draw The Crosshair This section of code put the title at the top of the screen, and displays the level and score in the bottom left and right corners of the screen. The reason I put this code here is because it's easier to position the text accurately in ortho mode. // Game Stats / Title glPrint(240,450,"NeHe Productions"); // Print Title glPrint(10,10,"Level: %i",level); // Print Level glPrint(250,10,"Score: %i",score); // Print Score This section checks to see if the player has missed more than 10 objects. If so, we set the number of misses (miss) to 9 and we set game to TRUE. Setting the game to TRUE means the game is over! if (miss>9) // Have We Missed 10 Objects? { miss=9; // Limit Misses To 10 game=TRUE; // Game Over TRUE } In the code below, we check to see if game is TRUE. If game is TRUE, we print the GAME OVER messages. If game is false, we print the players morale (out of 10). The morale is calculated by subtracting the players misses (miss) from 10. The more the player misses, the lower his morale. if (game) // Is Game Over? glPrint(490,10,"GAME OVER"); // Game Over Message else glPrint(490,10,"Morale: %i/10",10-miss); // Print Morale #/10 The last thing we do is select the projection matrix, restore (pop) our matrix back to it's previous state, set the matrix mode to modelview and flush the buffer to make sure all objects have been rendered. glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glPopMatrix(); // Restore The Old Projection Matrix glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix glFlush(); // Flush The GL Rendering Pipeline } This tutorial is the result of many late nights, and many many hours of coding & writing HTML. By the end of this tutorial you should have a good understanding of how picking, sorting, alpha blending and alpha testing works. Picking allows you to create interactive point and click software. Everything from games, to fancy GUI's. The best feature of picking is that you don't have to keep track of where your objects are. You assign a name and check for hits. It's that easy! With alpha blending and alpha testing you can make your objects completely solid, or full of holes. The results are great, and you don't have to worry about objects showing through your textures, unless you want them to! As always, I hope you have enjoyed this tutorial, and hope to see some cool games, or projects based on code from this tutorial. If you have any questions or you find mistakes in the tutorial please let me know... I'm only human :) I could have spent alot more time adding things like physics, more graphics, more sound, etc. This is just a tutorial though! I didn't write it to impress you with bells and whistles. I wrote it to teach you OpenGL with as little confusion as possible. I hope to see some cool modifications to the code. If you add something cool the the tutorial send me the demo. If it's a cool modification I'll post it to the downloads page. If I get enough modifications I may set up a page dedicated to modified versions of this tutorial! I am here to give you a starting point. The rest is up to you :) NOTE: It is VERY important that in the call to glTexImage2D you set both the format and the internal format to GL_RGBA. Otherwise alpha blending will not work! Jeff Molofee (NeHe) * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Christian Kindahl )
|