I hope they don't kill me. After the "m_" fiasco, I wonder how much I can
push...
It's time to use C++ to write object oriented programs. I started looking
into implementing the scene graph into our code and was stopped stone cold
because of some intersting C++ practices. In order to add the scene graph
cleanly, I'd have to rip apart 30% of the project and put it back together
again. Not something I want to do for a day project. Instead, I'm going the path
of least resistance and instead instituting some hard core C++ into the project.
What's this hardcore C++ bomb you may ask?
Abstract Interfaces (boom!)
Abstract interfaces actually aren't new to the project. DX9 is full of them.
What's new is that instead of using them, we're going to start writing them.
That's the hard part. When you use an abstract interfaces usually you're worried
about 3 functions:
It looks pretty simple right. It is and that's part of the beauty of using
abstract interfaces. The hard part is writing them. Not that writing an
interface is hard is physically hard. Writing them properly is the trick. It's
like using C++ to write object oriented code. It's not hard, it's hard to do
properly. I mean, harder to write C++ that looks and functions like C++ instead
of using C++ to write C. Get it?
What is an Abstract Interface?
An abstract interface is just a struct that contains nothing but pure virtual
functions.
[ISceneObject.h]
//
--------------------------------------------------------------------------------
// Interface for a scene object
//
--------------------------------------------------------------------------------
struct ISceneObject
{
virtual HRESULT Init(LPDIRECT3DDEVICE9 pDevice) = 0;
virtual HRESULT Restore(LPDIRECT3DDEVICE9 pDevice) = 0;
virtual HRESULT DestroyDeviceObjects() = 0;
virtual HRESULT Render(IScene* pScene) = 0;
virtual HRESULT Destroy() = 0;
}; |
So what? Why should I use it?
There are some interesting properties of the abstract interface. These are
some of the advantages.
Hidden implementation:
Notice that all the implementation details are hidden. This prevents programmers
from doing C-ish things like directly accessing data members. You can't because
you can't even see them. Now you're not bound to a particular implementation
that requires a certain type of member variable. You're only bound to the
interface you see.
Swapability:
Because you know that each scene object has at least this interface, you can
write code to this interface and not worry about what's behind it. You can swap
an objects behavior at run time as long as the new behavior follows the
interface. For example, let's say we have a ship class that renders differently
depending on the type of hardware the user has (a real situation in our case).
You don't have to write your game code with "if(bOldHardware)" all over the
place. All you need is a different ship class or render code for the different
levels of hardware. This can also be extended to user created objects, or
objects that perform the same functions, but have completely different
implementations.
Shorter compile times:
Since the implementation details are taken out of the public interface, you
don't have to recompile the entire project if you change a data member of a
particular class. Have you ever noticed how changing something from a USHORT to
a SHORT in a low-level class required a full rebuild of the project. Yeah, it
didn't change the way the code functioned, it didn't even change the size of the
object footprint. Still, you touched the file and everyone heard you do it. With
interfaces, you only have that kind of problem if you change the interface.
Something you should think hard about before doing when the interfaces become
mature.
Upgradeability:
This may affect us in the future. Because you write to an interface, an abstract
class with no data members, binary compatibility isn't an issue. You can upgrade
the implementation an interface without requiring a recompile or relink of any
existing code. If we were using .dlls, this means that a .dll can be updated on
the users machine without having to recompile an executable or any other module
that required the dll. You can even add functions to the interface as long as
you don't change the order of the existing functions. This also means you can't
add functions to the middle of the interface.
Traits:
Another thing you can do with abstract interfaces is implement an object as a
series of traits. Each trait is an abstract interface. Look here:
[IMovingObject.h]
//
--------------------------------------------------------------------------------
// Moving object interface
//
--------------------------------------------------------------------------------
struct IMovingObject
{
virtual void SetSpeed(float fSpeed) = 0;
virtual void AddDestination(IDestination* pDest) = 0;
virtual void Start() = 0;
virtual void Stop() = 0;
virtual bool IsDone() = 0;
};
[IShip.hpp]
//
--------------------------------------------------------------------------------
// Moving object interface
//
--------------------------------------------------------------------------------
#include "ISceneObject.h"
#include "IMovingObject.h"
struct IShip : public IMovingObject, public ISceneObject
{
// IMovingObject functions
// ISceneObject functions
// IShip functions
};
[Ship.cpp]
#include "IShip.h"
class CShip : public IShip
{
// Ship implementation here.
}; |
You've defined a ship as a moving scene object. Now, you can pass that ship
around to any function that takes an IMovingObject* or an ISceneObject* and
it'll behave properly. The function that uses the pointer doesn't even have to
know it's an IShip. It doesn't care. Pretty neat stuff right?
Proper design:
Using interfaces, you can enforce strict design rules on your subsystems. Since
you publish the interface that will be used, everyone must make sure their
object supports at least that interface. It's a compile time or link time error
if it doesn't. Not a runtime error. Better to catch these problems before the
user does.
I like it. What's the catch?
Nothing comes for free. There are a few gotchas. Not many, and none
insurmountable.
You have to type more:
Not only do you have to create the class you're using, but you also have to
create the interface. This is usually a cut and paste job, but you still have to
type more. There's also the support classes to consider. You may have to create
a class factory. Since the interfaces are abstract, you can't instantiate them
by themselves. You often have to create a specialized creation function to get
one of these guys. If you've used DX9, you're used to that.
D3DXCreate????????????.
It's harder to debug:
Remember, you've hidden the implementation. You can't actually see the
implementation in the debugger. Well, you can, it just requires some effort.
Imagine that I get an IMovingObject* in a function. If I want to see what it's
implementation data is, I have to cast it in the Watch Window. For example, if
it's a ship then I have to cast it to a ship (CShip*)pMovingObject. Not a big
deal really, but it can really throw someone off quickly.
You CAN go hog wild:
It's easy to get yourself bogged down in code when you use interfaces. Using
them as traits is a good way to keep you coding one class for much longer than
it should. You don't want class hierarchies where adding a new simple object to
the scene requires you to derive from 6 different interfaces. It can happen so
just watch out for it.
It's real OOD:
If you're not used to writing interfaces, it can be a real obstacle to having
fun on the job. The upside is that once you start using them, you're writing
real object oriented code. You'll be looking at classes by the public
interface they expose, not which member variable you have to tweak. Pretty soon,
you'll be writing functions that do more than just set a variable. You'll have a
great time discovering how to model classes. You'll then get a kick out of
modeling the interactions between classes, then subsystems, then entire systems,
then the world!
You've convinced me. What next?
Nobody has to worry about this yet. I'll handle the initial ground work.
Don't forget it though. As we expand from the initial ground work, knowing how
to write and use interfaces will become more important. If we don't do this
properly at the beginning, the system becomes a mish-mash if mixed programming
and irregular, hard to reproduce bugs.
I'll start going through our objects, classifying them and coming up with the
initial scene architecture, then it's just a matter of using the mega shoe horns
to fit our existing objects into the graph as quickly and painlessly as
possible. It may not be clean, but at least it'll be a big enough mess that
we'll be forced to clean it up later because it looks so damn ugly.