Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I am creating a relatively simple game engine in C++ and Qt. I am using Irrlicht for graphics, and as of now, I will not be using any physics or audio libraries. For time's sake, I am using Irrlicht's built-in bounding-box for collision detection and neglecting audio.

Here is what I would like:

GameObject: An update-able container that has guaranteed components. Note that an update-able object simply means there exists a virtual function, update, that allows the object to update with respect to the time from the last rendered frame. Updateable is an Abstract Class.

  • TransformComponent: Encapsulates the responsibility of storing position, rotation and size. Whenever a property is changed, TransformComponent emits a signal, informing its listeners that the property has changed by a given delta-value. GameObject's TransformComponent keeps its other components aligned. For example, changing the position of a GameObject's TransformComponent will change the position position of the GameObject's graphical representation and its physics-related information. This information is relayed via signals and slots that GameObject handles at instantiation-time. Note that the components know nothing of each other -- GameObject simply handles the following task: signals from a given component need to talk to slots of another component.
  • GraphicsComponent: Encapsulates the responsibility of rendering and playing animations. GraphicsComponent has a TransformComponent, which stores the graphical object's (Irrlicht-functionality) position, size and rotation. Note that updating a GameObject's TransformComponent will update the GameObject's GraphicsComponent's TransformComponent. However, these *TransformComponent*s are loosely coupled in the sense that it is possible to update the position of GraphicsComponent's TransformComponent without updating the position of the GameObject's TransformComponent. Again, this is all done via event-listeners.
  • PhysicsComponent: Encapsulates the responsibility of determining when it has collided with another GameObject. This is where I am having trouble.

In order to do this, GameObject must know about PhysicsComponent, and PhysicsComponent must know about GameObject. Not only is this is a circular reference, which denotes strong coupling, but one of my Components must know about its aggregate class, which contradicts the entire purpose of black-box reuse.

For the curious, here is some example code for how my GameObject works:

class Player : public GameObject
{

    Q_OBJECT

    public:
        Player();
        ~Player();
    protected slots:
       void onUpdate(float deltaTime); // virtual function from GameObject
       void onEnteredCollision(GameObject* gameObject) // An imaginary function that I would like to have.
};

Player::Player() : GameObject() {
    // Initialize player-specific member variables here.
    // Here is where I would setup some specific signals and slots.
    // Below I show how I would connect Player to PhysicsComponent's onEnteredCollision signal.
    connect(getPhysicsComponent(), SIGNAL(onEnteredCollision(GameObject*), this, SLOT(onEnteredCollision(GameObject*))); // Now our method, onEnteredCollision(GameObject* gameObject) is called whenever a GameObject collides with Player.
}

void Player::onUpdate(float deltaTime) {
    // An example of how to move the character:
    getTransform()->translate(0, 10 * deltaTime, 0); // Moves Player 10 units per second in the positive y-direction.
    // An example of how to move the character's graphical object:
    getGraphics()->getTransform()->translate(10 * deltaTime, 0, 0); // Moves Player's graphical object 10 units per second in the positive x-direction.
}

void Player::onCollisionEntered(GameObject* gameObject) {
    // Collision-code goes here.
}

(Note that I could continue using component-based paradigms to encapsulate the idea of capturing Player's collision into a PlayerCollisionComponent. However, the above code more than exemplifies what I would like to do, and I left that further abstraction out for simplicity.)

The issue with how this works is as aforementioned. PhysicsComponent must have some logic that calculates when its physics object (Collider, for example) has collided with another GameObject's Collider, and then inform (through signals) which GameObject is being collided with. This makes PhysicsComponent strongly coupled with GameObject and also creates an ugly C++ problem with circular dependencies. In a way, it makes sense that the two are coupled, but I am convinced there must be a better alternative.

Does anyone have any feedback or experience?

Thanks in advance,

share|improve this question

1 Answer 1

up vote 1 down vote accepted

What you're looking for is a messaging system that can communicate between components and objects, I've detailed such a system already, here:

Interaction between engine parts

The structure here is a Scene Manager that contains all Objects in a scene. Objects have a list of components that can listen for messages. Both the object and its components have a chance to listen for each message. The following code sample is generic (engine-agnostic).

Here's the code:

#include <iostream>
#include <stdio.h>

#include <list>
#include <map>

using namespace std;

struct Vector3
{
public:
    Vector3() : x(0.0f), y(0.0f), z(0.0f)
    {}

    float x, y, z;
};

enum eMessageType
{
    SetPosition,
    GetPosition,    
};

class BaseMessage
{
protected: // Abstract class, constructor is protected
    BaseMessage(int destinationObjectID, eMessageType messageTypeID) 
        : m_destObjectID(destinationObjectID)
        , m_messageTypeID(messageTypeID)
    {}

public: // Normally this isn't public, just doing it to keep code small
    int m_destObjectID;
    eMessageType m_messageTypeID;
};

class PositionMessage : public BaseMessage
{
protected: // Abstract class, constructor is protected
    PositionMessage(int destinationObjectID, eMessageType messageTypeID, 
                    float X = 0.0f, float Y = 0.0f, float Z = 0.0f)
        : BaseMessage(destinationObjectID, messageTypeID)
        , x(X)
        , y(Y)
        , z(Z)
    {

    }

public:
    float x, y, z;
};

class MsgSetPosition : public PositionMessage
{
public:
    MsgSetPosition(int destinationObjectID, float X, float Y, float Z)
        : PositionMessage(destinationObjectID, SetPosition, X, Y, Z)
    {}
};

class MsgGetPosition : public PositionMessage
{
public:
    MsgGetPosition(int destinationObjectID)
        : PositionMessage(destinationObjectID, GetPosition)
    {}
};

class BaseComponent
{
public:
    virtual bool SendMessage(BaseMessage* msg) { return false; }
};

class RenderComponent : public BaseComponent
{
public:
    /*override*/ bool SendMessage(BaseMessage* msg)
    {
        // Object has a switch for any messages it cares about
        switch(msg->m_messageTypeID)
        {
        case SetPosition:
            {                   
                // Update render mesh position/translation

                cout << "RenderComponent handling SetPosition\n";
            }
            break;
        default:
            return BaseComponent::SendMessage(msg);
        }

        return true;
    }
};

class Object
{
public:
    Object(int uniqueID)
        : m_UniqueID(uniqueID)
    {
    }

    int GetObjectID() const { return m_UniqueID; }

    void AddComponent(BaseComponent* comp)
    {
        m_Components.push_back(comp);
    }

    bool SendMessage(BaseMessage* msg)
    {
        bool messageHandled = false;

        // Object has a switch for any messages it cares about
        switch(msg->m_messageTypeID)
        {
        case SetPosition:
            {               
                MsgSetPosition* msgSetPos = static_cast<MsgSetPosition*>(msg);
                m_Position.x = msgSetPos->x;
                m_Position.y = msgSetPos->y;
                m_Position.z = msgSetPos->z;

                messageHandled = true;
                cout << "Object handled SetPosition\n";
            }
            break;
        case GetPosition:
            {
                MsgGetPosition* msgSetPos = static_cast<MsgGetPosition*>(msg);
                msgSetPos->x = m_Position.x;
                msgSetPos->y = m_Position.y;
                msgSetPos->z = m_Position.z;

                messageHandled = true;
                cout << "Object handling GetPosition\n";
            }
            break;
        default:
            return PassMessageToComponents(msg);
        }

        // If the object didn't handle the message but the component
        // did, we return true to signify it was handled by something.
        messageHandled |= PassMessageToComponents(msg);

        return messageHandled;
    }

private: // Methods
    bool PassMessageToComponents(BaseMessage* msg)
    {
        bool messageHandled = false;

        std::list<BaseComponent*>::iterator compIt = m_Components.begin();
        for ( compIt; compIt != m_Components.end(); ++compIt )
        {
            messageHandled |= (*compIt)->SendMessage(msg);
        }

        return messageHandled;
    }

private: // Members
    int m_UniqueID;
    std::list<BaseComponent*> m_Components;
    Vector3 m_Position;
};

class SceneManager
{
public: 
    // Returns true if the object or any components handled the message
    bool SendMessage(BaseMessage* msg)
    {
        // We look for the object in the scene by its ID
        std::map<int, Object*>::iterator objIt = m_Objects.find(msg->m_destObjectID);       
        if ( objIt != m_Objects.end() )
        {           
            // Object was found, so send it the message
            return objIt->second->SendMessage(msg);
        }

        // Object with the specified ID wasn't found
        return false;
    }

    Object* CreateObject()
    {
        Object* newObj = new Object(nextObjectID++);
        m_Objects[newObj->GetObjectID()] = newObj;

        return newObj;
    }

private:
    std::map<int, Object*> m_Objects;
    static int nextObjectID;
};

// Initialize our static unique objectID generator
int SceneManager::nextObjectID = 0;

int main()
{
    // Create a scene manager
    SceneManager sceneMgr;

    // Have scene manager create an object for us, which
    // automatically puts the object into the scene as well
    Object* myObj = sceneMgr.CreateObject();

    // Create a render component
    RenderComponent* renderComp = new RenderComponent();

    // Attach render component to the object we made
    myObj->AddComponent(renderComp);

    // Set 'myObj' position to (1, 2, 3)
    MsgSetPosition msgSetPos(myObj->GetObjectID(), 1.0f, 2.0f, 3.0f);
    sceneMgr.SendMessage(&msgSetPos);
    cout << "Position set to (1, 2, 3) on object with ID: " << myObj->GetObjectID() << '\n';

    cout << "Retreiving position from object with ID: " << myObj->GetObjectID() << '\n';

    // Get 'myObj' position to verify it was set properly
    MsgGetPosition msgGetPos(myObj->GetObjectID());
    sceneMgr.SendMessage(&msgGetPos);
    cout << "X: " << msgGetPos.x << '\n';
    cout << "Y: " << msgGetPos.y << '\n';
    cout << "Z: " << msgGetPos.z << '\n';
}
share|improve this answer
    
The messaging paradigm is taken care of by Qt's SIGNALs and SLOTs. However, the design pattern you are using (sort of like a Builder/Factory), in addition to referencing GameObjects by UID, can solve this problem. It's not the prettiest solution in the world, but it certainly is nifty and unique! When I am done implementing it (I just wrote it out on paper), I'll respond back with my code. Thanks, Nic! –  Alex May 2 '12 at 3:56
    
That sounds good, I'd love to see alternative techniques. The sample above is cut down and a bit rough, I've used cleaned up versions that include filtering of messages so they only go to the components that listen for them, and message types so different messages can be send and returned in different ways. By signals and slots are you referring to boost? –  Nic Foster May 2 '12 at 4:55

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.