Форум программистов, компьютерный форум, киберфорум
C++
Войти
Регистрация
Восстановить пароль
Карта форума Темы раздела Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.73/15: Рейтинг темы: голосов - 15, средняя оценка - 4.73
9 / 9 / 8
Регистрация: 08.11.2014
Сообщений: 215
Записей в блоге: 1
1

Реализация паттерна Entity-Component-System

09.10.2018, 18:00. Показов 2903. Ответов 5
Метки нет (Все метки)

Author24 — интернет-сервис помощи студентам
Возникло желание попробовать реализовать паттерн Entity-Component-System. Изрядно начитавшись статей я взялся за дело.
Сначала я реализовал такую архитектуру:
Абстрактный класс IEntity хранит в себе массив указателей абстрактного класса IComponent, который в свою очередь хранит указатель на сущность, которой принадлежит. Это создало перекрёстное наследование (вроде так называется), но об этом позже.
Есть класс Level, от которого наследуются уровни. Сам класс хранит в себе массив указателей IEntity (ObjectList) и массив указателей IComponent (DrawableList).
Сама сущность создаётся следующим образом:
C++
1
2
3
4
auto eobj = std::make_shared<EObject>(); 
auto body = std::make_shared<CTile>(IDGenerator::getNextId(), «body», CurrentPathFile);
body->Attach(eobj);
addObject(eobj);
Итак, по порядку:
  • EОbject — производный класс от IEntity.
  • CTile — дальний потомок IComponent.
  • IDGenerator — класс который генерирует id для компонентов, «body» - имя компонента, CurrentPathFile — путь к текстурке
  • body->Attach(eobj) — привязывает компонент к сущности
  • addObject(eobj) — добавляет сущность в массив ObjectList, который принадлежит уровню (класс Level). Так же в этом методе собираются все графические компоненты указанной сущности и помещаются в массив DrawableList.
Эта схема работала, но в ней есть несколько минусов, из-за которых я решил переделать архитектуру. Минусы:
  • IDGenerator создавал айдишники только для компонентов. У сущностей тоже есть id, но он генерируется внутри конструктора. Зачем я сделал такое разделение? ¯\_(ツ)_/¯
  • body->Attach(eobj) вызывается непосредственно уже после создания компонента. То есть до вызова этого метода, где-то в воздухе висит компонент, который ни к чему не привязан. К такому решению я пришёл поскольку использовал умные указатели. IComponent у меня наследуется от std::enable_shared_from_this<IComponent>. Я хотел добавлять компонент к сущности во время создания компонента, а-ля так auto body = std::make_shared<CTile>(eobj, IDGenerator::getNextId(), «body», CurrentPathFile), но к сожалению от этого пришлось отказаться, так как нельзя вызывать shared_from_this() в конструкторе
Были ещё несколько причин, но на них я не буду заострять внимание.
Спустя время обдумывания я пришёл к немного изменённой архитектуре (Диаграмма классов прикреплена ниже)

Я решил сделать акцент на том, что у сущностей и компонентов всё таки есть айдишники, которые я до этого почти не использовал. Я решил что массив сущностей будет храниться только в одном месте — в EntityManager, а в остальных местах только айдишники.
Как это должно работать:
Есть статический класс EntityManager, который может создавать и удалять сущности. Делать это может только он (конструктор и деструктор IEntity стали приватными, а EntityManager стал дружественным). Имеется метод Create, который возвращает объект *IEntity и добавляет его в массив
C++
1
2
3
4
5
6
IEntity *EntityManager::Create(const std::string &name)
{
    IEntity *ie = new IEntity(getNextId(), name);
    EntityManager::Entities.push_back(ie);
    return ie;
}
Теперь айдишники генерируются внутри EntityManager. ComponentManager работает по такому же принципу что и EntityManager
Сущности же в свою очередь теперь имеют массив айдишников компонентов. Например, чтобы удалить компонент с именем Sprite я делаю так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
auto player = dynamic_cast<IEntity *>(EntityManager::Create("Hero"));
if (!player)
{
    std::cout << "Can\'t create object" << std::endl;
    return 1;
}
 
if (!ComponentManager::Create(player->getId(), "Position"))
{
    EntityManager::Destroy(player->getId());
    return 1;
}
if (!ComponentManager::Create(player->getId(), "Sprite"))
{
    EntityManager::Destroy(player->getId());
    return 1;
}
if (!ComponentManager::Create(player->getId(), "Health"))
{
    EntityManager::Destroy(player->getId());
    return 1;
}
 
