Відокремлення ігрових даних / логіки від візуалізації


21

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


4
Використовуйте композицію, щоб фактично приєднати виправданий елемент до вашого об’єкта і мати ваш об'єкт взаємодіючи з цим m_renderableчленом. Таким чином, ви можете краще розділити свою логіку. Не застосовуйте передавальний "інтерфейс" на загальних об'єктах, які також мають фізику, ai та інше. Після цього ви можете керувати рендерами окремо. Вам потрібен шар абстрактизації над викликами функції OpenGL, щоб ще більше розв’язати речі. Таким чином, не сподівайтеся, що хороший двигун може здійснювати будь-які дзвінки GL API всередині різних реалізованих реалізацій. Ось це, в мікрокоротці.
теодрон

1
@teodron: Чому ти не поставив це як відповідь?
Тапіо

1
@Tapio: тому що це не стільки відповідь; це скоріше пропозиція.
теодрон

Відповіді:


20

Ідея полягає у використанні шаблону дизайну для відвідувачів. Вам потрібна реалізація 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.


1
Це досить гарна відповідь! Ви могли б Entity/Componentтрохи більше підкреслити альтернативу, оскільки вона може допомогти відокремити постачальників геометрії від інших частин двигуна (AI, Physics, Networking або загальний геймплей). +1!
теодрон

1
@teodron, я не буду пояснювати альтернативу E / C, оскільки це ускладнить речі. Але я думаю, що вам слід змінити ObjectAі ObjectBper, DrawableComponentAі DrawableComponentB, і всередині методів візуалізації, використовувати інші компоненти, якщо вам це потрібно, як-от: position = component->getComponent("Position");І в головному циклі у вас є список черпаючих компонентів, з якими можна викликати draw.
Чжен

Чому б просто не мати інтерфейс (наприклад Renderable), який має draw(Renderer&)функцію, і всі об'єкти, які можна візуалізувати, реалізувати їх? У такому випадку Rendererпросто потрібна одна функція, яка приймає будь-який об’єкт, що реалізує загальний інтерфейс та виклик renderable.draw(*this);?
Віте Сокіл

1
@ViteFalcon, Вибачте, якщо я не даю зрозуміти, але для детального пояснення мені потрібно більше місця та коду. В основному, моє рішення переміщує gl_*функції в рендерінг (відділяє логіку від візуалізації), але ваше рішення переміщує gl_*виклики в об'єкти.
Чжень

Таким чином, функції gl * дійсно переміщені з об'єктного коду, але я все ще зберігаю змінні ручки, що використовуються при візуалізації, як-от буфер / тексту тексту, уніформа / атрибути.
felipe

4

Я знаю, що ви вже прийняли відповідь Джен, але я хотів би поставити іншу там, на випадок, якщо це допоможе комусь іншому.

Щоб ще раз підтвердити проблему, ОП бажає можливість тримати код візуалізації окремим від логіки та даних.

Моє рішення - використовувати інший клас разом, щоб візуалізувати компонент, який є окремим від Rendererкласу логіки та. Спочатку повинен бути Renderableінтерфейс, який має функцію, bool render(Renderer& renderer);і Rendererклас використовує шаблон відвідувача для отримання всіх Renderableекземплярів, враховуючи список GameObjects і видає ті об'єкти, які мають Renderableекземпляр. Таким чином, Renderer не повинен знати про кожен тип об'єкта там, і він все ще несе обов'язок кожного типу об'єктів повідомляти це за Renderableдопомогою getRenderable()функції. Або ж ви можете створити RenderableVisitorклас, який відвідує всі GameObjects, і, виходячи з індивідуальних 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
    }
};

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;
}

3

Це повністю залежить від того, чи можете ви робити припущення щодо того, що є спільним для всіх суб'єктів, що надаються, чи ні. У моєму двигуні всі об'єкти відображаються однаково, тому просто потрібно забезпечити vbos, текстури та перетворення. Тоді рендерір отримує всі вони, таким чином ніякі виклики функцій OpenGL взагалі не потрібні для різних об'єктів.


1
погода = дощ, сонце, спекотно, холодно: P -> більш
сильно

3
@TobiasKienzler Якщо ви збираєтеся виправити його правопис, спробуйте правильно написати :-)
TASagent

@TASagent Що і гальмує закон Мафрі ? м- /
Тобіас Кіенцлер

1
виправив цю
друкарню

2

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

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

Далі слід відокремити екземпляр даної сітки від самої сітки. Скажіть, у вас на екрані сто дерев, кожне з яких використовує ту саму сітку. Ви хочете зберегти геометрію лише один раз, але вам знадобляться окремі матриці розташування та обертання для кожного дерева. Більш складні об'єкти, такі як анімовані гуманоїди, також матимуть додаткову інформацію про стан (наприклад, скелет, набір застосовуваних в даний час анімацій тощо).

