Mi estructura de motor de juego favorita es la interfaz y el modelo de componente de objeto <-> que usa mensajes para la comunicación entre casi todas las partes.
Tiene múltiples interfaces para las partes principales del motor, como su administrador de escena, cargador de recursos, audio, renderizador, física, etc.
Tengo al administrador de escena a cargo de todos los objetos en la escena / mundo 3D.
Object es una clase muy atómica, que contiene solo algunas cosas que son comunes a casi todo en su escena, en mi motor, la clase de objeto tiene solo posición, rotación, una lista de componentes y una ID única. La ID de cada objeto es generada por un int estático, de modo que no habrá dos objetos que tengan la misma ID, esto le permite enviar mensajes a un objeto por su ID, en lugar de tener que tener un puntero al objeto.
La lista de componentes en el objeto es lo que le da a los objetos sus propiedades principales. Por ejemplo, para algo que puede ver en el mundo 3D, le daría a su objeto un componente de representación que contiene la información sobre la malla de representación. Si desea que un objeto tenga física, le daría un componente de física. Si desea que algo actúe como una cámara, dele un componente de cámara. La lista de componentes puede seguir y seguir.
La comunicación entre interfaces, objetos y componentes es clave. En mi motor, tengo una clase de mensaje genérico que contiene solo una ID única y una ID de tipo de mensaje. La identificación única es la identificación del objeto al que desea que vaya el mensaje, y la identificación del tipo de mensaje es utilizada por el objeto que recibe el mensaje para que sepa qué tipo de mensaje es.
Los objetos pueden manejar el mensaje si lo necesitan, y pueden pasar el mensaje a cada uno de sus componentes, y los componentes a menudo harán cosas importantes con el mensaje. Por ejemplo, si desea cambiar la posición del objeto y le envía un mensaje SetPosition, el objeto puede actualizar su variable de posición cuando recibe el mensaje, pero el componente de representación puede necesitar un mensaje para actualizar la posición de la malla de representación, y el componente de física puede necesitar el mensaje para actualizar la posición del cuerpo de física.
Aquí hay un diseño muy simple de administrador de escena, objeto, componente y flujo de mensajes, que preparé en aproximadamente una hora, escrito en C ++. Cuando se ejecuta, establece la posición en un objeto, y el mensaje pasa a través del componente de representación, luego recupera la posición del objeto. ¡Disfrutar!
Además, he escrito una versión de C # y una versión de Scala del código a continuación para cualquier persona que pueda hablar con fluidez en lugar de C ++.
#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;
auto 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';
}