for (auto c : player->ComponentsId)
{
    if (ComponentManager::getComponent(c)->getName() == "Sprite")
    {
        ComponentManager::Destroy(c);
    }
}
У такого подхода так же есть свои минусы:
  • Мне не нравится такое обилие проверок (не критично)
  • Создать объект производный от IEntity мне так и не удалось. При dynamic_cast код не проходит первую же проверку if (!player).
C++
1
2
3
4
5
6
auto player = dynamic_cast<EObject *>(EntityManager::Create("Hero"));
if (!player)
{
    std::cout << "Can\'t create object" << std::endl;
    return 1;
}
Можно заменить dynamic_cast на static_cast, тогда проверка пройдёт, IDE будет считать что у меня объект EObject, но на деле это тот же IEntity

Дабы решить эту проблему был соблазн обмазаться шаблонами, но мне хотелось бы обойтись без них (ну просто не хочу и всё). Но даже с ними у меня не получилось (хотя в другом проекте похожая реализация у меня работала, но я не смог её найти).
C++
1
2
3
4
5
6
7
8
9
10
11
12
template<class T>
static T *Create(const std::string &name)
{
    T *t = dynamic_cast<T*>(new IEntity(getNextId(), name));
    if (!t)
    {
        std::cout << "Can\'t create object" << std::endl;
        return nullptr;
    }
    EntityManager::Entities.push_back(t);
    return t;
}
В интернетах я узнал что эта моя реализация очень схожа с реализацией фабричного метода. Отличие было разве что в том, что фабричный метод подразумевает написание класса для создания каждого класса, который будет наследоваться от IEntity, а мне бы очень хотелось этого избежать, дабы сделать систему менее громоздкой и более расширяемой.

Я не исключаю того, что могу не знать многих вещей, или попросту того что я что-то забыл.
Хотелось бы услышать критику и советы по поводу реализации
Миниатюры
Реализация паттерна Entity-Component-System  
0
Лучшие ответы (1)
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
09.10.2018, 18:00
Ответы с готовыми решениями:

Собственная реализация паттерна "Слушатель" - нужна конструктивная критика
Добрый день, наворотил код по работе с паттерном слушатель - есть класс контейнер данных и при...

System.UnauthorizedAccessException: Creating an instance of the COM component with CLSID {...} from the IClassFactory failed due to the following erro
COM-клиент пытается удаленно (по лок. сети) запустить COM-сервер: запуск выполняется, но сразу же...

Entity Component System, можно ли доработать класс Entity
Здравствуйте, сделал свою реализацию Entity Component System, но хотелось бы узнать ваше мнение по...

Реализация паттерна состояние
Парни, кто шарит, помогите реализовать паттерн. &quot;Игра перемещение по лабиринту&quot;. Как можно проще.

5
18842 / 9841 / 2408
Регистрация: 30.01.2014
Сообщений: 17,284
09.10.2018, 22:50 2
Цитата Сообщение от avraal Посмотреть сообщение
Мне не нравится такое обилие проверок
Исключения спасут отца русской демократии.

Цитата Сообщение от avraal Посмотреть сообщение
C++
1
IEntity *ie = new IEntity(getNextId(), name);
Так делать вы не должны, т.к. IEntity - это интерфейс (буковка I - намекает об этом). Создаваться должен именно наследник.
Придется запрограммировать способ, с помощью которого EntityManager, а по сути - фабрика, сможет научиться создавать объекты наследников IEntity. Например, можно завести метод void EntityManager::Register(const std::string &name, FactoryMethod createMethod) в котором выполнять регистрацию методов создания конкретных наследников.

dynamic_cast у вас здесь категорически не нужен.