Для надання, наївний підхід полягає в переході до кожної ігрової сутності та вказує на себе. Крім того, кожна сутність (коли вона породжує) може вставляти свої об'єкти, що передаються, в об'єкт сцени. Потім ваша функція візуалізації повідомляє сцені відображення. Це дозволяє сцені робити складні речі, пов'язані з візуалізацією, без вбудовування цього коду в будь-які ігрові об'єкти або в певний підклас для відтворення.


2

Ця порада насправді не характерна для надання, але повинна допомогти розробити систему, яка значною мірою несе речі. По-перше, спробуйте зберегти дані "GameObject" окремо від інформації про позицію.

Варто зазначити, що проста інформація XYZ про позиції може бути не такою простою. Якщо ви використовуєте двигун фізики, то позиціонуючі дані можуть зберігатися всередині сторонніх двигунів. Вам або доведеться синхронізувати між ними (що передбачає багато безглуздого копіювання пам'яті) або запитувати інформацію безпосередньо з двигуна. Але не всі об’єкти потребують фізики, деякі будуть закріплені на місці, тому простий набір поплавців прекрасно працює там. Деякі можуть бути прикріплені до інших об'єктів, тому їх положення фактично є зміщенням іншого положення. У розширених налаштуваннях у вас може бути збережена позиція лише на графічному процесорі, єдиний раз, коли це знадобиться комп'ютерній стороні, це сценарій, зберігання та реплікація мережі. Таким чином, ви, ймовірно, маєте декілька можливих варіантів для своїх позиційних даних. Тут є сенс використовувати спадщину.

Замість того, щоб об'єкт володів його позицією, натомість сам об'єкт повинен належати структурі даних індексації. Наприклад, "рівень" може мати Октре, або, можливо, "сцену" двигуна фізики. Коли ви хочете візуалізувати (або встановити сцену візуалізації), ви запитуєте у вашій спеціальній структурі об’єкти, видимі для камери.

Це також допомагає забезпечити гарне управління пам’яттю. Таким чином, об’єкт, який насправді не знаходиться в районі, навіть не має положення, яке має сенс, а не повернення 0,0-коордів або тих координат, які він мав, коли він був останній у зоні.

Якщо ви більше не зберігаєте координати в об'єкті, замість object.getX () ви отримаєте level.getX (object). Проблема з тим, що шукати об'єкт на рівні, швидше за все, буде повільною роботою, оскільки йому доведеться переглядати всі об'єкти, які відповідають його запитам.

Щоб уникнути цього, я, мабуть, створив би спеціальний клас "посилання". Той, який пов'язує між рівнем і об'єктом. Я називаю це "Місцеположення". Це містило б координати xyz, а також ручку до рівня та ручку для об'єкта. Цей клас посилань буде зберігатися в просторовій структурі / рівні, і об'єкт має слабке посилання на нього (якщо рівень / місце розташування знищено, референс об'єктів потрібно оновити до нуля. Можливо, також варто мати клас "Місце" фактично "власний" об'єкт, таким чином, якщо рівень видалений, так само особлива структура індексу, місця, які він містить, і його "Об'єкти".

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

Тепер інформація про позицію зберігається лише в одному місці. Не дублюється між Об'єктом, структурою просторової індексації, візуалізатором тощо.

Структурам просторових даних, таких як Octrees, часто навіть не потрібно мати координати об'єктів, які вони зберігають. Там позиція зберігається у відносному розташуванні вузлів у самій структурі (це можна розглядати як щось подібне до стиснення втрат, жертвуючи точністю для швидкого часу пошуку). Якщо об'єкт розташування знаходиться в Окремі, то фактичні координати знаходяться всередині нього, як тільки запит буде виконаний.

Або якщо ви використовуєте двигун фізики для управління місцями своїх об'єктів або сумішшю між ними, клас Location повинен це обробляти прозоро, зберігаючи весь код у одному місці.

Ще однією перевагою є позиція та відповідність рівню, які зберігаються в тому самому місці. Ви можете реалізувати object.TeleportTo (other_object) і налагодити його роботу на різних рівнях. Аналогічно, пошук ШІ може знайти щось в іншій області.

Що стосується надання. Ваша візуалізація може бути схожою з Місцеположенням. За винятком того, що там буде відображатися конкретний матеріал. Вам, мабуть, не потрібно зберігати в цій структурі об'єкт або рівень. Об'єкт може бути корисним, якщо ви намагаєтеся зробити щось на кшталт вибору кольорів чи надання візуальної панелі, що пливе над нею тощо, але в іншому випадку рендерінг дбає лише про сітку та інше. 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 був би там, де ви могли б кешувати речі, якщо камера не рухається сильно).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.