Отделение игровых данных / логики от рендеринга

21

Я пишу игру с использованием C ++ и OpenGL 2.1. Я думал, как я могу отделить данные / логику от рендеринга. На данный момент я использую базовый класс Renderable, который предоставляет чисто виртуальный метод для реализации рисования. Но у каждого объекта есть такой специализированный код, что только объект знает, как правильно установить форму шейдера и организовать данные буфера массива вершин. Я заканчиваю большим количеством вызовов функций gl * по всему коду. Есть ли общий способ рисования объектов?

Felipe
источник
4
Используйте композицию, чтобы фактически прикрепить визуализируемый объект к вашему объекту и заставить ваш объект взаимодействовать с этим m_renderableчленом. Таким образом, вы можете лучше отделить свою логику. Не применяйте визуализируемый «интерфейс» к общим объектам, которые также имеют физику, ai и еще много чего. После этого вы можете управлять визуализируемыми объектами отдельно. Вам нужен уровень абстракции по сравнению с вызовами функций OpenGL, чтобы еще больше отделить вещи. Поэтому не ожидайте, что хороший движок будет иметь какие-либо вызовы GL API внутри своих различных визуализируемых реализаций. Вот и все, в двух словах.
Теодрон
1
@teodron: Почему ты не назвал это ответом?
Тапио
1
@Tapio: потому что это не так уж много ответа; это скорее предложение.
Теодрон

Ответы:

20

Идея состоит в том, чтобы использовать шаблон проектирования Visitor. Вам нужна реализация Renderer, которая знает, как визуализировать реквизит. Каждый объект может вызвать экземпляр рендерера для обработки задания рендеринга.

В нескольких строках псевдокода:

class Renderer {
public:
    void render( const ObjectA & obj );
    void render( const ObjectB & obj );
};


class ObjectA{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

class ObjectB{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

Вещество gl * реализовано методами рендерера, и объекты хранят только данные, необходимые для визуализации, положение, тип текстуры, размер и т. Д.

Кроме того, вы можете настроить различные средства визуализации (debugRenderer, hqRenderer и т. Д.) И использовать их динамически, без изменения объектов.

Это также может быть легко объединено с системами Entity / Component.

Zhen
источник
1
Это довольно хороший ответ! Вы могли бы подчеркнуть Entity/Componentальтернативу немного больше, поскольку она может помочь отделить поставщиков геометрии от других частей движка (ИИ, Физика, Сеть или общий игровой процесс). +1!
Теодрон
1
@ Теодрон, я не буду объяснять альтернативу E / C, потому что это усложнит ситуацию. Но я думаю, что вы должны изменить ObjectAи ObjectBper, DrawableComponentAи DrawableComponentB, внутри, и методы рендеринга, использовать другие компоненты, если вам это нужно, например: position = component->getComponent("Position");А в основном цикле у вас есть список доступных для рисования компонентов, с которыми вызывается draw.
Жень
Почему бы просто не иметь интерфейс (например Renderable), который имеет draw(Renderer&)функцию, и все объекты, которые могут быть отображены, реализуют их? В каком случае Rendererпросто нужна одна функция, которая принимает любой объект, который реализует общий интерфейс и вызов renderable.draw(*this);?
Vite Falcon
1
@ViteFalcon, извините, если я не проясню, но для подробного объяснения мне нужно больше места и кода. По сути, мое решение перемещает gl_*функции в рендерер (отделяя логику от рендеринга), но ваше решение перемещает gl_*вызовы в объекты.
Жень
Таким образом, функции gl * действительно удаляются из объектного кода, но я по-прежнему сохраняю переменные дескриптора, используемые при рендеринге, такие как идентификаторы буфера / текстуры, расположение униформ / атрибутов.
Фелипе
4

Я знаю, что вы уже приняли ответ Жена, но я хотел бы выложить еще один на всякий случай, если он кому-нибудь поможет.

Чтобы повторить проблему, OP хочет иметь возможность отделить код рендеринга от логики и данных.

Мое решение состоит в том, чтобы использовать другой класс для визуализации компонента, который отделен от Rendererкласса логики и. Сначала должен быть Renderableинтерфейс, который имеет функцию, bool render(Renderer& renderer);и Rendererкласс использует шаблон посетителя для извлечения всех Renderableэкземпляров, учитывая список GameObjects, и отображает те объекты, которые имеют Renderableэкземпляр. Таким образом, Renderer не нужно знать о каждом типе объекта, и каждый тип объекта несет ответственность за информирование об этом Renderableчерез getRenderable()функцию. Или, в качестве альтернативы, вы можете создать RenderableVisitorкласс, который посещает все объекты GameObject и в зависимости от индивидуальных GameObjectусловий, которые они могут выбрать, добавлять или не добавлять их для визуализации посетителю. В любом случае, основная суть в том, чтоgl_*все вызовы находятся вне самого объекта и находятся в классе, который знает интимные детали самого объекта, а не того, который является частью Renderer.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ : Я написал эти классы вручную в редакторе, так что есть большая вероятность, что я что-то упустил в коде, но, надеюсь, вы поймете эту идею.

Чтобы показать (частичный) пример:

Renderable интерфейс

class Renderable {
public:
    Renderable(){}
    virtual ~Renderable(){}
    virtual void render(Renderer& renderer) const = 0;
};

GameObject учебный класс:

class GameObject {
public:
    GameObject()
        : mVisible(true)
        , mMarkedForDelete(false) {}

