From an “is-a” to a “has-a” Object Model

I’ve been conspicuously quiet the last couple of weeks and this is why. I have been evolving my rough particle system into a (very targeted) game engine. From a design standpoint, the particle system was a learning project where I tried to leverage as much as I could from the natural mechanisms of C++ to develop an object model for what I planned on turning into a more general purpose game engine. I decided to try and model my game objects using an inheritance structure. 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 chose the 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 to a “has-a” object model, where 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 that can demonstrate 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:

Black arrows denote inheritance.

And here is the same functionality refactored using a “has-a” model:

Clear arrows denote ownership by pointer. Black arrows denote inheritance.

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;
};

Leave a Comment

Tags:

April 21, 2011 Games