ПрограммированиеСтатьиОбщее

Компонентная система игровых сущностей.

Автор:

Не раз поднималась тема об иерархии игровых объектов, их взаимодействии и управления ими. Я пробовал всякие варианты построения игрового объекта, и в итоге остановился на компонентной системе. Было это давненько, так что система успела себя оправдать по удобству и масштабируемости. О получившейся схеме и пойдет речь. Но для начала рассмотрим возможные варианты.

Разновидности иерархий.

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
    ...
  };

Чтобы пояснить все происходящее, давайте представим, как может выглядеть простейший “объект”, при таком подходе:

component_system_object | Компонентная система игровых сущностей.

Такого рода “объекты” удобно создавать с помощью какой-нибудь функции, принимающей на вход параметры, и создающей систему связанных аспектов:

void World::createObject(const ObjectParams& params)
{
  uint32 i = params.getAspectCount();
  while(i--)
  {
    Aspect* aspect = World::createAspect( params.getAspectParams(i) );
    const char* parent_name = params.getAspectParent(i);
    aspect->subscribe( World::getAspect(parent_name) );
  }
}

Таким образом, мы можем хранить понятие “объект” где-то в файлике, скрипте, etc.  А для самого движка будут существовать только связанные между собой аспекты и ничего больше!

Наверное, возник вопрос, зачем вся эта хероверть с двойным именем в аспекте?

Это вариант решения именования “объектов”, созданных из одних и тех же параметров. Предположим мы хотим создать 2 фонарика: “light1” и “light2”.

Если бы у нас была возможность работать только с одним именем аспекта, то встал-бы вопрос: А какой аспект, к какому “объекту” принадлежит? То есть в мире-бы существовало 2 графических и 2 световых аспекта с одинаковыми именами. Фейл.

При добавлении “отчества”(имени абстрактного объекта) к аспекту, мы получаем возможность иметь в мире следующий набор компонент:
“light1.graphic”
“light2.graphic”
“light1.light”
“light2.light”

Теперь мы имеем гибкую систему сборки игровых объектов в рантайме, с вполне адекватной системой доступа к ее компонентам. Ну, и в конце, приведу пример реального файла параметров, из которого грузится и создается “объект”:

cube.object

essences : 
{
  {
    type : PhysicsEssence,
    name : PhysicsCube , 
    mass : 4, 
    shapes : 
      {
        {
          shape_type : ST_BOX, 
          dim : 1 1 1
        }
      },
  },
  {
    type : GraphicsEssence,
    mesh_name : f_box.mesh , 
    mesh_scale : 1 1 1 , 
    custom_material_name : 13D/Material/Metalbox, 
    name : GraphicsCube, 
    parent_name : PhysicsCube 
  }
}

Вот, пожалуй, и все, что я хотел вам рассказать. Enjoy!

#архитектура, #игровые объекты

3 апреля 2011 (Обновление: 3 июня 2013)

Комментарии [431]