Моя улюблена структура ігрового двигуна - це інтерфейс та об'єктна <-> компонентна модель, що використовує обмін повідомленнями для зв'язку між усіма частинами.
У вас є кілька інтерфейсів для основних частин двигуна, таких як ваш менеджер сцени, завантажувач ресурсів, аудіо, рендерінг, фізика тощо.
У мене є керівник сцени, який відповідає за всі об'єкти в 3D-сцені / світі.
Об'єкт - це дуже атомний клас, що містить лише декілька речей, які є загальними для майже всього у вашій сцені, в моєму двигуні об'єктний клас містить лише положення, обертання, список компонентів та унікальний ідентифікатор. Ідентифікатор кожного об'єкта генерується статичним int, так що жоден два об'єкти не матимуть однакового ідентифікатора, це дозволяє вам надсилати повідомлення об'єкту за його ідентифікатором, а не мати покажчик на об'єкт.
Список компонентів на об'єкті - це те, що дає цим об'єктам основні властивості. Наприклад, для чогось, що ви можете бачити в 3D-світі, ви надасте вашому об'єкту компонент візуалізації, який містить інформацію про сітку візуалізації. Якщо ви хочете, щоб об'єкт мав фізику, ви б дали йому фізичну складову. Якщо ви хочете, щоб щось виконувало роль камери, надайте їй компонент камери. Список компонентів можна продовжувати і продовжувати.
Зв'язок між інтерфейсами, об'єктами та компонентами є ключовим. У своєму двигуні у мене є загальний клас повідомлень, який містить лише унікальний ідентифікатор та ідентифікатор типу повідомлення. Унікальний ідентифікатор - це ідентифікатор об’єкта, на який ви хочете, щоб повідомлення перейшло, а ідентифікатор типу повідомлення використовується об'єктом, який отримує повідомлення, щоб він знав, який тип повідомлення це.
Об'єкти можуть обробляти повідомлення, якщо вони потребують, і вони можуть передавати повідомлення кожному своєму компоненту, а компоненти часто роблять важливі справи з повідомленням. Наприклад, якщо ви хочете змінити і позицію об'єкта, ви надсилаєте об'єкту повідомлення SetPosition, об'єкт може оновити змінну його позиції, коли отримує повідомлення, але компонент візуалізації може знадобитися повідомленням, щоб оновити положення мережі візуалізації, і фізичному компоненту може знадобитися повідомлення для оновлення положення фізичного тіла.
Ось дуже простий макет диспетчера сцен, об'єктів, компонентів і потоку повідомлень, який я прошив приблизно за годину, написаний на C ++. Під час запуску він встановлює позицію на об'єкті, і повідомлення проходить через компонент візуалізації, після чого отримує позицію від об'єкта. Насолоджуйтесь!
Крім того, я написав версію C # і версію Scala з наведеного нижче коду для тих, хто може вільно володіти тими, а не 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';
}