Еще один вопрос, связанный с дизайном C++

Привет!

Я пытаюсь найти некоторые оптимальные решения в шаблонах кодирования C++, и это один из моих вопросов, связанных с игровым движком.

Взгляните на объявление игрового объекта (я удалил почти все, что не имеет отношения к вопросу).

// Abstract representation of a game object
class Object : 
public Entity, 
       IRenderable, ISerializable {

   // Object parameters
   // Other not really important stuff

public:
   // @note Rendering template will never change while
   // the object 'lives'
   Object(RenderTemplate& render_template, /* params */) : /*...*/ { }

private:
   // Object rendering template
   RenderTemplate render_template;

public:
   /**
    * Default object render method
    * Draws rendering template data at (X, Y) with (Width, Height) dimensions
    *
    * @note If no appropriate rendering method overload is specified 
    * for any derived class, this method is called
    *
    * @param  Backend & b  
    * @return void
    * @see         
    */
   virtual void Render(Backend& backend) const {
      // Render sprite from object's
      // rendering template structure
      backend.RenderFromTemplate(
         render_template, 
         x, y, width, height
         );
   }
};

Вот также объявление интерфейса IRenderable:

// Objects that can be rendered
interface IRenderable {
   /**
    * Abstract method to render current object
    *
    * @param  Backend & b  
    * @return void
    * @see
    */
   virtual void Render(Backend& b) const = 0;
}

и образец реального объекта, полученного из Object (с серьезными упрощениями :)

// Ball object
class Ball : public Object {
   // Ball params
public:
   virtual void Render(Backend& b) const {
      b.RenderEllipse(/*params*/);
   }
};

Что я хотел получить, так это возможность иметь какую-то стандартную функцию, которая рисовала бы спрайт для объекта (это Object::Render), если нет соответствующей перегрузки.

Таким образом, у объекта могут быть объекты без метода Render(...), и если вы попытаетесь их визуализировать, будет вызван этот механизм рендеринга спрайтов по умолчанию. Кроме того, могут быть специализированные объекты, определяющие собственный способ отображения.

Я думаю, что такой способ ведения дел довольно хорош, но вот что я не могу понять: есть ли способ отделить реализацию "обычных" методов объектов (таких как Resize(...) или Rotate(...)) от их реализации рендеринга?< /сильный>

Потому что, если все сделать так, как описано ранее, общий файл .cpp, который реализует любой тип объекта, обычно смешивает реализацию методов Resize(...) и т. д. и этот метод virtual Render(...), и это кажется беспорядком. Я вообще-то хочу, чтобы процедуры рендеринга объектов были в одном месте, а их "логическая реализация" - в другом.

Есть ли способ это сделать (может быть, альтернативный шаблон, трюк или подсказка), или все эти полиморфные и виртуальные вещи отстойны с точки зрения размещения кода?


person M. Williams    schedule 21.04.2010    source источник
comment
Это не отвечает на ваш вопрос, но где ваш виртуальный деструктор в коде IRenderable? Без него вы потеряете много памяти.   -  person wheaties    schedule 22.04.2010
comment
Я добавлю его, когда у меня будет выделение памяти через new в реальном коде. То, что я делаю сейчас (только для тестирования и создания прототипа), в целом похоже на создание Ball ball и вызов ball.Render(backend);. Я понимаю, что в этой ситуации мне даже не нужен этот virtual материал, но я думаю, что скоро я сделаю что-то вроде std::vector<IRenderable*>, и тогда мне действительно понадобится виртуальный деструктор. В любом случае, спасибо за указание :)   -  person M. Williams    schedule 22.04.2010
comment
Лучше просто сделать это в самом начале и покончить с этим; зачем рисковать забыть потом? Это одна строка кода.   -  person Dennis Zickefoose    schedule 22.04.2010


Ответы (2)


В данном конкретном примере я не уверен, что желательно отделять Resize от Render, так как Resize напрямую влияет на визуализируемое изображение.

Я делаю так, чтобы Entitys содержали Renderable. Например. Entity будет иметь переменную-член, которая является визуализируемым спрайтом. Когда объект становится видимым, он передает спрайт системе рендеринга и говорит, по сути, «хорошо, рисуй это в каждом кадре, пока я не скажу тебе остановиться», хотя движок рендеринга отбрасывает объекты за кадром и прочее безумие. Возможно, очевидно, что движок рендеринга затем поддерживает список объектов для рендеринга (или, может быть, несколько списков, если вы хотите отделить спрайты от линий или частиц для эффективности рендеринга), а затем он просто перебирает этот гораздо менее сложный список элементов, когда приходит время сделать розыгрыш.

Таким образом, сущность может управлять визуализируемым объектом, масштабировать или поворачивать его (т. он "живой". Однако движок рендеринга знает только, как рисовать рассматриваемый элемент, и ему не нужны знания вашего графа сущностей.

person dash-tom-bang    schedule 23.04.2010
comment
Я, вероятно, должен упомянуть, что создание визуализируемого компонента компонента сущности, а не базового класса, позволяет совместно использовать визуализируемое представление между экземплярами любого заданного типа сущности. Вам нужно загрузить только один набор зомби-графики, даже если на вашем уровне десятки зомби. - person dash-tom-bang; 23.04.2010
comment
Этот подход кажется хорошим, я попытаюсь реорганизовать свой код и посмотреть, что произойдет и с какими другими проблемами я столкнусь. Большое спасибо. - person M. Williams; 23.04.2010

Вытащите IRenderable из Entity и наследуйте от этого интерфейса только тогда, когда захотите.

Затем используйте посетителя:



struct rendering_visitor : visitor<Entity>, visitor<IRenderable>
{
  void visit(Entity & e) { ... do default stuff ... }
  void visit(IRenderable & renderable) { renderable.Render(); }
};

См. "Современный дизайн C++" для хорошей реализации посетителя. Ваш Renderable должен быть доступен для посещения, как и ваши сущности. У вас есть какая-то высшая архия, о которой не говорится в рекомендуемой книге, но которую не так уж сложно понять.

Есть и другие способы реализации посетителей. Выбирайте.

person Edward Strange    schedule 21.04.2010
comment
Как это работает, когда в дизайне указано, что объект наследуется как от Entity, так и от IRenderable? Думаю, в С++ это будет неоднозначный вызов? (Но в C++ нет ключевого слова «интерфейс», не так ли?) - person dash-tom-bang; 23.04.2010
comment
Довольно часто в С++ переопределяют структуру как интерфейс, чтобы сохранить некоторую логику кода... Хотя я не уверен, что следует делать в этом случае множественного наследования. - person M. Williams; 23.04.2010
comment
@dash-tom-bang - Да, это может быть проблемой. Если оба базовых класса имеют реализации одной и той же функции (а с некоторыми шаблонами посетителей вы будете), у вас могут возникнуть проблемы. Пока функция, которую вы переопределяете, исходит от того же дедушки и бабушки, хотя кажется, что все в порядке. Я делаю это на одной платформе во всяком случае. Единственный раз, когда я столкнулся с проблемой, это когда обе базы объявляют новые виртуальные машины с одним и тем же именем. Когда это произойдет, вы застрянете, придумывая что-то другое и/или переделывая дерево/сеть. - person Edward Strange; 23.04.2010
comment
Моя точка зрения была конкретно с посетителем, хотя. Вызов myVisitor.visit(myObject) неоднозначен, поскольку Object является производным от обоих посещенных классов. Вместо этого вам придется сделать myVisitor.visit(static_cast<Entity>(myObject)), тем самым потеряв силу посетителя в этом случае. - person dash-tom-bang; 23.04.2010