Building the Final T3D Game Console
At this point, we have more than enough to implement our virtual graphics interface, including support for sound and input. Moreover, the virtual interface model can almost be implemented one-to-one with the data structures, globals, and functions you just reviewed that make up the three T3DLIB library modules from the first Tricks. Remember, our goal was to implement a double-buffered graphics system that supports 8- and 16-bit windowed and full-screen graphics, and that models our virtual computer interfaces functionality. We have exactly what we need to do that now.
The next step is to create a really solid Game Console "template" based on the alpha version we made a few sections back. However, before we go there, let's take a moment to look at the functionality of the virtual graphics interface, and the exact mapping to the real graphics interface in T3DLIB1.CPP. The sound and music stuff is much easier to grasp, so I'm not going to show the mapping. Moreover, the demos that follow in the final sections of this chapter have numerous examples of sound, music, and input, so you can see the functions in real programs. Even so, taking a look at the graphics part is worth the work, so let's do it.
Mapping the Real Graphics to the Unreal Graphics of the Virtual Interface
Earlier in this chapter, we went through a mental experiment and came up with the most generic graphics interface that we could think of as the minimum necessary to perform software-based 3D graphics on any computer. Now, with the tools of the libraries at hand, let's take a look at what the final mappings are from virtual to real.
Starting the System
In the virtual computer's graphics interface, we assumed that there was a function called Create_Window() with the prototype
Create_Window(int width, int height, int bit_depth);
that would take care of all the details of getting the graphics system ready, along with opening a window of the desired size and bit depth. In our real implementation, this functionality has to be split into two function calls, because we want to try and keep windows and DirectX separate. The first function calls are standard Windows calls to create a standard Windows window.
Now, listen up: If you are creating a full-screen application, you should use the window flags of WM_POPUP, but if you want a windowed application, you should use something like WM_OVERLAPPEDWINDOW. Hence, getting the graphics system ready is a two-step process:
Make a call to the Win32 API function Create_Window() with the appropriate parameters for either a full-screen window or a windowed application. With a full-screen window, you don't usually want any controls.
Call the DirectX wrapper function, along with the handle to the window (which is global) and the appropriate parameters to finish the job.
All the action really happens during step 2. This is where DirectDraw is initialized, the frame buffers are created, the palette for 8-bit modes is generated, and the clippers are attached to the graphics window and back buffer.
Because we are going to use the Game Console template, the plan of attack is to let the WinMain() function open the window up, and then in the call to Game_Init(), make a call to the function DDraw_Init(), which does all the dirty work. If you recall, here's its prototype:
int DDraw_Init(int width, int height, int bpp, int windowed=0);
Simple, huh? Well, just look inside the code to really appreciate all it does. Anyway, to recap, the single call in our virtual computer interface to create a window (in fantasy land) was Create_Window(). In real life, we need two calls to implement this functionality: one to create a Windows window, and the second to initialize DirectDraw and connect it to the window.
Global Mappings
The first things that we designed into our virtual graphics interface were two frame buffers: one visible, and one offscreen. We called these the primary and secondary (back) buffers, as shown in Figure 3.12.
Additionally, we agreed that for any given resolution and bit depth, these buffers would be linearly addressable, with the one constraint that the memory pitch to jump from line to line might not be the same as the pixel pitch, and that there would be variables to track this. The names of the virtual frame buffer pointers and the pitch variables were as follows:
UCHAR *primary_buffer; // the primary buffer int primary_pitch; // the memory pitch in bytes UCHAR *secondary_buffer; // the secondary buffer int seconday_pitch; // the memory pitch in bytes
Figure 3.12 The frame buffers.
In the real graphics library under T3DLIB1.CPP, there is almost an identical mapping. The variables that serve the same purpose are as follows:
LPDIRECTDRAWSURFACE7 lpddsprimary; // dd primary surface LPDIRECTDRAWSURFACE7 lpddsback; // dd back surface UCHAR *primary_buffer; // primary video buffer UCHAR *back_buffer; // secondary back buffer int primary_lpitch; // memory line pitch int back_lpitch; // memory line pitch
You'll notice two extra DirectX-centric variables: lpddsprimary and lpddsback. These are DirectDraw surface pointers to both the primary and secondary (back) surfaces. They are needed for some calls, and hence you should know them. Additionally, the width and height of the primary and secondary buffers are always the same. Although the primary buffer's client area might only be a window and not the entire desktop, the secondary buffer will always be the same size as the client area. Bottom linethe visible primary frame buffer and the invisible secondary frame buffer are always the same size and bit depth.
256-Color Modes
Although you saw the 256-color palette manipulation functions in the graphics API listing of T3DLIB1.CPP|H, I just wanted to reinforce them. The functions that enable you to manipulate the palette are as follows:
int Set_Palette_Entry(int color_index, LPPALETTEENTRY color); int Get_Palette_Entry(int color_index, LPPALETTEENTRY color); int Load_Palette_From_File(char *filename, LPPALETTEENTRY palette); int Save_Palette_To_File(char *filename, LPPALETTEENTRY palette); int Save_Palette(LPPALETTEENTRY sav_palette); int Set_Palette(LPPALETTEENTRY set_palette);
I have highlighted the functions of most interest. They are used to alter a single palette entry, or the entire palette at once. In most cases, it's inefficient to change one palette entry at a time. It's better to make all the changes at once. The data structure you pass is either a pointer to a single PALETTENTRY or to an array of them. Here's the PALETTENTRY data structure to refresh your memory:
typedef struct tagPALETTEENTRY { // pe BYTE peRed; // red channel 8-bits BYTE peGreen; // green channel 8-bits BYTE peBlue; // blue channel 8-bits BYTE peFlags; // flags control, PC_EXPLICIT for the first and last 10 // colors if windowed mode, else use PC_NOCOLLAPSE } PALETTEENTRY;
Finally, if you do select a 256-color mode, the palette data in PALDATA2.PAL will be loaded. This is a palette that I like to use, and it has good color space coverage. You can take a look at it by opening PALDATA2.BMP. Of course, you can always load your own 256-color palette off disk or from a bitmap's attached palette.
Locking/Unlocking Function Mappings
Alright, everything is looking good. Now let's take a look at the four functions we made up to lock/unlock the primary and secondary surfaces. In the virtual computer interface, they looked like this:
Lock_Primary(UCHAR **primary_buffer, int *primary_pitch); Unlock_Primary(UCHAR *primary_buffer); Lock_Secondary(UCHAR **secondary _buffer, int *secondary _pitch); Unlock_Secondary(UCHAR *secondary _buffer);
In the real graphics library, we have these functions that implement that functionality:
UCHAR *DDraw_Lock_Primary_Surface(void); int DDraw_Unlock_Primary_Surface(void); UCHAR *DDraw_Lock_Back_Surface(void); int DDraw_Unlock_Back_Surface(void);
The only difference is that the real functions alter the globals
UCHAR *primary_buffer; // primary video buffer UCHAR *back_buffer; // secondary back buffer int primary_lpitch; // memory line pitch int back_lpitch; // memory line pitch
directly, and hence don't need any parameters.
Animation Function Mapping
The last function that I want to mention the mapping for is the animation function that flips the secondary surface buffer to the primary (or copies it in some cases). If you remember, we called it Flip_Display() in the virtual computer software interface model. This function is what makes smooth animation possible. In the real library, the function that does the job is
int DDraw_Flip(void);
Nothing more than a different name, really. The functionality of the desired virtual model is exactly implemented in this function. DDraw_Flip() will work perfectly for both full-screen and windowed applications.
Well, that's it for the function mapping of the virtual computer model with our real functions. I just wanted you to really understand what we are trying to docreate a generic graphics system with an API that we have handy that abstracts the graphics rendering enough that we only think in terms of two frame buffers and a few functions. I really hate dumping an API on you, but my hands are tied (all eight of them), and I had to use something to implement the virtual computer interface for which we had the source.
The Final T3DLIB Game Console
Now we're finally ready to see the complete Game Console that we are going to use to create all of the demos and game programs from here on out. Of course, we might add to it, but for the most part what you are about to see captures all the functionality we need. The upgraded Game Console is named T3DCONSOLE2.CPP. Its source is shown in the following.
NOTE
You might be wondering what happened to T3DCONSOLE.CPP|EXE. In the first Tricks, I created T3DCONSOLE.CPP|EXE, so it only seems appropriate to append a "2" onto the new version, because this book is really a continuation of the previous Tricks and we want to keep our naming conventions consistent.
// T3DCONSOLE2.CPP - First template for Tricks 3D Vol II // Use this as a template for your applications if you wish // you may want to change things like the resolution of the // application, if it's windowed, the directinput devices // that are acquired and so forth... // currently the app creates a 640x480x16 windowed display // hence, you must be in 16 bit color before running the application // if you want fullscreen mode then simple change the WINDOWED_APP // value in the #defines below value to FALSE (0). Similarly, if // you want another bitdepth, maybe 8-bit for 256 colors then // change that in the call to DDraw_Init() in the function // Game_Init() within this file. // READ THIS! // To compile make sure to include DDRAW.LIB, DSOUND.LIB, DINPUT.LIB, // DINPUT8.LIB, WINMM.LIB in the project link list, and of course // the C++ source modules T3DLIB1.CPP,T3DLIB2.CPP, and T3DLIB3.CPP // and the headers T3DLIB1.H,T3DLIB2.H, and T3DLIB3.H must // be in the working directory of the compiler // INCLUDES /////////////////////////////////////////////// #define INITGUID // make sure al the COM interfaces are available // instead of this you can include the .LIB file // DXGUID.LIB #define WIN32_LEAN_AND_MEAN #include <windows.h> // include important windows stuff #include <windowsx.h> #include <mmsystem.h> #include <iostream.h> // include important C/C++ stuff #include <conio.h> #include <stdlib.h> #include <malloc.h> #include <memory.h> #include <string.h> #include <stdarg.h> #include <stdio.h> #include <math.h> #include <io.h> #include <fcntl.h> #include <ddraw.h> // directX includes #include <dsound.h> #include <dmksctrl.h> #include <dmusici.h> #include <dmusicc.h> #include <dmusicf.h> #include <dinput.h> #include "T3DLIB1.h" // game library includes #include "T3DLIB2.h" #include "T3DLIB3.h" // DEFINES //////////////////////////////////////////////// // defines for windows interface #define WINDOW_CLASS_NAME "WIN3DCLASS" // class name #define WINDOW_TITLE "T3D Graphics Console Ver 2.0" #define WINDOW_WIDTH 640 // size of window #define WINDOW_HEIGHT 480 #define WINDOW_BPP 16 // bitdepth of window (8,16,24 etc.) // note: if windowed and not // fullscreen then bitdepth must // be same as system bitdepth // also if 8-bit the a pallete // is created and attached #define WINDOWED_APP 1 // 0 not windowed, 1 windowed // PROTOTYPES ///////////////////////////////////////////// // game console int Game_Init(void *parms=NULL); int Game_Shutdown(void *parms=NULL); int Game_Main(void *parms=NULL); // GLOBALS //////////////////////////////////////////////// HWND main_window_handle = NULL; // save the window handle HINSTANCE main_instance = NULL; // save the instance char buffer[256]; // used to print text // FUNCTIONS ////////////////////////////////////////////// LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // this is the main message handler of the system PAINTSTRUCT ps; // used in WM_PAINT HDC hdc; // handle to a device context // what is the message switch(msg) { case WM_CREATE: { // do initialization stuff here return(0); } break; case WM_PAINT: { // start painting hdc = BeginPaint(hwnd,&ps); // end painting EndPaint(hwnd,&ps); return(0); } break; case WM_DESTROY: { // kill the application PostQuitMessage(0); return(0); } break; default:break; } // end switch // process any messages that we didn't take care of return (DefWindowProc(hwnd, msg, wparam, lparam)); } // end WinProc // WINMAIN //////////////////////////////////////////////// int WINAPI WinMain( HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) { // this is the winmain function WNDCLASS winclass; // this will hold the class we create HWND hwnd; // generic window handle MSG msg; // generic message HDC hdc; // generic dc PAINTSTRUCT ps; // generic paintstruct // first fill in the window class stucture winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; winclass.lpfnWndProc = WindowProc; winclass.cbClsExtra = 0; winclass.cbWndExtra = 0; winclass.hInstance = hinstance; winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); winclass.hCursor = LoadCursor(NULL, IDC_ARROW); winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); winclass.lpszMenuName = NULL; winclass.lpszClassName = WINDOW_CLASS_NAME; // register the window class if (!RegisterClass(&winclass)) return(0); // create the window, note the test to see if WINDOWED_APP is // true to select the appropriate window flags if (!(hwnd = CreateWindow(WINDOW_CLASS_NAME, // class WINDOW_TITLE, // title (WINDOWED_APP ? (WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION) : (WS_POPUP | WS_VISIBLE)), 0,0, // x,y WINDOW_WIDTH, // width WINDOW_HEIGHT, // height NULL, // handle to parent NULL, // handle to menu hinstance,// instance NULL))) // creation parms return(0); // save the window handle and instance in a global main_window_handle = hwnd; main_instance = hinstance; // resize the window so that client is really width x height if (WINDOWED_APP) { // now resize the window, so the client area is the actual size requested // since there may be borders // alsocontrols if this is going to be a windowed app // if the app is not windowed then it won't matter RECT window_rect = {0,0,WINDOW_WIDTH-1,WINDOW_HEIGHT-1}; // make the call to adjust window_rect AdjustWindowRectEx(&window_rect, GetWindowStyle(main_window_handle), GetMenu(main_window_handle) != NULL, GetWindowExStyle(main_window_handle)); // save the global client offsets, they are needed in DDraw_Flip() window_client_x0 = -window_rect.left; window_client_y0 = -window_rect.top; // now resize the window with a call to MoveWindow() MoveWindow(main_window_handle, 0, // x position 0, // y position window_rect.right - window_rect.left, // width window_rect.bottom - window_rect.top, // height FALSE); // show the window, so there's no garbage on first render ShowWindow(main_window_handle, SW_SHOW); } // end if windowed // perform all game console specific initialization Game_Init(); // disable CTRL-ALT_DEL, ALT_TAB, comment this line out // if it causes your system to crash SystemParametersInfo(SPI_SCREENSAVERRUNNING, TRUE, NULL, 0); // enter main event loop while(1) { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { // test if this is a quit if (msg.message == WM_QUIT) break; // translate any accelerator keys TranslateMessage(&msg); // send the message to the window proc DispatchMessage(&msg); } // end if // main game processing goes here Game_Main(); } // end while // shutdown game and release all resources Game_Shutdown(); // enable CTRL-ALT_DEL, ALT_TAB, comment this line out // if it causes your system to crash SystemParametersInfo(SPI_SCREENSAVERRUNNING, FALSE, NULL, 0); // return to Windows like this return(msg.wParam); } // end WinMain // T3D II GAME PROGRAMMING CONSOLE FUNCTIONS //////////////// int Game_Init(void *parms) { // this function is where you do all the initialization // for your game // start up DirectDraw (replace the parms as you desire) DDraw_Init(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_BPP, WINDOWED_APP); // initialize directinput DInput_Init(); // acquire the keyboard DInput_Init_Keyboard(); // add calls to acquire other directinput devices here... // initialize directsound and directmusic DSound_Init(); DMusic_Init(); // hide the mouse ShowCursor(FALSE); // seed random number generator srand(Start_Clock()); // all your initialization code goes here... // return success return(1); } // end Game_Init /////////////////////////////////////////////////////////// int Game_Shutdown(void *parms) { // this function is where you shutdown your game and // release all resources that you allocated // shut everything down // release all your resources created for the game here.... // now directsound DSound_Stop_All_Sounds(); DSound_Delete_All_Sounds(); DSound_Shutdown(); // directmusic DMusic_Delete_All_MIDI(); DMusic_Shutdown(); // release all input devices DInput_Release_Keyboard(); // shut down directinput DInput_Shutdown(); // shutdown directdraw last DDraw_Shutdown(); // return success return(1); } // end Game_Shutdown ////////////////////////////////////////////////////////// int Game_Main(void *parms) { // this is the workhorse of your game it will be called // continuously in real-time this is like main() in C // all the calls for you game go here! int index; // looping var // start the timing clock Start_Clock(); // clear the drawing surface DDraw_Fill_Surface(lpddsback, 0); // read keyboard and other devices here DInput_Read_Keyboard(); // game logic here... // flip the surfaces DDraw_Flip(); // sync to 30ish fps Wait_Clock(30); // check of user is trying to exit if (KEY_DOWN(VK_ESCAPE) || keyboard_state[DIK_ESCAPE]) { PostMessage(main_window_handle, WM_DESTROY,0,0); } // end if // return success return(1); } // end Game_Main
Let's take a look at some of the key points of the Game Console, and then we'll move on to using it. First, you'll notice that some of the code is highlighted in bold to indicate important code sections. Let's take a look at what does what.
Opening the Game Console Window
The first lines of code that are highlighted are in the #defines section:
#define WINDOW_WIDTH 640 // size of window #define WINDOW_HEIGHT 480 #define WINDOW_BPP 16 // bitdepth of window (8,16,24 etc.) // note: if windowed and not // fullscreen then bitdepth must // be same as system bitdepth // also if 8-bit the a palette // is created and attached #define WINDOWED_APP 1 // 0 not windowed, 1 windowed
These are very important because they control the size of the window (or full-screen size), the bit depth, and whether you want a windowed display rather than full screen. Currently, I have them set to 640x480 for the window size, 16-bits per pixel, and a windowed display. These parameters are used in a number of places in the code. However, the two most important are when the window is created in WinMain() along with the call to DDraw_Init() in Game_Main(). Let's talk about the call in WinMain() first.
If you take a look at the Create_Window() call in WinMain() (copied in the following fragment for your convenience), you will notice that there is a ternary conditional operator that tests whether the application is windowed.
// create the window, note the test to see if WINDOWED_APP is // true to select the appropriate window flags if (!(hwnd = CreateWindow(WINDOW_CLASS_NAME, // class WINDOW_TITLE, // title (WINDOWED_APP ? (WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION) : (WS_POPUP | WS_VISIBLE)), 0,0, // x,y WINDOW_WIDTH, // width WINDOW_HEIGHT, // height NULL, // handle to parent NULL, // handle to menu hinstance,// instance NULL))) // creation parms return(0);
This is so the window is created with the proper Windows flag. If we want a windowed display, we want to see the window borders and have a couple of controls such as a close box, so the Window flags (WM_OVERLAPPED | WM_SYSMENU |WS_CAPTION) are used.
On the other hand, if the code selects a full-screen mode, the window is made the same size as the DirectDraw surface, but with no controls; hence the window style WM_POPUP is used. Additionally, once the window is created (either in preparation for a windowed application or full-screen) the next section of code is rather tricky. It basically resizes the window so that the client area is the size that we are requesting, rather than the size we are requesting minus the size of the border and controls.
If you recall from Windows programming, when you create a window with size WINDOW_WIDTH x WINDOW_HEIGHT, it doesn't mean the client area will be WINDOW_WIDTH x WINDOW_HEIGHT. It means the window's entire area will be WINDOW_WIDTH x WINDOW_HEIGHT. Therefore, if there are no controls or borders, the client area will be WINDOW_WIDTH x WINDOW_HEIGHT, but if there are controls, we lose a little, as shown in Figure 3.13.
To solve this problem, we need to resize the window on the fly so that the size of the client area is the exact size that we are requesting for the window. Here's the code that does this:
// resize the window so that client is really width x height if (WINDOWED_APP) { // now resize the window, so the client area is the actual size requested // since there may be borders and controls if this is going to be a windowed app // if the app is not windowed then it won't matter RECT window_rect = {0,0,WINDOW_WIDTH-1,WINDOW_HEIGHT-1}; // make the call to adjust window_rect AdjustWindowRectEx(&window_rect, GetWindowStyle(main_window_handle), GetMenu(main_window_handle) != NULL, GetWindowExStyle(main_window_handle)); // save the global client offsets, they are needed in DDraw_Flip() window_client_x0 = -window_rect.left; window_client_y0 = -window_rect.top; // now resize the window with a call to MoveWindow() MoveWindow(main_window_handle, 0, // x position 0, // y position window_rect.right - window_rect.left, // width window_rect.bottom - window_rect.top, // height FALSE); // show the window, so there's no garbage on first render ShowWindow(main_window_handle, SW_SHOW); } // end if windowed
Figure 3.13 Total window area compared to client area.
There are a couple of other ways to do this, but this is the one I usually use. Another way is to know what controls you have, query Windows for the size of the controls, and then compute the size the window should be with the controls. Whatever way you do it, just make sure that if a windowed app is selected, the client area is exactly WINDOW_WIDTH x WINDOW_HEIGHT.
NOTE
If you set WINDOWED_APP to 0, this logic isn't executed, because a WM_POPUP window has no controls and we don't need to resize.
After the window is created and resized (if needed), at some point a call to Game_Init() is made. Within it you will add all your logic and initialization in the future. However, at this point, it has the bare minimum to get things going. In line with our discussion, a call to DDraw_Init() is made:
// start up DirectDraw (replace the parms as you desire) DDraw_Init(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_BPP, WINDOWED_APP);
You will notice that you don't have to do anything. Just make sure to set the #defines up at the top of the file to what you want and whammoyou're Kid Rock!
NOTE
You might notice those calls to SystemParametersInfo() around the main event loop. These calls make Windows think that the screensaver is on, and to ignore ALT+TAB. DirectX applications can get really mangled if you don't handle ALT+TAB-ing correctly (which we don't), so this way you hopefully don't have to worry about it. If you're interested, take a look at the DirectX SDK, but in general when your application loses focus, you have to restore all lost surfaces and reacquire all input devices and so on. It's a real pain!
Using and Compiling the Game Console
Basically T3DCONSOLE2.CPP is just a "shell" or template that you will use to create applications. You can use it to handle all the Windows stuff, and then just drop your code into the game functions Game_Init(), Game_Main(), and Game_Shutdown(). However, to compile it, you still need to include the following files:
T3DLIB1.CPP|HThe DirectDraw module
T3DLIB2.CPP|HThe DirectInput module
T3DLIB3.CPP|HThe DirectSound and DirectMusic module
These files must be in your root:
PALDATA1|2.PALThe default palettes for 256-color mode
And you must, of course, link with
DDRAW.LIB, DSOUND.LIB, DINPUT.LIB, and DINPUT8.LIB
Please set the compiler target to Win32 .EXE application. Don't forget to add the DirectX .LIB files to the link list, and to set the search paths for the compiler to find all the headers.
Just for fun, I have compiled the Game Console without anything in it, creating the T3DCONSOLE2.EXE. The application doesn't do much, but creates a 640x480x16-bit windowed display. However, to run it you must have your desktop in 16-bit mode.
CAUTION
Windowed applications based on the Game Console will not change the bit depth of the desktop. You can alter it to do this if you want, but it can be catastrophic. Here's why: If you have other applications running and you then start up a DirectDraw application that changes the bit depth of the screen, but creates a windowed display rather than a full-screen display, the other application might function improperly and/or crash the system if you switch focus to it. As a rule of thumb for full-screen applications, do what you will with resolution and bit depth. But for windowed applications, you might want to test for the bit depth and see whether it meets your target before starting your application and letting the user switch the desktop.
I could have just as easily made the application a full-screen 640x480x16 application with a single change to the #defines:
#define WINDOWED_APP 0 // 0 not windowed, 1 windowed
However, windowed applications are more fun and a little easier to debug, so I am supporting them heavily in this book. Nevertheless, when your game is finally ready to run, I suggest dumping the system into full-screen mode.