    virtual ~GameObject(){}

    virtual Renderable* getRenderable() {
        // By default, all GameObjects are missing their Renderable
        return NULL;
    }

    void setVisible(bool visible) {
        mVisible = visible;
    }

    bool isVisible() const {
        return getRenderable() != null && !isMarkedForDeletion() && mVisible;
    }

    void markForDeletion() {
        mMarkedForDelete = true;
    }

    bool isMarkedForDeletion() const {
        return mMarkedForDelete;
    }

    // More GameObject functions

private:
    bool mVisible;
    bool mMarkedForDelete;
};

(Частичный) Rendererкласс.

class Renderer {
public:
    void renderObjects(std::vector<GameObject>& gameObjects) {
        // If you want to do something fancy with the renderable GameObjects,
        // create a visitor class to return the list of GameObjects that
        // are visible instead of rendering them straight-away
        std::list<GameObject>::iterator itr = gameObjects.begin(), end = gameObjects.end();
        while (itr != end) {
            GameObject* gameObject = *itr++;
            if (gameObject == null || !gameObject->isVisible()) {
                continue;
            }
            gameObject->getRenderable()->render(*this);
        }
    }

};

RenderableObject учебный класс:

template <typename T>
class RenderableObject : public Renderable {
public:
    RenderableObject(T& object)
        :mObject(object) {}
    virtual ~RenderableObject(){}

    virtual void render(Renderer& renderer) {
        return render(renderer, mObject);
    }

protected:
    virtual void render(Renderer& renderer, T& object) = 0;
};

ObjectA учебный класс:

// Forward delcare ObjectARenderable and make sure the constructor
// definition in the CPP file where ObjectARenderable gets included
class ObjectARenderable;

class ObjectA : public GameObject {
public:
    ObjectA()
        : mRenderable(new ObjectARenderable(*this)) {}

    // All data/logic

    Renderable* getRenderable() {
        return mRenderable.get();
    }

protected:
    // boost or std shared_ptr to make sure that the renderable instance is
    // cleaned up with the destruction of this object.
    shared_ptr<Renderable> mRenderable;
};

ObjectARenderable учебный класс:

#include "ObjectA.h"

class ObjectARenderable : public RenderableObject<ObjectA> {
public:
    ObjectARenderable(ObjectA& instance) {
        : RenderableObject<ObjectA>(instance) {}

protected:
    virtual void render(Renderer& renderer, T& object) {
        // gl_* class to render ObjectA
    }
};
Vite Falcon
источник
4

Построить систему команд рендеринга. Высокоуровневый объект, который имеет доступ как к OpenGLRendererобъекту сцены, так и к игровому объекту, будет выполнять итерацию графа сцены или игровых объектов и создавать пакет RenderCmds, который затем будет представлен объекту, OpenGLRendererкоторый будет рисовать каждый по очереди, и, таким образом, содержащий все OpenGL. связанный код в нем.

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

class OpenGLRenderer
{
public:
    typedef GLuint GeometryBuffer;
    typedef GLuint TextureID;
    typedef std::vector<RenderCmd> RenderBatch; 