Добавлено через 2 часа 51 минуту
Цитата Сообщение от avraal Посмотреть сообщение
фабричный метод подразумевает написание класса для создания каждого класса,
Ничего подобного он не подразумевает. Определить фабричный метод вы можете совершенно любым способом, не обязательно в виде иерархии классов-создателей.
Если вы сделали подобный вывод глядя на реализацию, которую приводят обычно как иллюстрацию паттерна, то спешу вас уведомить, что данная реализация ни в коем случае не является эталоном. Тот или иной паттерн всегда применим к конкретной проблеме, которая всегда накладывает свои ограничения.
Идея, заложенная в паттерне, должна помочь выстроить архитектуру, а не навязывать конкретную реализацию как шаблон.
Т.е., проще говоря, паттерном является именно идея, а никак не реализация. Реализацию определяет программист исходя из своей конкретной задачи.
0
9 / 9 / 8
Регистрация: 08.11.2014
Сообщений: 215
Записей в блоге: 1
10.10.2018, 17:32  [ТС] 3
Цитата Сообщение от DrOffset Посмотреть сообщение
IEntity - это интерфейс
Да, задумка такая, но до сих пор этот класс не был абстрактным лишь для тестирования

Цитата Сообщение от DrOffset Посмотреть сообщение
выполнять регистрацию методов создания конкретных наследников
Я попробовал реализовать это, получилось следующее:
В EntityManager я добавил коллекцию для зарегистрированных методов, сам метод регистрации, и слегка изменил метод Create
C++
1
2
3
4
5
6
7
8
9
10
11
static std::map<std::string, IEntity *(*)(int, const std::string&)> RegisteredMethods;
static void Register(const std::string &name, IEntity *(*creator)(int, const std::string&))
{
    RegisteredMethods.insert(std::pair<std::string, IEntity *(*)(int, const std::string&)>(name, creator));
}
static IEntity *Create(const std::string &TypeName, const std::string &objName)
{
    auto ie = RegisteredMethods[TypeName](getNextId(), objName);
    EntityManager::Entities.push_back(ie);
    return ie;
}
Теперь для того, чтобы создать объект класса, класс должен иметь метод create
C++
1
2
3
4
5
static EObject *create(int id, const std::string &name)
{
    EObject *eo = new EObject(id, name);
    return eo;
}
И работает это теперь так:
C++
1
2
3
4
//Регистрация метода
EntityManager::Register("EObject", reinterpret_cast<IEntity *(*)(int, const std::string &)>(&EObject::create));
//Создание объекта
EObject *player = static_cast<EObject *>(EntityManager::Create("EObject", "Hero"));
Это работает, и если вызывать методы, которые переопределены в EObject, то вызываются именно они, а не методы родителя
Правда мне не очень нравится держать метод EObject::create в public (пока не придумал куда можно впихнуть регистрацию метода так, чтобы он оставался приватным)

Я правильно понял вашу идею?
0
18842 / 9841 / 2408
Регистрация: 30.01.2014
Сообщений: 17,284
10.10.2018, 18:15 4
Лучший ответ Сообщение было отмечено avraal как решение

Решение

