Ограничения:
1) не все, а лишь некоторые игровые объекты подлежат отрисовке и должны обладать методом draw();
2) не все, а лишь некоторые игровые объекты подлежат сериализации и должны обладать методами load()/save();
3) все объекты должны хранится в одной очереди, а то получится отдельная очередь для тех, кто апдейтится, отдельная — для тех кто рисуется, уродство одним словом.
Оказалось, это непросто
Полезнейшими функциями менеджера были бы "массовые действия" над всем контейнером objects_, типа serialize_all(), draw_all(). Но ведь в objects_ хранятся указатели на game_object, а, значит. вызов любой функции окромя update() невозможен.
Сразу отбросим очевидные ереси типа сохранения в объекте "кода типа".
Но остальные решения, которые я придумал, кажутся мне не менее корявыми.
1. dynamic_cast Лепим производные от game_object классы, обладающие необходимым интерфейсом, типа
На этапе выполнения проверяем возможность вызова нужной функции
// manager.hppclass manager {
public:
/* ... */void save_all() {
for (objects_queue::iterator it = objects_.begin(); it != objects_.end(); ++it) {
if (dynamic_cast<serializable*>(*it)) (*it)->save();
}
}/* ... */
};
2. Передача менеджерам сообщений объектам Менеджер посылает объекту (неизвестного ему динамического типа) сообщение (к примеру, элемент енума);
// manager.hppenum command { UPDATE, DRAW, SAVE, LOAD, };
class manager {
public:
/* ... */void send(command c) {
for (objects_queue::iterator it = objects_.begin(); it != objects_.end(); ++it) {
(*it)->execute(c);
}
}/* ... */
};
А уже объект определяет, в состоянии ли он выполнить запрос
// game_object.hppclass game_object {
/* ... */virtual void execute(command c) {
if (c == UPDATE) update();
}
/* ... */
};
// serializable.hppclass serializable : public game_object {
public:
virtual void save() = 0;
virtual void load() = 0;
virtual void execute(command c) {
game_object::execute(c);
if (c == SAVE) save();
else if (c == LOAD) load();
}
};[/b]
Вопросы:
1) Какие из упомянутых ограничений кажутся нелогичными?
2) Существует ли "красивый" (а еще лучше стандартный) способ решения этой проблемы?
3) Какое решение из предложенных предпочтительнее?
Приветствуются любые варианты.
Re: "Хитрый" проход по контейнеру полиморфных элементов.
AD>Может я не въехал в твою задачу, но почему-бы не добавть load()/save() в game_object.
AD>Те объекты, которые должны быть serializable, пускай их реализуют. Тоже самое касается и draw():
Боюсь, что это не решает самой проблемы, а именно: если завтра мне понадобится, чтобы менеджер умел выполнять над частью объектов еще пару операций (например, move(point), play_sound() etc.), снова придется лезть и переделывать game_object. Через несколько итераций мы получим "жирный" интерфейс: в классе game_object будет немалое количество методов-заглушек, созданных только для удовлетворения нужд какой-то ветви иерархии объектов .
Если исходник game_object.hpp не мой, это усугубит проблему.
(Согласен, что это не хуже того, что я нагородил )
Re: "Хитрый" проход по контейнеру полиморфных элементов.
Здравствуйте, Alxndr, Вы писали:
A>Добрый день, уважаемые эксперты. A>Решил написать сюда, а не в проектирование. Павел рассудит , правильно ли это было. Прошу прощения за длинный топик.
[skipped] A>Приветствуются любые варианты.
Вот это обсуждение
1. Использовать в менеджере несколько очередей, одна содержит интерфейсы serializabe зарегистрированных объектов, другая drawable тех же объектов. По очередям интерфейсы распихиваются при регистрации объекта, поддержка регистрируемым объектом определенного интерфейса выполняется также при регистрации средствами языка (в том числе, но не ограничиваясь dynamic_cast), запросом интерфейса через DOM (в случае, если DOM построен на COM, QueryInterface) или чрез GetCaps какого-нибудь интерфейса.
+ Наивысшая производительность каждой пакетной операции serialize_all, draw_all и пр. из-за того, что не надо не только выяснять поддерживает ли объект поведение или нет, но еще и в каждой конкретной операции будут принимать участие только соответствующие объекты.
+ Есть возможность вовсе избавиться от if'ов (как признака проблем oo-design'а). Так, при регистрации объекта, объект регистрирует себя не в одной очереди менеджера, а в нескольких в соответствии со своими возможностями. То есть register_drawable(this), register_serializabe(this) и т.п, вместо одного reg(this) наполненного ненавистными if'ами.
— Усложняются операции перемещения объектов в очередях (так как надо перемещать объект в каждой из очереди в которой он состоит), особенно если положение объекта отностительно других в нескольких очередях должно быть синхронизированно (напр, если A рисуется после B, то и расчитывать логику тоже например надо сначало у B, потом у A)
2. Хранить в очереди не интерфейсы, а дескриптор объектов:
Дескриптор создается, заполняется (выяснением какие интерфейсы объект поддерживает) и помещается в очередь при регистрации объекта. Далее при выполнении каждой пакетной операции, при итерировании достаточно проверить на неNULL'ность соответствующий член дескриптора и вызвать методы интерфейса.
+ При реализации "в лоб" производительность ниже, чем вариант 1, но если при изменении очереди строить соответствующие индексы, то производительность будет примерно равна варианту 1, учитывая штраф на создание индексов.
+ Также можно обойтись без if'ов. В данном случае, если obj_desc будет не статической структурой, внутренней для менеджера, а классом с операциями register_drawable(this), register_serializabe(this) и т.п. Тогда у менеджера будет операция:
public:
obj_desc* reg();
получив от которой obj_desc, регистрируемый объект сам вызовет соответствующие операции.
Re[3]: "Хитрый" проход по контейнеру полиморфных элементов.
Здравствуйте, Alxndr, Вы писали:
A>Боюсь, что это не решает самой проблемы, а именно: если завтра мне понадобится, чтобы менеджер умел выполнять над частью объектов еще пару операций (например, move(point), play_sound() etc.), снова придется лезть и переделывать game_object. Через несколько итераций мы получим "жирный" интерфейс: в классе game_object будет немалое количество методов-заглушек, созданных только для удовлетворения нужд какой-то ветви иерархии объектов . A>Если исходник game_object.hpp не мой, это усугубит проблему.
Вот твой вариант 1.b. Но реализованный более удобно:
class manager
{
...
public:
...
template <typename C> total_operation(void ( C::*method)() )
{
C *obj;
for (Iterator it = objects_.begin(); it != objects_.end(); it++)
{
obj = dynamic_cast<C*>(*it);
if (obj) (obj->*method)();
}
}
};
// где-то в коде:
// *** сериализация ***
manager_instance.total_operation<serializable_object>(&serializable_object::save);
// *** отрисовка ***
manager_instance.total_operation<drawable_object>(&serializable_object::draw);
У меня есть ещё несколько вариантов с шаблонами. Например, когда есть класс operation и его наследники, характеризующие выполняемые операции. В базовый класс игрового объекта в функцию make_operation передаётся нужная операция. Но этот вариант пока недоработанный (много спорных моментов).
1) описать интерфейсы сериализе, драва_бле
class draw_ble
{
parent* _ok;
public :
void draw(){ _ok->draw(); };
}.....
2) Принять что:
A>3) все объекты должны хранится в одной очереди,
+ отдельная очередь для тех _ИНТЕРФЕЙСОВ_, кто апдейтится, отдельная — для тех кто рисуется, ...
это называется гранями .
3) Объекты писать так:
class some
{
public:
operator draw_ble(){
return new draw_ble(*this);
}
};
4) A>На этапе _ВСТАВКИ_ проверяем возможность вызова нужной функции
serializa_ble* s = (serializa_ble*)(*it);
drawa_ble* d = (drawa_ble*)(*it);
if (s>) reg_serializa_ble(s);
...........
5) при выполнении "обобщенных" операций у тебя будет быстрее работать поскольку для "нарисовать все" будут перебдираться только те кто может рисоваться..
Веру-ю-у! В авиацию, в научную революци-ю-у, в механизацию сельского хозяйства, в космос и невесомость! Веру-ю-у! Ибо это объективно-о! (Шукшин)
Re[4]: "Хитрый" проход по контейнеру полиморфных элементов.
A>>3) все объекты должны хранится в одной очереди, dad>+ отдельная очередь для тех _ИНТЕРФЕЙСОВ_, кто апдейтится, отдельная — для тех кто рисуется, ...
В целом, очень похоже на версию Frostbitten'а.
dad>class some dad>{ dad>public: dad> operator draw_ble(){ dad> return new draw_ble(*this); dad> } dad>};
dad>4) A>>На этапе _ВСТАВКИ_ проверяем возможность вызова нужной функции
dad> serializa_ble* s = (serializa_ble*)(*it); dad> drawa_ble* d = (drawa_ble*)(*it); dad> if (s>) reg_serializa_ble(s); dad> ...........
Все же, мне кажется, что если пронаследовать some от draw_ble, то процесс регистрации в очереди, поддерживающей этот интерфейс, можно будет инкапсулировать в самом классе draw_ble.
Re[3]: "Хитрый" проход по контейнеру полиморфных элементов.
Подобные проблемы часто поддаются красивому решению при помощи паттерна Visitor.
1) надо сделать классы-наследники от game_object — draw_game_object, serialize_game_object и т.п. с соответствующими методами draw(), save()/load() и т.п. Менеджер должен об конкретных наследниках знать.
2) game_object должен обладать следующим интерфейсом:
4) функцию accept() каждый наследник реализует как
...
mgr->process(this);
...
Передавая таким образом this как указатель на конкретного наследника
5) а в функциях process(...) менеджер может творить всё что угодно, ориентируясь на известные ему интерфейсы наследников game_object'a
Достоинства метода:
— никаких кастов, всё строится на таблице виртуальных функций
— легко расширяемо
— нет условных конструкций
— саму конструкцию process/accept можно вынести и спрятать очень далеко
Re[4]: "Хитрый" проход по контейнеру полиморфных элементов.
Соответственно можешь делать любые проходы менеджером, вызывая функцию accept(this) у объектов. Конкретные действия менеджера в разных ситуациях можно задасть, используя стейт-машину или банально установкой флажков.
Re[3]: "Хитрый" проход по контейнеру полиморфных элементов.
A>>>3) все объекты должны хранится в одной очереди, dad>>+ отдельная очередь для тех _ИНТЕРФЕЙСОВ_, кто апдейтится, отдельная — для тех кто рисуется, ...
A>В целом, очень похоже на версию Frostbitten'а.
А других нормальных вариантов и не будет — отдельные очереди для разных интерфейсов. Просто та или иная реализация — приведение типов + наследование, приведение типов + грани, запрос интерфейса и т.д. и т.п. сути это не меняет..
ЗЫ — сорри за опечатки и плоский юмор — крайне тороплюсь.
Веру-ю-у! В авиацию, в научную революци-ю-у, в механизацию сельского хозяйства, в космос и невесомость! Веру-ю-у! Ибо это объективно-о! (Шукшин)
Re[4]: "Хитрый" проход по контейнеру полиморфных элементов.
К сожалению, не подходит.
_>Передавая таким образом this как указатель на конкретного наследника _>5) а в функциях process(...) менеджер может творить всё что угодно, ориентируясь на известные ему интерфейсы наследников game_object'a
Вот здесь корень зла: объект может быть как drawable, так и serializable.
Re[5]: "Хитрый" проход по контейнеру полиморфных элементов.
Здравствуйте, Alxndr, Вы писали:
_>>Передавая таким образом this как указатель на конкретного наследника _>>5) а в функциях process(...) менеджер может творить всё что угодно, ориентируясь на известные ему интерфейсы наследников game_object'a
A>Вот здесь корень зла: объект может быть как drawable, так и serializable.
Тебе нужно определится, что для тебя важнее:
1. Высокая скорость прохода по объектам
2. Расширяемая функциональность и независимость от реализации базовых классов.
Если первое — то виртуальные функции и "жирный" интерфейс, если второе — то тут несколько вариантов:
а. Интерфейсы, раелизующие вызов функции по её классу.
б. Самый банальный способ — множественное (+виртуальное) наследование и dynamic_cast.
в. Способ "наращивания" возможностей (аля VCL) — в нкаждом новом производном классе наращивается какая-нибудь функциональность. Название класса соответствует функциональности.
И ещё замечание: откуда такое требование — хранить всё объекты в одном контейнере. Ихмо — все проблемы из-за него.
PS: если возникают вопросы, требующие нетревиальных решений, это означает, что что-то неправильно спроектировали.
Ну и кто тебе мешает применить в draw_game_object в serialize_game_object виртуальное наследование от game_object? Далее наследуешься новым классом super_game_object от этих двух и пишешь соответствующий код в Manager.
По сути, для тебя главное — точно определиться с деревом наследования game_object. Если это возможно (т.е. если структура жесткая), то Visitor прокатит просто замечательно, поверь опыту ^_~
Если же твоя структура обязана быть гибкой (by design, что называется), то есть и второй вариант. Делай функции по количеству вариантов действий (т.е. draw(), save()/load() и .т.п.), причем в базовом game_object делай пустые реализации этих функций. А в производных классах пиши реализации выборочно, в зависимости от возможностей конкретного объекта. Это даст тебе следующее: каждый объект game_object будет "как бы" реализовывать всё, но реально — только то, что ты переписал в наследнике. Тогда ты сможешь в менеджере перебирать всю очередь с вызовом, допустим, с draw(), не заботясь о том, какой из объектов обработает вызов, а какой — нет.
Удачи.
Re: "Хитрый" проход по контейнеру полиморфных элементов.
уже упоминали паттерн Visitor, но я позволю себе несколько изменить и прокомментировать предложенное:
1) В game_object объявляем дополнительную виртуальную функцию:
class game_object {
public:
virtual void visit(object_visiter& visiter) {visiter.accept(*this);}
};
2) В каждый производный класс копируем visit как есть.
3) Пишим базовый класс object_visiter
struct object_visiter {
void accept(game_object&) {};
//свой accept для каждого производного интерфейсаvoid accept(serializable& obj) {}
void accept(dravable& obj) {}
};
4) В manager-е добовляем функцию прохода по списку
void manager::for_each(object_visiter& visiter) {
for (objects_queue::iterator it = objects_.begin(); it != objects_.end(); ++it)
(*it)->visit(visiter);
}
А вот теперь пишем собственно логику с помощью конечных (конкретных ) visiter-ов:
Сразу видны недостатки подхода:
1) если иерархия часто меняется поддерживать этот код довольно трудно.
2) на одно действие с объектом не менее 2 вызова виртуальных функций.
Зато если иерархия стабильна лёгкость добавления новой функциональности просто
Со скоростью можно разобраться, когда поймёшь где тормазит, с помощю специализированных контейнеров, для однотипных элементов. Например, хотелось ускорить проход при рисовке:
уже упоминали паттерн Visitor, но я позволю себе несколько изменить и прокомментировать предложенное:
мне кажется адбстракция визитор не соответвует посталеной задаче..
вот вариант с гранями не требующий переписывать провежуточные классы при добавлении новых функций (новые функци добавляются как внешние грани, а в базовом классе прописывается виртуаьный оператор получения грани)
class drawable //грани для классов
{
public:
virtual void doit() = 0;
virtual operator bool () = 0;
};
class serialize //тоже
{
public:
virtual void doit() = 0;
virtual operator bool () = 0;
};
template<class valueT> // обертка для класса имеющего граньclass draw_face : public drawable
{
valueT* _p;
public:
draw_face(valueT* src) : _p(src) {}
virtual void doit() { _p->draw() ; }
virtual operator bool () { return _p == 0 ; }
};
template<class valueT> //тоже оберткаclass serialize_face : public serialize
{
valueT* _p;
public:
serialize_face(valueT* src) : _p(src) {}
virtual void doit() { _p->serlz() ; }
virtual operator bool () { return _p == 0 ; }
};
//базовый классclass game_object
{
public:
virtual operator drawable* () { return 0; }
virtual operator serialize* () { return 0; }
};
//два класса один моджет обе операции другой только одну#include <iostream>
class any_game_obj : public game_object
{
public:
operator drawable* () { return new draw_face<any_game_obj>(this); }
operator serialize* () { return new serialize_face<any_game_obj>(this); }
void draw() { std::cout << " any game obje :: draw" << std::endl; }
void serlz() {std::cout << " any game obje :: serlz" << std::endl; }
};
class any_draw_obj : public game_object
{
public:
operator drawable* () { return new draw_face<any_draw_obj>(this); }
void draw() {std::cout << " any dwaw obj :: draw" << std::endl; }
};
//контейнер (память не чищу либо!)#include <list>
class engine
{
std::list<drawable*> _drawed;
std::list<serialize*> _serlz;
void add_draw(drawable* src)
{
if ( src ) _drawed.push_back(src);
}
void add_serialize(serialize* src)
{
if ( src ) _serlz.push_back(src);
}
public:
void reg_obj(game_object& src)
{
add_serialize(src);
add_draw(src);
}
void draw_all()
{
for ( std::list<drawable*>::iterator it = _drawed.begin(); it != _drawed.end(); ++ it)
(*it)->doit();
}
void serlz_all()
{
for ( std::list<serialize*>::iterator it = _serlz.begin(); it != _serlz.end(); ++ it)
(*it)->doit();
}
};
int main()
{
engine e;
any_game_obj o1;
any_draw_obj o2;
e.reg_obj(o1);
e.reg_obj(o2);
e.draw_all();
e.serlz_all();
return 0;
}
any game obje :: draw
any dwaw obj :: draw
any game obje :: serlz
Веру-ю-у! В авиацию, в научную революци-ю-у, в механизацию сельского хозяйства, в космос и невесомость! Веру-ю-у! Ибо это объективно-о! (Шукшин)