Overclock.net › Forums › Software, Programming and Coding › Coding and Programming › Game Engine Architecture, Part 1: Creating an engine using an object<->component hierarchy
New Posts  All Forums:Forum Nav:

Game Engine Architecture, Part 1: Creating an engine using an object<->component hierarchy

post #1 of 7
Thread Starter 
I thought I would post some information about some of the basics behind creating your own game engine. It's against the TOS to link outside this side to other helpful information, sadly, so I'll just regurgitate it here.

This is advanced level C++, if you're new to programming then you should probably hit the books for a little while longer before diving into making a game engine.

I also have written a sample of this architecture in C# and in Scala, I will post either of those if there are requests for it. Anyway, here you go, enjoy.


My favorite game engine structure is the interface and object<->component model using messaging for communication between almost all parts.

You have multiple interfaces for main engine parts such as your scene manager, resource loader, audio, renderer, physics, etc.

I have the scene manager in charge of all objects in the 3D scene/world.

Object is a very atomic class, containing only a few things that are common to almost everything in your scene, in my engine the object class holds only position, rotation, a list of components, and a unique ID. Every object's ID is generated by a static int, so that no two objects will every have the same ID, this allows you to send messages to an object by its ID, rather than having to have a pointer to the object.

The list of components on the object is what gives that object its main properties. For example, for something that you can see in the 3D world, you would give your object a render component that contains the information about the render mesh. If you want an object to have physics you would give it a physics component. If you want something to act as a camera, give it a camera component. The list of components can go on and on.

Communication between interfaces, objects, and components is key. In my engine I have a generic message class that contains only a unique ID, and a message type ID. The unique ID is the ID of the object you want the message to go to, and the message type ID is used by the object receiving the message so it knows what type of message it is.

Objects can handle the message if they need, and they can pass the message on to each of their components, and components will often do important things with the message. For example, if you want to change and object's position you send the object a SetPosition message, the object may update its position variable when it gets the message, but the render component may need to message to update the position of the render mesh, and the physics component may need the message to update the physics body's position.

Here is a very simple layout of scene manager, object, and component, and message flow, that I whipped up in about an hour, written in C++. When run it sets the position on an object, and the message passes through the render component, then retrieves the position from the object.
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';
}

Edited by lordikon - 3/9/12 at 7:08pm
Foldatron
(17 items)
 
Mat
(10 items)
 
Work iMac
(9 items)
 
CPUMotherboardGraphicsGraphics
i7 950 EVGA x58 3-way SLI EVGA GTX 660ti GTX 275 
RAMHard DriveHard DriveHard Drive
3x2GB Corsair Dominator DDR3-1600 80GB Intel X25-M SSD 2TB WD Black 150GB WD Raptor 
Hard DriveOSMonitorKeyboard
2x 150GB WD V-raptor in RAID0 Win7 Home 64-bit OEM 55" LED 120hz 1080p Vizio MS Natural Ergonomic Keyboard 4000 
PowerCase
750W PC P&C Silencer CoolerMaster 690 
CPUGraphicsRAMHard Drive
Intel Core i5 2500S AMD 6770M 8GB (2x4GB) at 1333Mhz 1TB, 7200 rpm 
Optical DriveOSMonitorKeyboard
LG 8X Dual-Layer "SuperDrive" OS X Lion 27" iMac screen Mac wireless keyboard 
Mouse
Mac wireless mouse 
CPUGraphicsRAMHard Drive
i7-2600K AMD 6970M 1GB 16GB PC3-10600 DDR3 1TB 7200rpm 
Hard DriveOptical DriveOSMonitor
256GB SSD 8x DL "SuperDrive" OS X 10.7 Lion 27" 2560x1440 iMac display 
Monitor
27" Apple thunderbolt display 
  hide details  
Reply
Foldatron
(17 items)
 
Mat
(10 items)
 
Work iMac
(9 items)
 
CPUMotherboardGraphicsGraphics
i7 950 EVGA x58 3-way SLI EVGA GTX 660ti GTX 275 
RAMHard DriveHard DriveHard Drive
3x2GB Corsair Dominator DDR3-1600 80GB Intel X25-M SSD 2TB WD Black 150GB WD Raptor 
Hard DriveOSMonitorKeyboard
2x 150GB WD V-raptor in RAID0 Win7 Home 64-bit OEM 55" LED 120hz 1080p Vizio MS Natural Ergonomic Keyboard 4000 
PowerCase
750W PC P&C Silencer CoolerMaster 690 
CPUGraphicsRAMHard Drive
Intel Core i5 2500S AMD 6770M 8GB (2x4GB) at 1333Mhz 1TB, 7200 rpm 
Optical DriveOSMonitorKeyboard
LG 8X Dual-Layer "SuperDrive" OS X Lion 27" iMac screen Mac wireless keyboard 
Mouse
Mac wireless mouse 
CPUGraphicsRAMHard Drive
i7-2600K AMD 6970M 1GB 16GB PC3-10600 DDR3 1TB 7200rpm 
Hard DriveOptical DriveOSMonitor
256GB SSD 8x DL "SuperDrive" OS X 10.7 Lion 27" 2560x1440 iMac display 
Monitor
27" Apple thunderbolt display 
  hide details  
Reply
post #2 of 7
I used to be interested in programming years ago (5+), but switched my focus of studies... however, I have always been interested in seeing what some of the "end products" of these masterpieces look like.

While I know what you wrote may be quite "simple" compared to what we would find in something such as CryEngine3, I still found this to be an enjoyable read, and I loved looking at the code.

Fantastic addition to the community, as per usual, lordikon. Another +1 rep well deserved.
 
F@H
(14 items)
 
 
CPUMotherboardGraphicsRAM
Core i7 920 @ 4.0ghz EVGA x58 132-BL-E758-A1 2x EVGA 460 1 GB G.SKILL 3 x 2 GB 
Hard DriveOSMonitorKeyboard
Mushkin 40 GB SSD / WD Black 1 TB Windows 7 Home Premium 64-bit DCLCD 20.1" Logitech G15 
PowerCaseMouse
Antec TP 750 Antec 900 Logitech G5 
CPUMotherboardRAMCooling
i7 2700k ASUS Maximus Gene-Z z68 G.Skill 2133mhz Noctua NH-D14 
OSPowerCase
Ubuntu 10.10 BFG 650 Silverstone TJ08-E 
  hide details  
Reply
 
F@H
(14 items)
 
 
CPUMotherboardGraphicsRAM
Core i7 920 @ 4.0ghz EVGA x58 132-BL-E758-A1 2x EVGA 460 1 GB G.SKILL 3 x 2 GB 
Hard DriveOSMonitorKeyboard
Mushkin 40 GB SSD / WD Black 1 TB Windows 7 Home Premium 64-bit DCLCD 20.1" Logitech G15 
PowerCaseMouse
Antec TP 750 Antec 900 Logitech G5 
CPUMotherboardRAMCooling
i7 2700k ASUS Maximus Gene-Z z68 G.Skill 2133mhz Noctua NH-D14 
OSPowerCase
Ubuntu 10.10 BFG 650 Silverstone TJ08-E 
  hide details  
Reply
post #3 of 7
Thanks for sharing! +rep
I have an interest in game development sense I was younger (the first time i played Halo, my mind instantly fell in love and i instantly knew i wanted to design and develop software as my career) and i hope to be on a AAA dev team at some point in my career. Your code snippet helped open my mind even more on how an engine is built. Now if i can just find a few good books and time to set aside to begin practicing. Any suggestions on articles, forums or books?
post #4 of 7
Thread Starter 
Quote:
Originally Posted by Madog View Post

Thanks for sharing! +rep
I have an interest in game development sense I was younger (the first time i played Halo, my mind instantly fell in love and i instantly knew i wanted to design and develop software as my career) and i hope to be on a AAA dev team at some point in my career. Your code snippet helped open my mind even more on how an engine is built. Now if i can just find a few good books and time to set aside to begin practicing. Any suggestions on articles, forums or books?

Books on engine architecture, I'm not sure, the techniques I've learned have been from industry experience and dissecting about 1-2 game engines per year.

As far as books related to programming for AAA titles it really depends on what kind of field you're going to be in (e.g. A.I., physics, networking, user interface, audio, gameplay, graphics, database, etc.). Your best bet is to learn C++ really well, it is used by almost all AAA titles. I had a good C++ book in college but that was years ago so I forget the name of it, if I find the name of it I'll let you know.

As for forums, http://gamedev.net is by far the best, and for specific programming questions I recommend http://stackoverflow.com, or for game programming specifically there is http://gamedev.stackexchange.com/
Foldatron
(17 items)
 
Mat
(10 items)
 
Work iMac
(9 items)
 
CPUMotherboardGraphicsGraphics
i7 950 EVGA x58 3-way SLI EVGA GTX 660ti GTX 275 
RAMHard DriveHard DriveHard Drive
3x2GB Corsair Dominator DDR3-1600 80GB Intel X25-M SSD 2TB WD Black 150GB WD Raptor 
Hard DriveOSMonitorKeyboard
2x 150GB WD V-raptor in RAID0 Win7 Home 64-bit OEM 55" LED 120hz 1080p Vizio MS Natural Ergonomic Keyboard 4000 
PowerCase
750W PC P&C Silencer CoolerMaster 690 
CPUGraphicsRAMHard Drive
Intel Core i5 2500S AMD 6770M 8GB (2x4GB) at 1333Mhz 1TB, 7200 rpm 
Optical DriveOSMonitorKeyboard
LG 8X Dual-Layer "SuperDrive" OS X Lion 27" iMac screen Mac wireless keyboard 
Mouse
Mac wireless mouse 
CPUGraphicsRAMHard Drive
i7-2600K AMD 6970M 1GB 16GB PC3-10600 DDR3 1TB 7200rpm 
Hard DriveOptical DriveOSMonitor
256GB SSD 8x DL "SuperDrive" OS X 10.7 Lion 27" 2560x1440 iMac display 
Monitor
27" Apple thunderbolt display 
  hide details  
Reply
Foldatron
(17 items)
 
Mat
(10 items)
 
Work iMac
(9 items)
 
CPUMotherboardGraphicsGraphics
i7 950 EVGA x58 3-way SLI EVGA GTX 660ti GTX 275 
RAMHard DriveHard DriveHard Drive
3x2GB Corsair Dominator DDR3-1600 80GB Intel X25-M SSD 2TB WD Black 150GB WD Raptor 
Hard DriveOSMonitorKeyboard
2x 150GB WD V-raptor in RAID0 Win7 Home 64-bit OEM 55" LED 120hz 1080p Vizio MS Natural Ergonomic Keyboard 4000 
PowerCase
750W PC P&C Silencer CoolerMaster 690 
CPUGraphicsRAMHard Drive
Intel Core i5 2500S AMD 6770M 8GB (2x4GB) at 1333Mhz 1TB, 7200 rpm 
Optical DriveOSMonitorKeyboard
LG 8X Dual-Layer "SuperDrive" OS X Lion 27" iMac screen Mac wireless keyboard 
Mouse
Mac wireless mouse 
CPUGraphicsRAMHard Drive
i7-2600K AMD 6970M 1GB 16GB PC3-10600 DDR3 1TB 7200rpm 
Hard DriveOptical DriveOSMonitor
256GB SSD 8x DL "SuperDrive" OS X 10.7 Lion 27" 2560x1440 iMac display 
Monitor
27" Apple thunderbolt display 
  hide details  
Reply
post #5 of 7
I realise that I'm resurrecting a thread that is a few months old here, but this is some of the best written C++ code that I've ever come across. Bravo!
I've been researching component-based game engines for a while now, but this example is crystal clear.

I do have a few questions though; Should your - for example - render mesh component hold its own transformation values, duplicating the transform in the parent object class? As far as I'm aware, the observer pattern does not allow dependants to directly "talk back" to the subject (in this case, a component cannot send a message directly to it's parent Object). While it would be fairly straightforward to allow components a pointer to their parent object, also allowing access to the messaging system, this could lead to an infinite loop if a component were to make a call on the parent which then made a call to that component, which then called the parent again, etc. Is there a solution to this scenario, or is it just something that the programmer should take care to avoid?

In a similar vein, should components be able to communicate with other components (in the same object)? For example, a physics component may want to inspect a separate bounding box component, or detect weather or not the object even has a bounding box component.

Thanks again for some great code!
post #6 of 7
DAVco,

In my game engine, each mesh object has its own stored transformation values, which are set on render of the object. This means, on each frame where the object is rendered, the world transform matrix is set to the way the mesh needs it and then the mesh is rendered.
Each of the objects created is passed a pointer to the parent engine, and so has access to the other objects stored inside the parent engine. The parent engine has access to all objects. It may be able to have an infinite loop, but I haven't come across one yet and so I don't consider it that important of a problem. I just have to be careful.
My physics engine contains its own set of objects, which use the values of the objects in the main engine. It's like a mirrored version of the main engine, but only for physics. (See crummy paint diagram below)
35cpzid.png
Another way of thinking about it is to split each section (AI, Physics, Rendering, etc) into its own miniature engine, and have one larger engine on top managing all the smaller engine components.
    
CPUMotherboardGraphicsGraphics
FX-8350 Asus Crosshair V MSI GTX460 Hawk 1gb MSI GTX460 Hawk 1gb 
RAMHard DriveHard DriveHard Drive
Kingston HyperX 8gb (2x4gb) Crucial M4 64GB Samsung F3 1TB Western Digital 320GB 
CoolingOSMonitorMonitor
Custom WC Windows 7 Ultimate X64 Dell E2311H Dell E2311H 
MonitorKeyboardPowerCase
LH 23EN43 Ducky Year of the Dragon 2012 SilverStone Strider 1000W-P Corsair 800D 
Audio
Asus Xonar Essence STX 
  hide details  
Reply
    
CPUMotherboardGraphicsGraphics
FX-8350 Asus Crosshair V MSI GTX460 Hawk 1gb MSI GTX460 Hawk 1gb 
RAMHard DriveHard DriveHard Drive
Kingston HyperX 8gb (2x4gb) Crucial M4 64GB Samsung F3 1TB Western Digital 320GB 
CoolingOSMonitorMonitor
Custom WC Windows 7 Ultimate X64 Dell E2311H Dell E2311H 
MonitorKeyboardPowerCase
LH 23EN43 Ducky Year of the Dragon 2012 SilverStone Strider 1000W-P Corsair 800D 
Audio
Asus Xonar Essence STX 
  hide details  
Reply
post #7 of 7
Thanks FiX, that's the sort of architecture I'm aiming for - with each "subsystem" as compartmentalized as possible. In fact, one option I was looking at was an "outboard" component system (sometimes called "pure aggregation") that does away with the "Object/Entity" component container alltogether. In such a system, each subsystem would have a list of only the type of component in which it is interested, and each component would have some sort of identifier which ties it together with other components. This isn't something that I can easily implement right now though, as I have to interface with an AI system which uses a more "traditional" approach of calling functions on game-objects.

I'm thinking the best way around my problem is to just simple store a pointer to the "dispatcher" in each object, like you say, and attempt to avoid any infinite loops.

Just to clarify - Your Object/Entity class has an instance of a Mesh Object and both the base Object and Mesh Object store their own transformations?
New Posts  All Forums:Forum Nav:
  Return Home
  Back to Forum: Coding and Programming
Overclock.net › Forums › Software, Programming and Coding › Coding and Programming › Game Engine Architecture, Part 1: Creating an engine using an object<->component hierarchy