Цитата Сообщение от avraal Посмотреть сообщение
Правда мне не очень нравится держать метод EObject::create в public
Можно разрулить с помощью шаблона-регистратора. ((иллюстрацию см. в примере ниже)

Цитата Сообщение от avraal Посмотреть сообщение
C++
1
reinterpret_cast<IEntity *(*)(int, const std::string &)>
Зачем тут reinterpret_cast?

Вообще очень много красоты в эту систему можно добавить, если сделать кое-что через шаблоны. Например метод Register сделать шаблонным, и наделить его возможностью самостоятельно искать "создателя" у переданного типа.
Пример:
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <string>
#include <map>
#include <iostream>
#include <list>
#include <cstdint>
 
class EntityManager;
 
class IEntity
{
    friend class EntityManager;
public:
    virtual void foo() = 0; // public interface
    
    uint64_t id() const
    {
        return m_id;
    }
    std::string name() const
    {
        return m_name;
    }
    
protected:
    explicit IEntity(uint64_t id, std::string const & name)
        : m_id(id), m_name(name)
    { }
    
    virtual ~IEntity() = 0;
    
private:
    uint64_t m_id;
    std::string m_name;
};
 
IEntity::~IEntity() // in cpp
{ }
 
//-----------------------------------------------------------------
 
class EntityManager
{
public:
    ~EntityManager();
    
    template <typename Registerable>
    void Register(const std::string &name);
    
    IEntity * Create(const std::string &type, const std::string &name);
    
private:
    uint64_t getNextId()
    {
        return m_nextId++;
    }
    
private:
    std::map<std::string, IEntity * (*)(uint64_t, std::string const &)> m_prototypes;
    std::list<IEntity *> m_entities;
    
    uint64_t m_nextId = 0;
};
 
IEntity * EntityManager::Create(const std::string &type, const std::string &name)
{
    auto fit = m_prototypes.find(type);
    if(fit != m_prototypes.end())
    {
        auto ient = fit->second(getNextId(), name);
        m_entities.push_back(ient);
        return ient;
    }
    return nullptr;
}
 
template <typename Registerable>
void EntityManager::Register(std::string const & name)
{
    m_prototypes.insert({ name, &Registerable::Create });
    // additional checks
}
 
EntityManager::~EntityManager()
{
    for(auto & entity : m_entities)
    {
        delete entity;
    }
}
//-------------------------------------------------------------------------
 
// Mix-in Registerable allows to register types to Сreate
template <typename D>
class Registerable
{
    friend class EntityManager;
private:
    static IEntity * Create(uint64_t id, std::string const & name)
    {
        return new D(id, name);
    }
};
 
class Hero 
    : public IEntity
    , public Registerable<Hero> // Hero is Registerable type
{
    friend class Registerable<Hero>; // Ability to call private constructors from Registerable 
public:
    void foo() override
    {
        std::cout << "This is " << name() << std::endl;
    }
    
private:
    Hero(uint64_t id, std::string const & name)
        : IEntity(id, name)
    { }
 
    ~Hero() = default;
};
 
int main()
{
    EntityManager enManager;
 
    enManager.Register<Hero>("Hero");
 
    IEntity * ihero = enManager.Create("Hero", "Vasya"); // Creating Hero named Vasya
    if(ihero)
    {
        ihero->foo();
        
        Hero * hero = static_cast<Hero *>(ihero); 
        
        // do something with hero
        
        (void)hero;
    }
}
Онлайн демка: https://wandbox.org/permlink/ypSUSttFQuIRJFMk


Цитата Сообщение от avraal Посмотреть сообщение
C++
1
auto ie = RegisteredMethods[TypeName](getNextId(), objName);
Лучше использовать find - и проверку на end(), т.к. при неправильном имени TypeName бедует создаваться элемент map по умолчанию - нулевой указатель на функцию.

Цитата Сообщение от avraal Посмотреть сообщение
C++
1
RegisteredMethods.insert(std::pair<std::string, IEntity *(*)(int, const std::string&)>(name, creator));
Для этого есть std::make_pair. А в последних версия языка вообще можно написать RegisteredMethods.insert({ name, creator });.

В целом - да, идею верно поняли.
2
Mental handicap
1246 / 624 / 171
Регистрация: 24.11.2015
Сообщений: 2,429
06.11.2018, 15:39 5

Не по теме:

DrOffset, эхх.. вот бы еще от вас увидеть реализацию Сomponent и как они взаемодействуют с Entity на этом простом примере, ато в интеренете сразу дают готовую релизацию с миллионом еще много чего в добавок и где даже не видно как оно должно работать.. Заинтересовал данный паттерн и хотел бы коечто переписать используя его, но сразу понять его как-то сложно, в интеренете предоставляют довольно сложную реализацию сходу, а мне хотелось бы переписать уже готовый код под этот паттерн и я думаю не все что там пишут мне нужно, типо аллокаторов, интересно можно ли ее упростить? Еще что сбивает - у каждого своя реализация.. Был бы рад вашему ответу или лучше создать отдельную тему?



зы - за ап темы извиняюсь, но уж очень интересны некоторые моменты.
0
18842 / 9841 / 2408
Регистрация: 30.01.2014
Сообщений: 17,284
07.11.2018, 20:31 6
Azazel-San, Да, конечно, чуть попозже смогу показать.
1
07.11.2018, 20:31
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
07.11.2018, 20:31
Помогаю со студенческими работами здесь

Реализация паттерна MVC
Доброго времени суток. Допустим у меня есть класс Database в котором 2 метода: class Database...

Реализация паттерна Singleton
Добрый день. Необходимо реализовать класс Storage, объект которого будет единственным в программе....

Реализация паттерна MVVM
Добрый день, форумчане. Ни разу не пользовался данным паттерном программирования, прочитал уже кучу...

Реализация паттерна MVP в Windows Forms
Здравствуйте! В приложении пробую реализовать MVP. Есть несколько форм, класс Presenter и класс...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
6
Ответ Создать тему
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru