Not too long ago I was coding a game engine. Oh, it had everything; sound was automated, 3d meshes of varying formats were loaded in nicely consolidated classes, I had font classes and sprites for 2d animation, and all in all it was about an eight thousand-line project. It was fairly solid, had never crashed on me, and I thought I was ready to create a game.
Then I ran it through Microsoft's Visual C++ debugger. Darn that Microsoft. When the program shut down, a half-dozen warning messages showed up in the debug output. It was just a bunch of random hexadecimal numbers and some techno-crap jargon that compilers give you. I thought nothing of it until it started happening every time I ran the program.
It was telling me that I had memory leaks. Not too many of them, just a few. A half-dozen leaks of a few kilobytes apiece. It was nothing a modern machine couldn't handle, and my Windows 2000 machine cleaned up very nicely after my program.
Then, as I do far too often, I caught myself. There I was again, thinking I could just blow off a few warnings and random crashes. I had promised myself that this particular engine was going to be done right. Completely object-oriented and catching all possible errors. It was the first time I'd slowed myself down and forced myself to code an entire large project the right way.
So I set myself to finding those memory leaks. I scoured all my code, looked at every place I was doing a 'new' and making sure there was a corresponding 'delete.' I started using those useful SAFE_DELETE and SAFE_DELETE_ARRAY macros that are in the DirectX samples (check if the pointer is NULL, then delete it if it isn't and set it NULL). I still, once in awhile, got those memory leaks.
So I slowed down, took a good, long look at my code and my class structure, and over the course of the next few days, came up with this near-foolproof solution to my problem.
My entire game engine was, at some level, contained within or managed by my Cb3dEngine class.
My Engine class contained:
- A SoundSystem class, which managed my sounds and music classes.
- A pointer to a ViewportSet class, which in turn contained:
- A series of pointers to Viewport classes, which in turn contained:
- A series of pointers to renderable objects (meshes, text, sprites...)
- A series of lights
- A ParticleSystem class, which contained all of the particle emitters, particle textures, animations, etc.
- A Keyboard, Mouse, and Joystick class It seemed to be fine. My initialization could be extremely short, allowing me to develop DirectX applications very quickly. For instance, my initialization could have looked like this:
Just like that, I was up and running in a 640x480 windows DirectX application with a keyboard and a mouse and a Milkshape mesh all ready to go, lit by my one light. Everything was wonderful, except that somehow some of my objects were not getting destroyed during the execution of the program.
InitSettings.UpdateFrame = MainLoop; //app-defined main loop
InitSettings.KeyHandler = KeyHandler; //app-defined handler functions
InitSettings.MouseHandler = MouseHandler;
InitSettings.Hinst = hInst;
InitSettings.Windowed = true;
InitSettings.Width = 640;
InitSettings.Height = 480;
Cb3dEngine* g_pB3DEngine = new Cb3dEngine;
Cb3dViewportSet* MainSet = new Cb3dViewportSet();
Cb3dMilkshapeMesh* msm = new Cb3dMilkshapeMesh("mesh.txt");
Cb3dMeshObject* msmObject = new Cb3dMeshObject(msm);
Light.Type = D3DLIGHT_DIRECTIONAL;
Light.Diffuse.r = Light.Diffuse.g = Light.Diffuse.b = 1.0f;
Light.Specular.r = Light.Specular.g = Light.Specular.b = 1.0f;
Light.Ambient.r = Light.Ambient.g = Light.Ambient.b = 0.3f;
Light.Direction = D3DXVECTOR3(1,0,0);
One problem was immediately apparent to me with this setup. I honestly had no idea who owned the pointers. For instance, take the Mesh class. I created it with a 'new' in my initialization procedure. Then I created a MeshObject, which is what controls where the Mesh is placed, how its animations are done, and so on. This way I only need one Mesh of a person to render an entire army, saving enormously on memory. The problem is, I passed the pointer to the Mesh into the constructor of the MeshObject, and so immediately there are two pointers to the Mesh; the application has one, and the MeshObject has one.
Now, in this case it is fairly obvious that the application should delete the Mesh and then the MeshObjects. But let's have a look at the SoundSystem class. I create a new Sound object and pass it into the SoundSystem class, where it is stored to be played later. Now the application has a pointer to the Sound, and so does the single SoundSystem class. Who deletes this one?
It became apparent to me that I was writing a game engine. The game code itself should never have to deal with any pointers to random engine elements, like vertex arrays or meshes or sounds.
From previous observations, the solution should be simple - just have the engine create and destroy all of its components. However, this is a much more complicated task than it sounds like. Let's look what I've done to make my engine totally self-reliant.
The first thing that you need to do is to make every single constructor and destructor of every major engine element protected or private. At first, that sounds ridiculous. If the constructor and destructor are protected or private, nothing could create or destroy the object but itself! That is not entirely true. Those of you familiar with design patterns may recognize this technique from the Singleton design pattern. In the Singleton design pattern, the constructor is made private and it is called from a static member function. So, our engine components could be created from static creation methods. However, there is another way to access private and protected constructors and destructors. It is through friend classes.
So, the second thing you need to do is define the following macro:
Now, go through each one of your engine components (sounds, meshes, objects of any sort) and put a CREATOR into their class declarations. For instance, my Sound object now looks like:
#define CREATOR(p) friend class p
Since the Sound class has declared that the SoundSystem class is its friend, the SoundSystem can create sounds. Along those same lines, my engine class can create my Keyboard, Mouse, Joystick, Mesh, and MeshObject classes. More importantly, my engine class MUST destroy those same objects. I will no longer be able to accidentally create an object that will not get destroyed at the proper time. And no longer will the debugger quietly tell me that I have leaked memory. It will give me a full-blown error if the objects are not deleted by the time the program ends, because those destructors are private and can not be called except by the object's CREATOR.
In order to implement this fully, of course, more work is needed. First and foremost, you need to write creator functions for each constructor that you made private. For instance, my Engine class has the creator functions CreateMilkshapeMesh, Create3DSMesh, CreateMeshObject, CreateVertexBuffer, CreateIndexBuffer, and numerous others. Also, you need to store a linked list of all the pointers created by the engine. For instance, my engine class stores a list of all the meshes it creates, and then in the engine's destructor I destroy them all in turn, and in the correct order (i.e. before Direct3D is released).
One important thing to remember is that not every class needs to have a CREATOR. For instance, it is absurd to make the Engine's constructor and destructor private, because all programs using it will manually create it.
Draw your own conclusions ;-)
Well, let me tell you how it went. I implemented all of the changes I outlined above. I could swear that the actual lines of code that ran were exactly the same with or without the above changes. That is, I could swear that every object was getting created and destroyed in exactly the same order. But despite whatever I thought, I now get absolutely zero memory leaks.
Questions? Comments? Complaints? Hate mail? Email me at [email="email@example.com"]firstname.lastname@example.org[/email].
- A series of pointers to Viewport classes, which in turn contained: