Introduction
Well if you've been hanging around the DirectX newsgroup for any amount of time, you'll notice and/or probably discover that one of the most discussed topics (besides OpenGL vs. DX) is concerning questions about using classes and/or interfaces with DLL files.
Currently, Gamedev.net has one article dealing with using DLL files with classes, written by Gaz Iqbal. It's an excellent starting point, and I urge the reader to peruse that document first. This is an attempt to build on that document, while at the same time demonstrating the power at our fingertips.
Why use a separate Renderer DLL?
Some may be asking that question to themselves right at the moment. Although everyone has their own reasons, I'll attempt to answer a few of the ones that strike me as being the largest:
- Object Oriented Design: Handling your Renderer object in a separate DLL environment allows for a more flexible object orientated implementation of your game/application. This layout would enable your small team of programmers to easily see the readability and reusability of separate code modules.
- Shipping Updates: By structuring your rendering code this way, you are creating an easier environment in which to ship updates to your customers. Instead of having to recompile your libraries and executables, you only need to ship a newer DLL (Note: provided of course that the "external" interfaces to your DLL do NOT change; only the implementation code inside the classes contained in the DLL should be modified).
- Educational: Although this response is similar to the first one, basically this can be viewed as a "cool" exercise in computer programming. Not only will it wow your friends, but you will come away from it learning some new tricks that are not only applicable to your game, but also to software development in general.
Why not use COM?
A few others might be asking this question as well. My first response to that question, would be that COM is a pretty advanced topic of discussion. Most "newbies" are still getting used to some of the most basic concepts of Object Oriented design philosophies, without having to worry about picking up COM. Provided your product is targeted solely for the Win32 environment, COM could be the ideal solution. The targeted audience for this paper, however, are the programmers who are on the road to understanding COM, yet not quite there yet. Another response to this question, is that COM is Win32 dependent. If you are a Linux developer, then you are outta luck with COM.
Getting started
I am one of "those" types of people that need to see a clear example along with the explanation. Because of this, I've attempted to provide as much clarity for this article as I can. The goal of this paper is to create an example DLL file which we can use in a, or indeed any, game.
In order for the DLL to function properly, it is paramount that we isolate ANY graphics code from our main library. That way, should anything change in our DLL, then (most of the time) NO modifications will need to be made to the library, and vice versa.
What we're going to do is first create a static library holding our engine/framework structure. Within this project, we are then going to create a DLL project, in order to implement the rendererInterface object interface.
Requirements:
- Visual Studio 6.0 (can function on Visual Studio .NET as well)
- The DirectX SDK (if you want to implement the renderer in Direct3D8)
- The OpenGL headers and libraries (if you want to implement the renderer in OpenGL)
- Willingness to have fun!
Now for the most part, most of you will have all of this already, right? Right. If you're using Borland, then I'm 80-90% sure that this "method" of DLL implementation will work as well. (I'm endeavoring to keep it non-compiler specific as much as possible).
Design Patterns
For those unfamiliar with the concept of Design Patterns, they are a useful tool in the world of Software Development. They are an approach to solving specific object oriented problems.
The goal of our class framework is to create an object that we can use independently of the actual API. So in other words, we want to be able to use an object which will give us the ability to freely call its methods, without having to worry about "proper" Direct3D or OpenGL calls. This kind of abstraction is, and can be, a great benefit to our game engine design.
The more astute of you will immidiately realize that we will need to create a base class which will contain the relevant methods for our rendererInterface object. In OO terms, this "base" class can also be called an Interface.
I'm not sure if this is the "OO correct" approach, but my common method to designing an interface class, is to first jot down a few things that we NEED it to do. For our example, we'll keep things nice and simple. It needs to:- create and initialize the graphics hardware
- clear the screen
- start sending primitives to the hardware
- finish sending primitives to the hardware
- change the screen clearing color
- destroy and cleanup our graphics hardware
See?? Nothing too complicated...hehe
The design pattern which seems to best fit our needs is called the Abstract Factory design pattern. According to Gamma [1], the official definition of this pattern is to:
[indent=1][bquote]Provide an interface for creating families of related or dependent objects without specifying their concrete classes[/bquote]
Which is our intent.
Create the static library project
First, create a "static library" project. We'll call ours GameFramework. (Note that for clarity purposes, we'll only talk about the classes relevant to this article. For following along, feel free to download the accompanying source code).
Let's first sketch out our interface class:
//rendererInterface.h
//This class is to provide us with an interface object to our graphics hardware,
//to free us from having specific graphic API calls inside our static library.
class rendererInterface
{
protected:
//some base level objects can go here
public:
rendererInterface(){};
virtual ~rendererInterface(){};
//method to create and initialize our hardware
virtual HRESULT createDevice(HWND, DWORD, DWORD, DWORD, BOOL) = 0;
//method to destroy it
virtual void destroyDevice() = 0;
//method to begin rendering primitives
virtual HRESULT beginRenderingScene() = 0;
//method to end rendering primitives
virtual void endRenderingScene() = 0;
//method to change the screen clearing color
virtual void setClearColor(FLOAT, FLOAT, FLOAT, FLOAT) = 0;
};
//In order to properly follow a kind of OO design, let's create ourselves
//a "factory" object, which is SOLELY responsible for the creation and
//destruction of our rendererInterface object. Note that we made the member
//functions of the rendererInterface object pure virtual, in order to prevent
//the programmer from instantiating this class DIRECTLY.
class rendererFactory
{
protected:
rendererInterface *m_pRenderer;
HINSTANCE m_hInst;
HMODULE m_hDLL;
public:
rendererFactory(HINSTANCE hInst){ m_hInst = hInst;};
~rendererFactory(){ destroyInterface();};
HRESULT initInterface(TCHAR *szType);
rendererInterface* getInterface(){ return m_pRenderer; };
void destroyInterface();
};
Okay we'll have to do some explainin'...first off, we're just creating a rendererInterface object which is responsible for interfacing with our graphics API. We're setting up an environment within our framework, so that we can abstract our graphics API calls. The rendererFactory object's responsibility is to properly create and destroy our rendererInterface object.
What are DLL's??
Again, if you're unsure of what DLL's are, then please consider re-reading GameDev.net's other DLL article. Back already? Okay then let's keep going. Within our GameFramework workspace, create a win32 DLL project with NO files, as we'll add them ourselves. For this sample, we'll call it GameD3DRenderer. Again, follow along with the source code, but for the article's sake, we'll just jot down the more pertinent information:
;GameD3DRenderer.def
;The following functions are exported from this DLL, in order to
;properly create and destroy our renderer object without worrying about
;name mangling.
LIBRARY "GameD3DRenderer.dll"
EXPORTS
createRendererInterface
destroyRendererInterface
//now in our .cpp file
HRESULT createRendererInterface(rendererInterface **pInterface)
{
if(!*pInterface)
{
//convert our interface pointer to a D3DRenderer object
//note: that in our GameOGLRenderer project, we just
//need to replace the following class with OGLRenderer
*pInterface = new D3DRenderer;
return S_OK;
}
return E_FAIL;
}
// Release our Handle to the class
HRESULT destroyRendererInterface(rendererInterface **pInterface)
{
if(!*pInterface)
{
return E_FAIL;
}
delete *pInterface;
*pInterface = 0;
return S_OK;
}
The two functions above should be clear enough. We're taking as input the rendererInterface object, which we use to properly create and destroy an instance of our D3DRenderer class, which is the Direct3D implementation of our rendererInterface.
"-almost there" "I can't shake him!"
So now that we've got our rendererInterface class defined, we can now just briefly cover how our rendererFactory object will work. To create and initialize a rendererInterface object, we'll need to follow a few steps that we can encapsulate within this factory object.- decide which implementation the user wants (ie. OpenGL or Direct3D)
- use run-time linking to load up the proper DLL
- create and initialize the rendererInterface object
- return it for use to the programmer
Simple, eh? That's what usually happens when you break down your problem into smaller tasks.
//rendererFactory.cpp
HRESULT rendererFactory::initInterface(TCHAR *szType)
{
if(strcmp(szType, "OpenGL") == 0)
{
//we're using OpenGL for our renderer
m_hDLL = LoadLibraryEx("GameOGLRenderer.dll",NULL,0);
if(!m_hDLL)
{
MessageBox(NULL,
"Error loading up GameOGLRenderer.dll",
"Fatal Error",
MB_OK | MB_ICONERROR);
return E_FAIL;
}
}
else
{
//we're using Direct3D for our renderer
m_hDLL = LoadLibraryEx("GameD3DRenderer.dll",NULL,0);
if(!m_hDLL)
{
MessageBox(NULL,
"Error loading up GameD3DRenderer.dll",
"Fatal Error",
MB_OK | MB_ICONERROR);
return E_FAIL;
}
}
//okay now that we've got our dll loaded, let's get a pointer
//to our createInterface function
CREATERENDERERINTERFACE CreateInterface = 0;
HRESULT hr;
CreateInterface = (CREATERENDERERINTERFACE)GetProcAddress(m_hDLL,
"createRendererInterface");
hr = CreateInterface(&m_pRenderer);
if(FAILED(hr)){
MessageBox(NULL,
"Error using our createInterface function",
"FATAL Error", MB_OK | MB_ICONERROR);
m_pRenderer = NULL;
return E_FAIL;
}
return S_OK;
}
void rendererFactory::destroyInterface()
{
DESTROYRENDERERINTERFACE DestroyInterface = 0;
HRESULT hr;
DestroyInterface = (DESTROYRENDERERINTERFACE)GetProcAddress(m_hDLL,
"destroyRendererInterface");
hr = DestroyInterface(&m_pRenderer);
if(FAILED(hr)){
MessageBox(NULL,
"Error using our destroyInterface function",
"GameFramework Error", MB_OK);
m_pRenderer = NULL;
}
}
Well, this might look a little complicated, but again, reread the other GameDev.net article on using DLL's and you'll see that it's not so bad. We're just getting a function pointer into our DLL to the proper creating/destroying function. As a reminder, we need to do this in order to deal with the name mangling that accompanies DLL development.
What's Included?
With this article, I've provided all of the source code listed herein, as well as a TestFramework project, which demonstrates our rendering functionality in action. It's a project built with Visual Studio 6.0.
What we learned
Well I'm hoping that you had as much fun as I did! We covered quite a bit of material in this article, so hopefully I haven't glossed over things too much. If you have any corrections, or questions, please don't hesitate to [email="wazoo@wazooenterprises.com"]email me[/email].
References
- Gamma, Helm, Johnson & Vlissides. Design Patterns: Elements of reusable Object-Oriented Software, Addison Wesley, 1995.
- Visual Studio 6.0 (can function on Visual Studio .NET as well)