You have to be most careful with the design of the Graphics object if you want to make it flexible enough for any game engine you might write. The best engines have support for multiple APIs such as both Direct3D and OpenGL (although I hear OpenGL may be taking a back seat due to DirectX 8.0's exciting new capabilities). Still, even if you only code for Direct3D, the correct design will leave the way open for new graphic API additions later. So, for this discussion, I will continue to pursue the importance of also including OpenGL, if not for its necessity, for the utility of demonstrating the support of multiple APIs.
The Graphics object should be capable of the following:
As with the rest of the game engine's primary components, you will only have
ONE Graphics object declared. And it should contain the generic prototypes
for all the functions that will need to be called. Some of the functions will
perform general tasks like initialization or resource cleanup, others will
be for rendering primitives or complex models.
The way I managed to support multiple APIs in this object was to declare some subobjects within the Graphics object. These objects would be the D3D object and the OpenGL object, both derived from a base Graphics3D object. What I did was define the public prototypes for Graphics3D as virtual functions. And within the Graphics object, I have a Graphics3D type pointer that points to the currently active API object (either D3D or OpenGL). After that, it was simply a matter of coding the functions within D3D and OpenGL to perform the API specific tasks necessary to fulfill the needs of the function called. Such functions would be DrawRectangle, DrawModel, DrawText, etc. That way when g_graphics->DrawRectangle() is called, it forwards the request to m_APIptr->DrawRectagle(), effectively routing the request to the correct API.
There may be better ways of doing this with less overhead, but hopefully you get the idea.
Whenever the API is changed, the pointer will just take on the value of the other variable, after proper cleanup of resources takes place, of course.
And when a new graphics API is to be added, all that is necessary is to create a new class derived off Graphics3D, include it in the Graphics object declaration, provide a way to enumerate and initialize it, and code all the functions to use the new API to accomplish the same tasks as D3D and OpenGL. A lot of work, no doubt, but at least the groundwork is there.
Your engine should require DirectX, as most engines do.
However, not all machines have the OpenGL DLLs installed, nor may they have the DLLs for any other rendering API that the engine may use.
If you compile your code with the .LIB files for the corresponding API, the program will immediately require the presence of the DLLs when it executes. This would create a problem for the user that doesn't plan to use OpenGL and doesn't have it installed.
To get around this, there is a way to avoid requiring the DLLs: DON'T COMPILE WITH THE LIBS!
I know what you're asking, being the experienced C++ programmer that you are: If we aren't going to compile with the LIBs, won't the compiler generate "unresolved external" errors? Yes, it would, IF you used the standard reference to the functions of the API (i.e. ::glMatrixMode() would require that opengl32.lib be in your linker list). There is a way to refer to the function without requiring the LIBs. It requires a different style of coding, but it works.
The first step: Behold, two wonderful little functions of the Win32 API library: LoadLibrary() and GetProcAddress().
LoadLibrary() allows you to load up a DLL in memory explicitly. If the load fails, you know that DLL is not on the system, and you can make that API unavailable to the user. If successful, it returns a handle to the loaded DLL.
GetProcAddress() allows us to retrieve the memory address of a function within a loaded DLL. All we need to provide is the handle of the DLL and the name of the function (such as "glMatrixMode"). Once that address is retrieved, you can call the function using that address and it behaves exactly as it would have if the program loaded the DLL naturally from compiling with the LIB!
Oh, and by the way, FreeLibrary() unloads the DLL from memory.
The second step: We need to define our own function pointers, one for each function within the DLL we plan to call. This will end up being a huge list, granted, but it's better than getting that ugly "DLL not found" error! It is VITAL that you declare these function pointer variables with the same type of parameter list as the originals. Otherwise, you will get stack errors when those functions return!
Here's a coding example for glMatrixMode:
Define your pointer type like this:
typedef void (WINAPI * GLMatrixMode)(GLenum);
Define your function pointer variable like this:
Now in your code, load up the library and feed the function's address to
hDLL = LoadLibrary("opengl32.dll");
if (hDLL != NULL)
// If this returns NULL, the function isn't there
glMatrixMode = (GLMatrixMode)GetProcAddress(hDLL, "glMatrixMode");
Now, you can use the function as normal:
Don't forget to release the library when you are all done:
I freaked when that worked the first time.
For those of you that are brave enough to try your hand at the 3Dfx MiniGL DLL, this same method is how you crack it open and use it.
In order to shield the programmer from needing to understand the specific workings of any given rendering API, all API-specific calls are hidden within the Graphics object.
In order to accomplish this, you, the engine designer, must come up with enough generalized rendering requests to support the needs of your engine. These requests are a combination of function calls, and structures specifically designed to hold the model data. The structure is passed to the corresponding routine to perform its rendering. As your engine evolves and you make additions to it, your graphic routines and structures will gradually reach the mainstream of the typical games of today.
Don't expect too much of yourself right out of the gate. It takes a lot of research to come up with optimized rendering routines and generate "world class" graphics. But it is within everyone's reach. Use a discerning eye. You'll be amazed by your first attempts (and you should, it's exciting to see!), but be sure to leave yourself open for what more needs to be done.
An example of a generalized routine might be the RenderSimpleModel() routine, which simply parses a 3D model and renders all of its polygons. A structure holds the model, and that structure references all textures and alpha blending data that might be used in the rendering.
You may also have specialized routines and structures for particular types of models. Consider a terrain model, where there are many polygons in a single model to render. To just parse the structure blindly, trying to render every polygon exactly as it is, would take too long. So instead of using RenderSimpleModel() to draw the terrain, you would write a more specific routine that parses the terrain model and submits it to the rendering API in a more optimized fashion, such as RenderTerrain().
Other types of routines include setting up the rendering volume, performing translations, scaling and rotations, activation and placement of light sources, depth buffer and alpha blending settings, etc. These would be standard for any rendering API, and for the most part, the general routine should translate directly to a corresponding routine in the actual API.
Build it slowly, piece by piece, until you get a good set of rendering routines as a starting point. Then keep adding to it as your design visions expand. Try to make it as modular as possible, but try to write the model rendering routines as independent functions, avoid having them depend on each other, and optimize them as much as possible. This way, your engine has the best potential for running at its fastest, which is important, because what slows down a rendering engine more than anything is a poor structure parse and submittal routine.
Also note that sometimes, when a different API is activated, it may actually require loading models into a different kind of structure that is more well suited for optimized rendering. DirectX 8.0 has specialized structures that models can be loaded into that it works with very well, but they are specific to DirectX. OpenGL requires a more low-level approach to parsing models and rendering them, unless you find a development kit that adds functionality to it. Just be warned that a package that might seem to make your programming easier may actually slow it down, such as a tool that claims to provide parsing routines for predefined structure types, but does so very poorly. Always be prepared to improve upon what already exists.
A strong engine allows the user to change resolution, bit depth and the API being used on the fly. The change may take a few seconds as API objects are destroyed and the new ones created, but this change doesn't happen very often, so the length of time it takes to change shouldn't be a factor in deciding not to include it. There may still be those games that change resolution naturally, such as those that show their menus in a different screen mode, but this should have little impact. Just don't force the user to stop the program and rerun it just because they wanted to choose a different rendering option!
I always hated having to choose the accelerator and resolution BEFORE I actually run the program. That is the reason I chose to write an engine that could change everything during the execution.
Here's how you do it:
When the engine first starts up, a function within the Graphics object that enumerates available graphics devices and APIs will be called. This enumeration routine calls enumeration routines within the D3D and OpenGL subobjects. Where possible, bit depth and resolution possibilities should also be enumerated. The results of this enumeration should be stored in a structure that can be called upon at any time. That way, a form can be designed that shows everything that is available (display device, API, resolution, bit depth), and the user simply selects the hardware device, rendering API, resolution and bit depth that they want, click Apply, and it just happens.
To meet this challenge, you need to know how every API accomplishes this task for itself.
DirectX is fairly easy. The device you are using, the API, resolution and bit depth are all parameters in the call to initialize the display mode. You just need to retrieve the GUIDs for the available devices and store them in your enumeration structure when the engine first starts up.
OpenGL is not so easy. Whether or not it is available depends upon the existance of the DLL, which we can detect with the Win32 LoadLibrary() function. Changing the resolution in an OpenGL application also requires the assistance of the Win32 API, particularly the ChangeDisplaySettings() function. After the settings are changed, THEN the OpenGL instance is created. Finally, the rendering mode (windowed or fullscreen) is determined by the use of SetWindowPos() with the HWND_TOPMOST flag after the OpenGL instance is created. The textures can be loaded any time afterwards. When changing resolution, I like to destroy the OpenGL instance and textures to free up memory, then change the resolution, then recreate it again. There may be a better way to do this, but currently this is how I understand it.
Restoring is the act of taking a minimized application and making it the foreground application again.
When this occurs, if you don't properly make sure all your buffers are intact, you will have problems. When a program restore takes place, explicitly call whatever routines are necessary to make sure any API objects are intact.
This is the main reason I like to load resources both into system memory AND the API objects. If a buffer needs to be restored, the resource data is already in system memory and just needs to be copied into the API object again. This avoids time taken for reloading the resources from the hard drive.
There are two main reasons for allowing the application to run in either mode: First, windowed applications allow you, the programmer, to step through your code during debugging. Second, allowing the user to run in windowed mode may help them overcome compatibility issues on some occasions.
I prefer to hard code the debug version of the application to run in windowed mode always. The release version by default runs in full screen.
Windowed applications should never allocate exclusive access to system resources. What good is stepping through your code in the programming environment if the keyboard is exclusively bound to the running application, and you have no mouse? Full screen applications can and probably should take exclusive access to guarantee that they can reach the hardware that they need.
Try to code your engine to allow hot-swapping between windowed and full screen mode with a single function call. Personally, I haven't done it yet, but when I do I'll post the discoveries on how to do that here. My concerns are the need to destroy the parent window that drives the application and recreating it without causing problems, which I haven't researched yet.