    void Render(const RenderBatch& renderBatch);   // set shaders, set active textures, draw geometry, ...

    MeshID CreateGeometryBuffer(...);
    TextureID CreateTexture(...);

    // ....
}

struct RenderCmd
{
    GeometryBuffer mGeometryBuffer;
    TextureID mTexture;
    Mat4& mWorldMatrix;
    bool mLightingEnabled;
    // .....
}

std::vector<GameObject> gYourGameObjects;
RenderBatch BuildRenderBatch()
{
    RenderBatch ret;

    for (GameObject& object : gYourGameObjects)
    { 
        // ....
    }

    return ret;
}
KaiserJohaan
источник
3

Это полностью зависит от того, можете ли вы сделать предположения о том, что является общим для всех визуализируемых объектов или нет. В моем движке все объекты отображаются одинаково, поэтому нужно просто предоставить vbos, текстуры и преобразования. Затем средство рендеринга извлекает их все, поэтому вызовы функций OpenGL в разных объектах вообще не нужны.

Данияр
источник
1
погода = дождь, солнце, жарко, холодно: P -> wether
Тобиас Кинцлер
3
@TobiasKienzler Если вы собираетесь исправить его орфографию, попробуйте правильно
написать
@TASagent Что, и тормозят закон Муфри ? m- /
Тобиас Кинцлер
1
исправлена , что опечатка
Данияр
2

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

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

Кроме того, вы должны отделить экземпляр данной сетки от самой сетки. Скажем, у вас есть сто деревьев на экране, каждое из которых использует одну и ту же сетку. Вы хотите сохранить геометрию только один раз, но вам понадобятся отдельные матрицы местоположения и поворота для каждого дерева. Более сложные объекты, такие как анимированные гуманоиды, также будут иметь дополнительную информацию о состоянии (например, скелет, набор применяемых в данный момент анимаций и т. Д.).

Для рендеринга наивный подход состоит в том, чтобы перебирать каждую игровую сущность и указывать ей визуализировать себя. Альтернативно, каждая сущность (когда она появляется) может вставлять свой визуализируемый объект (ы) в объект сцены. Затем ваша функция рендеринга сообщает рендеру сцену. Это позволяет сцене делать сложные вещи, связанные с рендерингом, без встраивания этого кода в игровые объекты или в конкретный воспроизводимый подкласс.

Andrews
источник
2

Этот совет не является специфическим для рендеринга, но должен помочь создать систему, которая в значительной степени разделяет вещи. Сначала попытайтесь сохранить данные GameObject отдельно от информации о местоположении.

Стоит отметить, что простая позиционная информация XYZ может быть не такой простой. Если вы используете физический движок, то данные о вашем местоположении могут храниться в стороннем движке. Вам нужно будет либо синхронизировать их (что потребует большого количества бессмысленного копирования памяти), либо запросить информацию непосредственно из движка. Но не всем объектам нужна физика, некоторые из них будут зафиксированы на месте, так что простой набор поплавков там отлично работает. Некоторые из них могут даже быть прикреплены к другим объектам, поэтому их позиция фактически является смещением другой позиции. В расширенной настройке положение может храниться только на графическом процессоре, единственное время, которое потребуется на стороне компьютера, - это скрипты, хранилище и сетевая репликация. Таким образом, у вас, вероятно, будет несколько возможных вариантов для ваших позиционных данных. Здесь имеет смысл использовать наследование.

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

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

Если вы больше не сохраняете координаты в объекте, вместо object.getX () у вас получится level.getX (object). Проблема с поиском объекта на уровне, вероятно, будет медленной операцией, так как ему придется просматривать все свои объекты и соответствовать тому, который вы запрашиваете.

Чтобы избежать этого, я бы, вероятно, создал специальный класс 'link'. Тот, который связывает между уровнем и объектом. Я называю это «Место». Это будет содержать координаты XYZ, а также дескриптор уровня и дескриптор объекта. Этот класс ссылок будет храниться в пространственной структуре / уровне, и объект будет иметь слабую ссылку на него (если уровень / местоположение будет уничтожен, необходимо изменить ссылку на объект на null. Возможно, также стоит иметь класс Location на самом деле «владеть» объектом, таким образом, если уровень удаляется, то же самое относится и к специальной структуре индекса, расположению, которое он содержит, и его объектам.

typedef std::tuple<Level, Object, PositionXYZ> Location;

Теперь информация о местоположении хранится только в одном месте. Не дублируется между объектом, пространственной структурой индексации, рендерером и т. Д.

Пространственные структуры данных, такие как Octrees, часто даже не нуждаются в координатах объектов, которые они хранят. Там положение сохраняется в относительном расположении узлов в самой структуре (это можно рассматривать как своего рода сжатие с потерями, жертвуя точностью ради быстрого времени поиска). С объектом местоположения в Octree тогда фактические координаты находятся внутри него, как только запрос сделан.

Или, если вы используете физический движок для управления местоположением ваших объектов или их сочетанием, класс Location должен обрабатывать это прозрачно, сохраняя весь ваш код в одном месте.

Еще одним преимуществом теперь является положение и ссылка на уровень хранится в том же месте. Вы можете реализовать object.TeleportTo (other_object) и заставить его работать на разных уровнях. Точно так же поиск пути ИИ может следовать за чем-то в другую область.

Что касается рендеринга. Ваш рендер может иметь аналогичную привязку к местоположению. За исключением того, что там будут конкретные вещи для рендеринга. Вы, вероятно, не нуждаетесь в 'Object' или 'Level', чтобы быть сохраненными в этой структуре. Объект может быть полезен, если вы пытаетесь сделать что-то вроде выбора цвета или рендеринга хитбара, плавающего над ним и т. Д., Но в противном случае средство визуализации заботится только о сетке и тому подобном. RenderableStuff будет Mesh, также может иметь ограничивающие рамки и так далее.

typedef std::pair<RenderableStuff, PositionXYZ> RenderThing;

renderer.render(level, camera);
renderer: object = level.getVisibleObjects(camera);
level: physics.getObjectsInArea(physics.getCameraFrustrum(camera));
for(object in objects) {
    //This could be depth sorted, meshes could be broken up and sorted by material for batch rendering or whatever
    rendering_que.addObjectToRender(object);
}

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

Ваш физический движок может иметь аналогичную абстракцию, так как ему также не нужны данные объекта, только сетка столкновений и физические свойства.

Все ваши основные данные объекта будут содержать имя меша, который использует объект. Затем игровой движок может загружать его в любом формате, который ему нравится, не обременяя ваш класс объектов кучей специфичных для рендеринга вещей (которые могут быть специфичными для вашего API рендеринга, то есть DirectX против OpenGL).

Это также держит различные компоненты отдельно. Это позволяет легко выполнять такие вещи, как замена вашего физического движка, поскольку эти вещи в основном находятся в одном месте. Это также делает юнит-тестирование намного проще. Вы можете тестировать такие вещи, как физические запросы, без необходимости устанавливать какие-либо поддельные объекты, поскольку все, что вам нужно, это класс Location. Вы также можете оптимизировать вещи проще. Это делает более очевидным, какие запросы нужно выполнять для каких классов и отдельных мест для их оптимизации (например, вышеупомянутый level.getVisibleObject был бы местом, где вы могли бы кэшировать вещи, если камера не перемещается слишком сильно).

Дэвид С. Бишоп
источник