When evolving my rough particle system into a (very targeted) game engine, I started to learn why object oriented design has gone out of favor for many game engines. From a design standpoint, the particle system was a learning project where I tried to leverage C++ inheritance as much as I could. For the game engine, I attempted to model my game objects using an inheritance structure as well. I felt that in the real world, nearly everything falls into some sort of classification, often with distinct parent-child relationships. However, as I began adding game specific objects to my engine, I realized that not only is it prohibitively difficult to try to model the real world (the way I felt it should be modeled) due to the sheer volume and complexity, but game objects are simply an approximation of real world occurrences, and as such they tend to “cheat” to achieve a certain effect. Objects in a game can choose to be invisible, or defy the laws of physics. This basically breaks whatever elegant classification structure I had planned. Luckily, this can be addressed by converting from an “is-a” model (where an Object “is a” physics-based object) to a “has-a” model, (where an object “has a” physics component). In this model, objects will contain pointers to optional collections of data and functionality.
Here is an example. Lets take a simple shape, like a cube, and distill the common information that defines a cube into a parent “Cube” class. This is basically the geometric configuration of a cube (vertices, position, orientation). Naturally we would probably want to render this cube so we can see it on the screen, so Cube will come equipped with a “Draw()” function, so any child of Cube will know how to draw itself to the screen. Cube will inherit from a Shape, which declares an interface for Draw. Now one obvious way to think of a cube is as a solid, physical object that you can pick up and throw, it bounces around, and you can interact with it. In programming land, that means that this cube has a bunch of physics data. If I want to put this sort of cube in my world, it’s as simple as inheriting from the Cube class and adding some physics specific information. Let’s call this class “CubePhysics”. But what happens when you want to use the cube shape as a bounding box, with collision response, but obviously as a bounding box it is not visible to the player. Well, now things start to look out of place because your CubePhysics object inherits from a Cube object, which has render information defined in it. Sure you could use it and just ignore the render data, but this results in a lot of wasted data, and can be extremely confusing if you only partially define your object.
Taking this another step further, what if you wanted a Cube with some kind of AI functionality? In a is-a model, you would inherit from a definition of a Cube and add AI information and functionality. However, which definition of Cube do you inherit from? If you only inherit from Cube, you don’t get physics. However, if you inherit from CubePhysics you get the physics, but potentially extraneous render functionality. And you certainly don’t want to create two separate classes that each inherit from a Cube object and CubePhysics object.
Another example would be the notion of a Particle in the particle system. A Particle is some base class of whatever is intended to be spawned by the emitter. Lets assume that a Particle only requires render information, and that the emitter handles the physics for us. Should Particle inherit from Cube? Certainly not; what if we wanted to use a sphere or a conic shape?
When starting with a small subset of functionality, the typical object inheritance model may seem like a nice, elegant object model. My particle system for example initially only had a Particle class, and a Cube class which inherited from Particle. Simple polymorphism allowed me to use any arbitrary shape in the emitter. Unfortunately, as the project expanded, this inheritance structure exploded. I simply needed an easier way to pick and choose which laws and properties I wanted to utilize, and which I wanted to ignore. This boils down to using a “has-a” object model, where a particular game object may have physics or it may not, and likewise for rendering, AI, animation, etc. The book Game Engine Architecture compares and contrasts the is-a and has-a object models, citing engines such as the Unreal 2 engine as an example of how inheritance structures can become complex and unwieldy. The exact implementation of the has-a object model can vary greatly between game engines, and the following implementation is still a work in progress.
Here is a look at some of the classes I had when developing using an “is-a” model, and what it was starting to look like:
And here is the same functionality refactored using a “has-a” model:
As you can see, the new model significantly cuts down on the specialization and inheritance chain explosion. This model allows for any game object to selectively choose what aspects of interaction they will simulate.
Finally, for those that like code, here’s a look at the new game objects.
class ObjectData
{
public:
ObjectData();
ObjectData(const btVector3& pos,
float scale,
const D3DCOLORVALUE& color,
short collisionGroup = COL_NOTHING,
short collisionMask = COL_NOTHING,
const btVector3& vel = btVector3(0,0,0));
~ObjectData();
void Draw(const D3DXMATRIX& matVP);
void Update(float dt);
RenderData* m_pRenderData;
PhysicsData* m_pPhysicsData;
btVector3 m_pos;
btQuaternion m_rot;
float m_scale;
D3DCOLORVALUE m_color;
};
struct InitRenderData
{
const D3DVERTEXELEMENT9* pVertexDeclElements;
const Vertex* pVertices;
const short* pIndices;
const DWORD NumVertices;
const DWORD NumPrimitives;
const DWORD NumIndices;
};
class RenderData
{
public:
RenderData(IDirect3DDevice9* pDevice, const InitRenderData& initRenderData);
~RenderData();
void Draw(const D3DXMATRIX& transform);
inline DWORD GetNumPrimitives(){return m_InitRenderData.NumPrimitives;};
inline DWORD GetNumVertices(){return m_InitRenderData.NumVertices;};
inline DWORD GetNumIndices(){return m_InitRenderData.NumIndices;};
inline IDirect3DVertexDeclaration9* GetVertexDecl(){return m_pVertexDecl;};
inline IDirect3DVertexBuffer9* GetVB(){return m_pVB;};
inline IDirect3DIndexBuffer9* GetIB(){return m_pIB;};
private:
IDirect3DDevice9* m_pDevice;
const InitRenderData m_InitRenderData;
IDirect3DVertexDeclaration9* m_pVertexDecl;
IDirect3DVertexBuffer9* m_pVB;
IDirect3DIndexBuffer9* m_pIB;
};
struct InitPhysicsData
{
btCollisionShape* pBtCollisionShape;
short CollisionGroup;
short CollisionMask;
btScalar Mass;
btVector3 InitPos;
btQuaternion InitRot;
btVector3 InitVel;
};
class PhysicsData
{
public:
PhysicsData(btDiscreteDynamicsWorld* pBtDynamicsWorld, const InitPhysicsData& initPhysicsData);
~PhysicsData();
void UpdatePosition(const btVector3& pos);
void SetPosition(const btVector3& pos);
void ResetPhysics();
void Draw(const D3DXMATRIX& matVP);
void RegisterPhysics();
void UnregisterPhysics();
btCollisionObject* GetCollisionObject();
btDiscreteDynamicsWorld* m_pBtDynamicsWorld;
const InitPhysicsData m_InitPhysicsData;
btDefaultMotionState* m_pBtMotionState;
btRigidBody* m_pBtRigidBody;
};