2014-09-28 5 views
6

Я большой поклонник создания игрового движка, способного адаптироваться не только в том, что он может сделать, но и в том, как он может обрабатывать новый код. В последнее время для моей графической подсистемы, я написал class быть переопределен, который работает следующим образом:Wrapper over Graphics APIs

class LowLevelGraphicsInterface { 
    virtual bool setRenderTarget(const RenderTarget* renderTarget) = 0; 
    virtual bool setStreamSource(const VertexBuffer* vertexBuffer) = 0; 
    virtual bool setShader(const Shader* shader) = 0; 
    virtual bool draw(void) = 0; 

    //etc. 
}; 

Моя идея состояла в том, чтобы создать список функций, которые являются универсальными среди большинства графических API. Тогда для DirectX11 Я бы просто создать новый дочерний class:

class LGI_DX11 : public LowLevelGraphicsInterface { 
    virtual bool setRenderTarget(const RenderTarget* renderTarget); 
    virtual bool setStreamSource(const VertexBuffer* vertexBuffer); 
    virtual bool setShader(const Shader* shader); 
    virtual bool draw(void); 

    //etc. 
}; 

Каждая из этих функций будет затем взаимодействовать с DX11 напрямую. Я понимаю, что здесь есть слой косвенности. Люди отключены этим фактом?

Является ли это широко используемым методом? Есть ли что-то еще, что я мог/должен был делать? Существует возможность использования препроцессора , но это кажется мне грязным. Кто-то также упомянул шаблоны для меня. Ребята, что вы думаете?

+1

Ваше решение легче читается, а препроцессоры и шаблоны могут избавиться от 'виртуальных' функций. – dari

+0

@ dari Действительно. Мне нравится читаемость и простота, но я также не хочу, чтобы слой косвенности слишком медленно замедлял работу.Честно говоря, я бы не колебался, если бы не тот факт, что мы говорим о графике здесь. –

+0

Существует также возможность просто связываться с соответствующей реализацией класса. Тогда вам не нужна косвенность. Но вы теряете способность обрабатывать несколько таких одновременно, а также возможность отправлять один exe, который работает с любой комбинацией таких вещей. –

ответ

7

Если вызов виртуальной функции становится проблемой, существует метод времени компиляции, который удаляет виртуальные вызовы с использованием небольшого количества препроцессора и оптимизации компилятора. Одна из возможных реализаций что-то вроде этого:

Объявите базовый рендер с чисто виртуальными функциями:

class RendererBase { 
public: 
    virtual bool Draw() = 0; 
}; 

Объявить конкретную реализацию:

#include <d3d11.h> 
class RendererDX11 : public RendererBase { 
public: 
    bool Draw(); 
private: 
    // D3D11 specific data 
}; 

Создать заголовок RendererTypes.h для пересылки декларируют на основе видеообработки по типу, который вы хотите использовать с некоторым препроцессором:

#ifdef DX11_RENDERER 
    class RendererDX11; 
    typedef RendererDX11 Renderer; 
#else 
    class RendererOGL; 
    typedef RendererOGL Renderer; 
#endif 

создать Также заголовок Renderer.h включить соответствующие заголовки для видеообработки:

#ifdef DX11_RENDERER 
    #include "RendererDX11.h" 
#else 
    #include "RendererOGL.h" 
#endif 

Теперь везде вы используете ваш рендер относится к нему как Renderer типа, включает в себя RendererTypes.h в файлах заголовки и Renderer.h в ваших CPP файлах.

Каждая из реализаций вашего рендеринга должна быть в разных проектах. Затем создайте различные конфигурации компоновки для компиляции с использованием какой бы визуализации вы не захотели использовать. Например, вы не хотите включать код DirectX для конфигурации Linux.

В отладочных сборках все еще могут быть выполнены вызовы виртуальных функций, но в сборках релизов они оптимизированы, потому что вы никогда не вызываете вызовы через интерфейс базового класса. Он используется только для обеспечения общей подписи для ваших классов визуализатора во время компиляции.

Хотя для этого метода требуется немного препроцессора, он минимален и не мешает читабельности вашего кода, так как он изолирован и ограничен некоторыми typedef и включает в себя. Единственным недостатком является то, что вы не можете переключать реализации рендеринга во время выполнения с помощью этого метода, поскольку каждая реализация будет построена для отдельного исполняемого файла. Однако, во всяком случае, нет никакой потребности в переключении конфигураций во время выполнения.

+2

Стоит отметить, что базовый класс и виртуальные функции не нужны, поскольку весь код относится к конкретному типу «Renderer». После исправления этого остается выбор правильной реализации в исходном коде с использованием пропроцессора. На самом деле нет никакой реальной альтернативы для выполнения этого выбора реализации в исходном коде, но это можно сделать более широко с помощью механизма сборки, просто имея соответствующий заголовок, включающий пути для системы под рукой. –

+0

Короче говоря, это может быть значительно упрощено. По существу мой ранний комментарий (до того, как этот ответ был опубликован). –

+1

Истинный базовый класс не нужен, но мне нравится иметь по двум причинам. Во-первых, для обеспечения того, чтобы реализации обеспечивали согласованный интерфейс и сохранялись синхронно. Два, чтобы обеспечить общий код между реализациями в базовом классе. Согласился, что настройка включает в себя пути - это хорошее решение для обработки заголовков. – megadan

0

Я использую подход с абстрактным базовым классом к устройству рендеринга в своем приложении. Прекрасно работает и позволяет динамически выбирать рендеринг для использования во время выполнения. (Я использую его для перехода с DirectX10 на DirectX9, если первый не поддерживается, то есть в Windows XP).

Я хотел бы указать, что вызов виртуальной функции не является частью, которая требует производительности, а включает преобразование типов аргументов. Чтобы быть действительно общим, публичный интерфейс с визуализатором использует собственный набор типов параметров, таких как пользовательский IShader и настраиваемый тип Matrix3D. Никакой тип, объявленный в API DirectX, не отображается в остальной части приложения, так как OpenGL будет иметь разные типы матриц и шейдерные интерфейсы. Недостатком этого является то, что мне нужно преобразовать все типы Matrix и Vector/Point из моего пользовательского типа в тот, который использует шейдер в реализации конкретного рендеринга. Это намного дороже, чем стоимость виртуального вызова функции.

Если вы выполняете различие с использованием препроцессора, вам также необходимо сопоставить различные типы интерфейсов, подобные этому. Многие из них одинаковы между DirectX10 и DirectX11, но не между DirectX и OpenGL.

Редактировать: См. Ответ в c++ Having multiple graphics options для примера реализации.

0

Итак, я понимаю, что это старый вопрос, но я не могу удержаться от него. Желание написать такой код - это просто побочный эффект, связанный с объектно-ориентированной индоктринацией.

Первый вопрос: действительно ли действительно необходимо поменять местами рендеринг, или просто подумайте, что это круто. Если соответствующий момент времени может быть определен во время сборки для данной платформы, тогда проблема решена: используйте простой, не виртуальный интерфейс с реализацией, выбранной во время сборки.

Если вы обнаружите, что вам действительно нужно поменять его, по-прежнему используйте не виртуальный интерфейс, просто загрузите реализации в виде разделяемых библиотек. При таком подкачке вам, скорее всего, понадобится код рендеринга кода, а также некоторый высокопроизводительный игровой код рендеринга, зависящий от производительности, и заменяемый. Таким образом, вы можете использовать общий, высокоуровневый интерфейс рендеринга движка для вещей, выполняемых главным образом движком, при этом все еще имея доступ к встроенному конкретному коду, чтобы избежать затрат на преобразование, упомянутых PMF.

Теперь следует сказать, что в то время как обмен с разделяемыми библиотеками представляет окольный, 1. Вы можете легко получить косвенность быть < до ~ =, что виртуальных вызовов и 2. высокого уровня косвенность никогда в любой существенной игре/движке. Основным преимуществом является сохранение разгруженного кода (и с пути), а также упрощение API и общий дизайн проекта, повышение удобочитаемости и понимания.

Начинающие, как правило, не знают об этом, потому что в наши дни так много слепых OO, но этот стиль «OO first, ask questions never» равен не без стоимости. Этот вид дизайна имеет стоимость понимания кода налогообложения и приводит к коду (гораздо более низкому уровню, чем этот пример), который по своей сути медленный. Конечно, ориентация на объекты имеет свое место, но (в играх и других приложениях с высокой производительностью) лучший способ разработки, который я нашел, - написать приложения как можно минимально OO, только уступая, когда проблема заставляет вас за руку. Вы разработаете интуицию для того, чтобы рисовать линию, когда вы приобретаете больше опыта.