Компонентная система игровых сущностей.
Автор: 3eR0.1ive
Не раз поднималась тема об иерархии игровых объектов, их взаимодействии и управления ими. Я пробовал всякие варианты построения игрового объекта, и в итоге остановился на компонентной системе. Было это давненько, так что система успела себя оправдать по удобству и масштабируемости. О получившейся схеме и пойдет речь. Но для начала рассмотрим возможные варианты.
Разновидности иерархий.
1. Blob game object (God game object)
2. Compile time component object
3. Dynamic component object (Aspect oriented object)
1. Blob game object (God game object)
Blob game object (God game object) – система “все в одном”, то есть некий класс объекта, который содержит в себе все возможные данные: физику, графику, звук, etc. Это самый простой и распространенный вариант написания игрового объекта. В этой же кажущейся простоте и кроется главный недостаток – полное отсутствие гибкости. Выглядеть класс такого объекта, к примеру, может так:
class Object { …... private: uint32 mFlagUseComponent; NodeGraphics* mGraphicsNode; PhysicsNode* mPhysicsNode; Sound* mSound; ….. }
Такой вариант построения может сгодиться в казуальных играх, ну или в мелких игрушках, сделанных на коленке. Для более или менее серьезного проекта вариант в большей степени не приемлем.
2. Compile time component object
Compile time component object – объект, строящийся из отдельных компонент (графической, физической, etc) на этапе компиляции. Выглядеть может, к примеру так:
class Component { public: virtual void setPosition(const vec3&); virtual void setOrientation( const vec4&); virtual const vec3& getPosition( ) const; virtual const vec4& getOrientation( ) const; }; class Graphics : public Component { //implementation ... }; class Physics: public Component { //implementation ... }; template <typename TTypeList> class Object { template <typename T> T* getComponent( ) { return TypeList::At<TTypeList, T>::result; } protected: TTypeList mComponents; }; typedef Object< MakeTypelist( Graphics, Physics) > Object1; typedef Object< MakeTypelist( Graphics, Sound) > Object2;
*Здесь под TTypeList понимается некая смесь из списка типов и обычного динамического списка. Расписывать не буду, так как это не основная тема, о которой идет речь.
Эта схема уже пригодна для использования, и есть реальные игры, где она применяется и довольно успешно. Немного поясню, здесь мы на этапе компиляции собираем объект только из тех компонентов, которые реально могут пригодиться. И если мы попытаемся использовать что-либо, чего нет в объекте, то попросту получим ошибку компиляции. Видно, что в отличие от предыдущей схемы, нет ничего лишнего, это конечно плюс, минусом такого подхода является взаимодействие компонентов, но, впрочем, обычно все взаимодействие заключается в передаче трансформаций. При таком подходе мы получили бОльшую гибкость, но все же недостаточную для удобного управления игровым объектом. И таким образом, мы плавно переходим к конечному варианту:
3. Dynamic component object (Aspect oriented object)
Dynamic component object (Aspect oriented object) — схема, основанная на динамически связанных компонентах (здесь и далее я буду называть их не компонентами, а аспектами), то есть вся связанность может быть создана и разрушена в риалтайме. Еще эта идея напоминает Аспектно Ориентированное построение систем. Основная идея заключается в том, что такое понятие как “игровой объект” вообще атавизм. Ненужная вещь. Лишняя связанность, которая только мешает. По началу, это трудновато укладывается в голове, но на практике, это оказывается очень удобным.
Теперь, когда мы избавились от понятия “объект”, нужно подумать, как обеспечить связанность аспектов, для решения этой задачи можно применить обыкновенную подписку (subscribe, link). В нашем случае подписанный аспект просто повторяет трансформации того, на кого он подписан (конечно, с учетом смещения). Давайте предположим, как это может выглядеть:
class Aspect { public: virtual void setPosition(const vec3&); virtual void setOrientation( const vec4&); virtual const vec3& getPosition( ) const; virtual const vec4& getOrientation( ) const; void subscribe( const Aspect*); void unsubscribe( ); const char* getName( ) const; const char* getFullName( ) const; // mName + mPantonimic protected: Aspect* mParent; const char mName[MAX_NAME]; const char mPantonimic[MAX_NAME]; }; class Graphics : public Aspect { //implementation ... }; class Physics: public Aspect { //implementation ... };
Чтобы пояснить все происходящее, давайте представим, как может выглядеть простейший “объект”, при таком подходе: