.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.
Last updated: 10:55 AM Monday October 2008
View the errata, additions and corrections to this tutorial
![]()
This tutorial is going to be a bit of a wrapup, and some enhancement to the code we've written over the last 2 tutorial series'. We're creating a new class, but unlike the other tutorials it won't actually draw anything on screen. Instead, it will become the basis for our management and grouping of classes. It will be the base class for most of our future graphics based classes.
If there was ever a time you needed to understand classes properly, it's now! Where things are complicated I'll include a short description of the C++ side of things, but as I outlined in the requirements (and in lots of other places) I assume you understand the C++ principles. But treat this as an advanced warning, we're going to be using the power of inheritance! This is going to be one of the harder tutorials we'll have to complete, so stick with it. It's worth it in the end ;)
Before we go any further, let me explain why I have chosen to create a heirarchy with a single generic base class at the top, with all our graphics related classes derived from it. It all comes down to a question of scene and code management. With one single base class, we can standardise all our error handling and automatically hand down important code to classes which inherit from it. For example, this tutorial will demonstrate a simple but handy debugging class method. Without a base class and inheritance, we would be forced to duplicate the code in every class we write. That means a lot of code to update. With a single base class we can add the debugging code at the top of the tree, and all our current and future classes will inherit it. In addition to this, with the use of some pure virtual methods we can ensure that a standard design is used in all derived classes. Finally, it means we can implement a nifty scene management system. The easiest way to explain this is with some pseudo-code. Up until now we've only had one or two objects in our application, and therefore we've used the following type of code:
HRESULT Render()
{
// Setup code
D3DDevice->BeginScene();
FrameRate_Font.Render(Device, BackBuffer);
Spaceship_Sprite.Render(Device, Backbuffer);
D3DDevice->EndScene();
}
That's all well and good for 2 objects, but what if we had 100? Or 1000? Modern games have huge amounts of scene objects to render, and following our code so far we would be forced into doing the following (pseudo-code):
HRESULT Render()
{
// Setup code
D3DDevice->BeginScene();
Object1.Render(Device, BackBuffer);
Object2.Render(Device, Backbuffer);
:
:
Object999.Render(Device, Backbuffer);
Object1000.Render(Device, Backbuffer);
D3DDevice->EndScene();
}
Typing all those objects out is not a nice thing to do, and close on impossible to maintain. However, using a single base class with all our graphics classes derived from it allows us to do the following:
HRESULT Render()
{
// Setup code
D3DDevice->BeginScene();
CBaseClass* pIterationPointer=Object1;
while(pIterationPointer)
{
pIterationPointer->Render(Device, BackBuffer);
pIterationPointer=pIterationPointer->GetNextObjectInList();
}
D3DDevice->EndScene();
}
Hopefully you can immediately see the advantage of this! No doubt some of you will notice the obvious question - the pseudo-code above depends on us knowing the first item in the linked list. If we create the linked list in GameInit(), and we modify it over time (as the game/demo progresses), it's possible that the first item in the linked list will change (ie: it will no longer be Object1). For the moment that is a limitation we'll just have to accept, but when we get a little further into the 3D side of things I'll present a tutorial that addresses this problem.At this point you should probably know that a lot of people disagree with the "single base class" way of doing things. Amongst other things, they claim it's slow, unwieldy, and totally against the object orientated programming methodology. I disagree with all of these points, except the last one. The argument most often used is as follows:
"Code derived from a base class called Object is bad OO code. Quite simply, object orientated code is meant to work around a heirarchial structure where each connected object has a relationship. People use a base class called Object because it's generic - a car, a house and a newspaper are all objects. In the real world there is absolutely no relationship between these objects, but coders link them all together with a single base class. In code, people derive timers, textures, 3D objects and sprites all from the same base class. They are all objects, after all. This is wrong."
I agree with this statement to a point. Class coding allows for powerful inheritance, but it's only useful as long as your objects have a relationship to each other. Linking a timer with a sprite by using the same base class doesn't make good sense technically or logically. However using the same base class for a 3D object (a cube for example) as for a sprite definately does make good sense, providing that your class implementations conform to a standard. The important point is that they have a similar purpose - to draw to the screen. Granted, one is a 2D sprite and the other a 3D object, but they both draw something and have similar class methods - Render(), Initialise() etc. On top of all this, they are actual objects in our scene, something that will become important later. So, this is why the code I will present in this tutorial will become the base class for CD3DSprite (and by inheritance, CD3DRasterFont), and for our future graphic-related classes. However we will not be deriving any old class from it - texture classes and our timer class are 2 good examples of classes that should not come from the same base class!
Anyway, when you're managing a 3D scene with hundreds of objects, I personally think this is without a doubt the best way of dealing with the multiple calls to class methods. It is my personal opinion, so you may disagree and that's your choice. However all future tutorials will be based on this heirarchy.
With all this in mind, here's the source code for this tutorial:
[ Download complete VC6 workspace as .zip ]
ObjectDemo.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
CD3DObject.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .h file ]
CD3DObject.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]D3DFuncs.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .h file ]
D3DFuncs.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
Note! With the new implementation of CD3DObject, I've made some small changes to the implementations of CD3DSprite and CD3DRasterFont. The changes are as follows:
1. Changed the CD3DSprite definition to inherit from CD3DObject.
2. Added code to the CD3DSprite and CD3DRasterFont constructors to initialise the RTTI and class name.
3. Changed all the error handling code to use Error().
I have also added a new function to the D3DFuncs.cpp/.h source files. The updated code is all included in the Workspace ZIP above, so please download it and review the code changes.
Here's the rundown on the CD3DObject class methods:CD3DObject::Render()
Pure virtual method to ensure all our derived classes have a Render() method.
CD3DObject::RestoreVolatile()
Pure virtual method to ensure all our derived classes have a RestoreVolatile() method. Provides a way to restore unmanaged resources after a lost device.
CD3DObject::ReleaseVolatile()
Pure virtual method to ensure all our derived classes have a ReleaseVolatile() method. Provides a way to release unmanaged resources after a lost device.
CD3DObject::SetClassDesc()
Sets a human readable description of the class, used for debugging and tracking.
CD3DObject::SetRTTI()
Sets the Run Time Type Information of the class, enabling us to discover what type of class we have a pointer to at runtime.
CD3DObject::Error()
Provides more comprehensive error reporting.
CD3DObject::SetNextObject()
Sets the next object in the linked list.
CD3DObject::GetNextObject()
Retrieves the next object in the linked list.
CD3DObject::SetPrevObject()
Sets the previous object in the linked list.
CD3DObject::GetPrevObject()
Retrieves the previous object in the linked list.
I've also made one addition to our D3DFuncs.cpp/.h source code. At the bottom of the .cpp, you'll see a new function - D3DError(). This function has an identical purpose to the CD3DObject::Error() method, except it is used with non-class code. Examine the function body and you'll see what I mean. Because the 2 functions are identical I won't cover them seperately - the same principles apply.For this tutorial we're going to jump around the code a little, so be warned! First up, lets talk about the virtual methods. If you're unsure what a virtual method is, now would be a good time to research it. In simple terms, declaring a class method as virtual makes it possible to redefine the method (ie: override it) in a derived class. For example, making CBaseClass::Test() a virtual method allows us to redefine it as CDerivedClass::Test(). You may now be reasoning that you can do this anyway, even without declaring it as virtual. You would be correct, except for one thing! One of the most useful features of inheritance is being able to access any derived class with a pointer to it's base class. If you do not declare a method as virtual, you cannot access it through a pointer to a base class. This is because the compiler keeps a vector table of methods that are declared virtual and just changes the pointer in that table if you redefine a virtual method in a derived class. You may have heard this referred to as "late binding". Beyond virtual methods, you can declare a pure virtual method. Creating a pure virtual method forces all derived classes to implement the function, and is declared by placing an "=0" at the end of a method declaration. It also means that you cannot create an instance of any class containing one. This is fairly sensible as there is no good cause to create an instance of a CD3DObject, because the class does nothing in it's own right. Hopefully you now understand what a virtual and pure virtual method is. If not, please research it as it will become important shortly!
As you can see from the class header file, all our base class methods are defined as virtual. We probably won't need to, but I've done this in case we choose to override one of the methods at some point. We also have 3 pure virtual methods: Render(), RestoreVolatile() and ReleaseVolatile(). You can no doubt guess why we've chosen to declare Render() as a pure virtual method - by forcing all derived classes to implement a Render() method we will ensure some structure and uniformity to our code. Hopefully it will also make us stop and think - if we derive a file handler class from CD3DObject and suddenly realise we have to implement a totally inappropriate Render() method, we will stay away from the "everything derives from one class" trap!
However, RestoreVolatile() and ReleaseVolatile() are two totally new methods, and bear a little explanation. If you remember back to tutorial 4 of Series 1, we wrote a ValidateDevice() function which ensured our application responded directly to "lost devices" (A quick recap - a lost device is when our D3D app loses the focus by alt-tab, or another application taking over). At that point, I neglected to mention one very important fact. Most of the time, we can allow D3D to manage our resources - textures, surfaces etc. This means that should a device become lost, D3D will automatically destroy and recreate our resource as appropriate. However sometimes we have no choice but to create a resource that is not managed automatically by D3D. In this instance, we are responsible for destroying and recreating the resource ourselves. Should a class contain a resource that needs to be manually managed in the case of a lost device, the code to destroy the old data should be placed in ReleaseVolatile() and the code to recreate it should be placed in RestoreVolatile(). Up to this point we've not written any code that requires this manual handling, so as you can see in the updated CD3DSprite class both methods just return immediately after being called. In the future, the basic principle we will employ is that should a device become lost we iterate through all our objects calling ReleaseVolatile() and then RestoreVolatile(). We'll look at the iteration code when we discuss the linked list later in this tutorial. As for the actual code to release/restore our resources, it's 100% class and data dependant. When we come across a situation requiring it, I'll discuss it in depth then. It will be a later tutorial though, so for now just concentrate on understanding the rest of the class.
Next I want to examine the CD3DObject::Error() method, which looks like this:
HRESULT CD3DObject::Error(HRESULT hrCode, int nLine, char* szFile, char* szExtraInfo)
{
::OutputDebugString("----------------------32Bits.co.uk Error Handling-\n");
char pszError[256];
wsprintf(pszError, "The following error occurred on line %d of %s\n", nLine, szFile);
::OutputDebugString(pszError);
::OutputDebugString(DXGetErrorString8(hrCode));
::OutputDebugString("\n");
if(szExtraInfo)
{
::OutputDebugString("Extra information: ");
::OutputDebugString(szExtraInfo);
::OutputDebugString("\n");
}
if(m_pszClassDesc)
{
::OutputDebugString("Class Description: ");
::OutputDebugString(m_pszClassDesc);
::OutputDebugString("\n");
}
return hrCode;
}
The code is very straightforward. It takes the parameters, parses them into some nice strings, and outputs the info to the debug window. The trick is in the way we call the method. Taken from CD3DSprite:
// Next, create the sprite
rslt=D3DXCreateSprite(pDevice, &m_pSprite);
if(FAILED(rslt))
{
return Error(rslt, __LINE__, __FILE__, "Failed to create the sprite");
}
Take a look at the call to Error(). The first parameter is the HRESULT code that failed. The second and third are handy VisualC++ 6 macros that expand into the current line in the file, and the current filename. The final parameter is a short description that we choose. From a function call such as this, you would see the following type of output:
![]()
Fig 5.1: A much more useful debug output!
Note the second line of the error output - "D3DXERR_INVALIDDATA". If you review the code in CD3DObject::Error() above, you'll see that we've actually converted the pretty-useless hex HRESULT code into a pretty-useful english error using DXGetErrorString8(). To use this function you need to ensure your project is linked with dxerr8.lib, and you #include <dxerr8.h>.It's a very simple method, but because of inheritance we get a powerful debug output tool without extra work in any derived classes. You can now also see the purpose of CD3DObject::SetClassDesc() - if we have a large amount of instances of the same class, we can quickly see which instance has caused us the trouble. Here's a simple example:
CD3DRasterFont FrameRateFont;
FrameRateFont.SetClassDesc("Framerate RasterFont");
One line of code, and should an error occur we'll immediately know it was the CD3DRasterFont instance which displays the framerate that caused the problem. The last point of interest is that the CD3DObject::Error() method returns the HRESULT code it was passed, which of course means that if you check the returncode of any call to it, the code will evaluate as TRUE to the FAILED() macro.
If you use Visual Studio .Net, it also includes a handy macro __FUNCTION__, which expands to the fully qualified function or method that you use it in. As I don't use VC.Net for DirectX coding yet, I've not included the code for the extra macro. You may wish to add it into the Error() method and the D3DError() function for your own benefit. Wogston reliably informs me that __VISUALC__ is only defined in VC.Net. EMoon^TBL also informs me that _MSC_VER is defined in every version of Visual C++ and is equal to 1200 for Visual C++ 6, and 1300 for Visual C++ .Net. That should be enough to get you started!
Nearly there! Next we need to discuss the linked list methods - SetNextObject() and GetNextObject(). Your first question may be "What is a linked list?". If you've used some of the STL, you'll already know what this is, however if not imagine a linked list as a long chain of items where each item knows where to find the next item in memory. I discussed this in brief at the top of the tutorial, but just for a quick recap it looks something like this:
Fig 5.2: A diagrammatic linked list
The code on the left begins with a valid pointer to the first item (in our case, CD3DObject or a derived class), which would be equal to Class1.Pointer. It then iterates through a while loop as long as the pointer is valid. It first calls a class method using the current pointer, Class1.Render(), and then uses pointer to assign itself to the next address in the linked list. So, after the first iteration of the while loop pointer is now equal to the address of Class2 as stored in Class1.Pointer. The while loop iterates for the second time, Class2.Render() is called and pointer is set to the address of Class3. The while loop iterates again, Class3.Render() is called and pointer is set to the address in Class3.Pointer which is NULL. The while loop iterates, but it's valid pointer condition is FALSE and so we exit the loop. Hey presto, we've just called the Render() method on every one of our classes in the linked list without hard-coding their names!Hopefully you can now see the value of SetNextObject() and GetNextObject(). With the codebase we currently have, a linked list is only slightly useful. However when we move into 3D coding with multiple objects, matrix stacks and transformations, we will be able to group our objects in a clear hierarchy using this linked list as a basis. We need to do a lot more work on it before that's possible, though!
It doesn't end there, however. We've only addressed 2 of the 4 linked list methods, with SetPrevObject() and GetPrevObject() still to go. No doubt you can guess what these 2 methods are for, as they complete the functionality of our linked list code. In a sentance, they allow us to go up the list as well as down it. Technically what we've created is called a "doubly linked list" because we can traverse it in both directions, but to save space I'll just call it a linked list! Following on from Fig 5.2, a doubly linked list looks something like this:
Fig 5.3: A diagrammatic doubly linked list
As noted above, the main advantage of a doubly linked list over a single linked list is that we can traverse the list in both directions. Therefore if we only have a pointer to object 10 of a 20 object list, we can go backwards to the start of the list as well as forwards to the end. Note that the first and last objects in a doubly linked list are null pointers. If we wanted, we could set the first object's previous pointer to the address of the last object, and the last object's next pointer to the address of the first object. This would create a "circular linked list", where we would iterate through the list, reach the end and automatically retrieve a pointer to the start of the list. There are specific uses for this type of list which I won't go into here, as we're way off topic already!The last piece of the linked list puzzle is in the CD3DObject destructor. Should an object be destroyed while it's a member of a linked list, we need to make sure that the previous and next objects in the list are updated to link to each other, cutting out the destroyed object.
Fig 5.4: The destructor joins the adjacent classes
Figure 5.4 explains it pretty well. If we were to destroy Class2, we set Class3.PreviousPointer equal to Class2.PreviousPointer (which is a pointer to Class1). We then set Class1.NextPointer equal to Class2.NextPointer, which is a pointer to Class3. In doing so, we've joined Class1 and Class3, and cut Class2 out the linked list ready to be destroyed. We need to add a little logic in case we're destroying a class at the start or end of a list (Class1 or Class3 in the above example), but the principle remains the same. Here's the CD3DObject destructor:
CD3DObject::~CD3DObject()
{
if(m_pszClassDesc)
delete [] m_pszClassDesc;
// If our previous object pointer is valid...
if(m_llpPrevObject)
{
//... and our next object pointer is valid
if(m_llpNextObject)
{
// set a link between the previous and next object, cutting out this object.
m_llpNextObject->m_llpPrevObject=m_llpPrevObject;
m_llpPrevObject->m_llpNextObject=m_llpNextObject;
}
else
{
// ...but the next object pointer is not valid, tell the previus object it's the last
// one in the linked list.
m_llpPrevObject->m_llpNextObject=NULL;
}
}
// But if our previous object pointer is not valid...
else
{
// ...and our next object pointer is valid...
if(m_llpNextObject)
{
// tell it that it's the first object in the linked list.
m_llpNextObject->m_llpPrevObject=NULL;
}
}
}
Rather than explain this code line by line, I'll leave it to you to step through. Take the following 3 situations, and walk yourself through the code. Situation 1, this class is object 10 of a 20 object linked list, so it has a valid pointer to a previous and next object. Situation 2, this class is object 1 of a 10 object list, so it has a NULL for the previous object pointer but a valid next object pointer. Situation 3, this class is object 10 of a 10 object list, so it has a valid previous object pointer but a NULL next object pointer. When you've walked yourself through the code you should be happy with how doubly linked lists work, and how this destructor works!
Just so you know, I've used another one of my butchered hungarian notation-isms. The "llp" in m_llpPrevObject stands for "Linked List Pointer". Another one for the list, eh ;)
One final note on the GetNextObject() and GetPrevObject() methods. In our previous classes the "Get" methods have all taken a pointer as a parameter, and used that parameter as the destination var to fill with data. For example:
CD3DSprite::GetModulateColour(D3DCOLOR* colour)
{
*colour=m_ModulateColour;
}
However, with the linked list methods I've taken a slightly different approach. The methods still accept a pointer as a parameter, but I've provided a default parameter of NULL. I also use the return value of the methods to return the pointer requested. Here's the code for GetNextObject():
CD3DObject* CD3DObject::GetNextObject(CD3DObject** pD3DObject)
{
if(*pD3DObject)
*pD3DObject=m_llpNextObject;
return m_llpNextObject;
}
As you can see, the method first checks to see if the pD3DObject parameter is a valid pointer. If it is, it's set equal to the class member m_llpNextObject. Finally, regardless of whether the pD3DObject parameter is a valid pointer or not, the value of m_llpNextObject is returned. The immediate benefit of this is that you can use as a parameter in functions, making the following type of code possible:
pThisObject->GetNextObject()->SetPrevObject(pThisObject->m_llpPrevObject);
Nice and unclear, eh! In the example code above, we have a pointer to an existing CD3DObject (or a derived class). This pointer is called pThisObject. We use GetNextObject() to retrieve a pointer to the next object in the linked list. When we have that pointer, we use SetPrevObject() to set the class instance's previous object in the linked list. In one single line of code, we've cut pThisObject out of the linked list.
Now that you understand linked lists, it's time to explain what that CD3DObject::SetRTTI() method is all about. Consider the linked list for a second. For it to work, we a pointer to the base class to access the classes derived from it. This means that whilst we can index through all the classes, we're doing it blind - ie: we don't know what the class actually is. Our workaround for this is a custom Run Time Type Information system. We create an enum that holds identifiers for each of our D3D class types, add the enum as a member var to the CD3DObject class, and set the enum every time we create an instance of a CD3DObject derived class. This probably makes more sense in code. First, the enum declaration from CD3DObject.h:
enum D3DRTTI {UNKNOWN, SPRITE, RASTERFONT};
That's simple enough, we've got entries for our 2 classes plus one UNKNOWN to be used for initialisation purposes. Next, we add our RTTI to our CD3DObject class:
private:
D3DRTTI m_nObjectType; // Holds the class type (run time type info)
And finally we set the RTTI in our derived class constructors:
CD3DSprite::CD3DSprite()
{
SetClassDesc("Sprite class");
SetRTTI(SPRITE);
It doesn't get much easier than this! By setting the RTTI for the class we are able to do the following at runtime (pseudocode):
while(pIterationPointer)
{
pIterationPointer->Render(Device, BackBuffer);
pIterationPointer=pIterationPointer->GetNextObjectInList();
switch(pIterationPointer->GetRTTI())
{
case SPRITE:
// custom sprite code
break;
case RASTERFONT:
// custom font code
break;
default:
// other action
break;
}
}
Again, because we've only created a couple of simple classes the value of the RTTI code isn't immediately obvious. Trust me though, in future it will become a massive help. The only caveat is that when we create new object classes, we need to update the enum list in CD3DObject.h.Well, if you managed to stay with me through that lot you're doing well. The code is probably a little confusing just to read on paper, so I recommend you download the workspace zip, compile the code and build some linked lists with it just to see how it works. We've written a fair amount of code that we won't need right away, but I chose to present this tutorial now as we'll be moving onto 3D in the next series, and the less new code the better! Sit back, relax, and be proud of yourself - you've just written about 50% of your first scene management system.
In the future I'll be using the code as contained in the workspace zip for this tutorial. I suggest that you move any older code to a backup folder and use the new code from the .zip.
I'll leave you with a few exercises to try at your leisure:
1. I've provided SetNextObject() and SetPrevObject() methods, but these only set the pointer required. Implement 2 new methods, SetNextObjectClean() and SetPrevObjectClean() for example, which set the pointer and also update the appropriate objects in the linked list to include the new object. For example:
Object1.PrevObject = NULL
Object1.NextObject = Object2
Object2.PrevObject = Object1
Object2.NextObject = Object3
Object3.PrevObject = Object2
Object3.NextObject = NULLCalling Object2.SetPrevObjectClean(pNewObject) should produce the following result:
Object1.PrevObject = NULL
Object1.NextObject = pNewObject
pNewObject.PrevObject = Object1
pNewObject.NextObject = Object2
Object2.PrevObject = pNewObject
Object2.NextObject = Object3In other words, the object is cleanly inserted into the linked list.
2. Create a linked list containing a couple of CD3DRasterFont and CD3DSprite instances. Add an iteration loop into the main Render() function to index through the linked list and render each object.
3. This one will require some time and research! Because you can't see the debug window when running a D3D app in fullscreen mode, add to the CD3DObject::Error() method some code to output all the debug information to a file. You could add a .html extension to the file, and output the debug information with HTML tags (bold, italics, colours, fontsize etc) so you can quickly see which errors are critical, and which have been dealt with. The better debugging system you implement now, the easier your 3D coding will be!
4. You may want to expand your debugging system even further. Add some code to switch on the RTTI type, and output class-specific information. For example, if the object is a CD3DRasterFont class instance, output the number of characters per line, letter width and letter height.
Scroll down for some hints!
Click here to send feedback, bug reports, comments and ideas etc!
Hint for question 1: Most of the code you need is in this tutorial, either as pseudocode or in the destructor.
Hint for question 2: This code is shown in pseudocode in the tutorial
Hint for question 3: You may want to use ofstream with #include <fstream.h>, or you may choose your own method of file output. MSDN will help!
Hint for question 4: This code is shown in pseudocode in the tutorial
Corrections, Additions, Errata
06/08/2002: Chris Caldwell pointed out that I got a bit enthusiastic with the copy and paste, and messed up the GetNextObject()/GetPrevObject() accessor functions. Note that the bug only occurred if you passed the functions a CD3DObject pointer, if you just used the return value of the functions it worked fine. I've changed the function parameters to accept a ** (pointer to a pointer), which therefore means that you now call the function as follows:
CD3DObject* pObject;
MyObject.GetNextObject(&pObject);
The following code was not affected by this bug:
CD3DObject* pObject = MyObject.GetNextObject();
Thanks Chris!