将游戏数据/逻辑与渲染分离


21

我正在使用C ++和OpenGL 2.1编写游戏。我在想如何将数据/逻辑与渲染分开。目前,我使用基类“ Renderable”,该基类提供了一种纯虚拟方法来实现绘图。但是每个对象都有如此专门的代码,只有对象知道如何正确设置着色器制服和组织顶点数组缓冲区数据。最后,我在代码中多次进行gl *函数调用。有没有通用的绘制对象的方法?


4
使用合成将可渲染对象实际附加到对象上,并使对象与该m_renderable成员进行交互。这样,您可以更好地分离逻辑。不要在也具有物理特性,ai和诸如此类的常规对象上强制使用可渲染的“接口”。之后,您可以单独管理可渲染的对象。您需要在OpenGL函数调用上进行抽象化,以便进一步分离事物。因此,不要指望一个好的引擎在其各种可呈现的实现中包含任何GL API调用。就这么简单了。
teodron

1
@teodron:为什么不把它作为答案?
Tapio

1
@Tapio:因为答案不多;相反,它只是一个建议。
teodron

Answers:


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等)并动态使用它们,而无需更改对象。

与实体/组件系统结合起来也很容易。


1
这是一个很好的答案!您可能会更加强调Entity/Component替代方案,因为它可以帮助将几何图形提供程序与其他引擎部件(AI,物理,网络或一般游戏玩法)分开。+1!
teodron

1
@teodron,我不会解释E / C替代方案,因为它会使事情复杂化。但是,我认为你应该改变ObjectAObjectBDrawableComponentADrawableComponentB和内部渲染方法,如果你需要它,就像使用其他组件:position = component->getComponent("Position");在主循环,你必须绘制组件的列表来调用与借鉴。
2013年

为什么不仅仅拥有一个Renderable具有draw(Renderer&)功能的接口(如),并且所有可以呈现的对象都实现它们呢?在那种情况下,Renderer只需要一个函数接受任何实现公共接口并调用的对象renderable.draw(*this);
Vite Falcon

1
@ViteFalcon,对不起,如果我不清楚,但是要详细解释,我需要更多空间和代码。基本上,我的解决方案将gl_*函数移到渲染器中(将逻辑与渲染分离),但是您的解决方案将gl_*调用移到对象中。
2013年

这样,gl *函数确实移出了目标代码,但是我仍然保留渲染中使用的句柄变量,例如缓冲区/纹理ID,统一/属性位置。
felipe

4

我知道您已经接受了Zhen的回答,但是我想再提出一个答案,以防它对其他人有帮助。

为了重申这个问题,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
    }
};

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

这完全取决于您是否可以对所有可渲染实体的共同点做出假设。在我的引擎中,所有对象都以相同的方式呈现,因此只需要提供vbo,纹理和转换即可。然后,渲染器将提取所有这些渲染器,因此根本不需要在不同对象中调用OpenGL函数。


1
天气=雨,太阳,热,冷:P->
下午

3
@TobiasKienzler如果您要纠正他的拼写,请尝试拼写是否正确:-)
TASagent 2013年

@TASagent什么,刹车穆弗里定律m- /
Tobias Kienzler

1
更正了该错字
danijar

2

绝对将渲染代码和游戏逻辑放在不同的类中。合成(如Teodron所建议的)可能是做到这一点的最佳方法。游戏世界中的每个实体都会有自己的Renderable-或一组。

除了基本的纹理和照明着色器之外,您可能仍然具有多个Renderable子类,例如,用于处理骨骼动画,粒子发射器和复杂的着色器。Renderable类及其子类应仅包含渲染所需的信息:几何,纹理和着色器。

此外,应将给定网格的实例与网格本身分开。假设您的屏幕上有一百棵树,每棵树都使用相同的网格。您只想存储一次几何体,但是每棵树都需要单独的位置和旋转矩阵。更复杂的对象(例如动画人形生物)也将具有其他状态信息(例如骨架,当前应用的动画集等)。

要进行渲染,幼稚的方法是遍历每个游戏实体,并告诉其进行渲染。或者,每个实体(在生成时)都可以将其可渲染对象插入场景对象。然后,您的渲染函数告诉场景进行渲染。这允许场景执行复杂的与渲染相关的事情,而无需将该代码嵌入游戏实体或特定的可渲染子类中。


2

该建议并非真正针对渲染,但应该有助于提出一个使事情基本分开的系统。首先,尝试将“ GameObject”数据与位置信息分开。

值得注意的是,简单的XYZ位置信息可能不是那么简单。如果您使用的是物理引擎,则您的位置数据可以存储在第三方引擎中。您可能需要在它们之间进行同步(这将涉及很多无意义的内存复制),或者直接从引擎查询信息。但是并非所有物体都需要物理,某些物体将被固定在适当的位置,因此一组简单的浮标就可以很好地工作。有些甚至可能附着在其他对象上,因此它们的位置实际上是另一个位置的偏移。在高级设置中,您可能只在计算机端需要脚本,存储和网络复制的情况下才将位置存储在GPU上。因此,您可能有几种可能的位置数据选择。在这里使用继承是有意义的。

该对象本身应该由索引数据结构所有,而不是拥有其位置的对象。例如,“级别”可能具有八进制,或者可能具有物理引擎“场景”。当您要渲染(或设置渲染场景)时,可以查询特殊结构以获取相机可见的对象。

这也有助于提供良好的内存管理。这样,实际上不在区域中的对象甚至都没有有意义的位置,而不是返回0.0坐标或它最后在区域中时所具有的坐标。

如果不再将坐标保留在对象中,而不是object.getX(),则最终将具有level.getX(object)。这样做的问题是在关卡中查找对象可能是一个缓慢的操作,因为它必须仔细查看所有对象并匹配您查询的对象。

为了避免这种情况,我可能会创建一个特殊的“链接”类。绑定在一个关卡和一个对象之间的对象。我称其为“位置”。这将包含xyz坐标以及关卡的句柄和对象的句柄。该链接类将存储在空间结构/级别中,并且该对象将对其具有较弱的引用(如果级别/位置被破坏,则对象的引用需要更新为null。实际上,最好使用Location类“拥有”对象,这样,如果删除了某个级别,则特殊的索引结构,其包含的位置及其对象也是如此。

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

现在,位置信息仅存储在一个地方。在对象,空间索引结构,渲染器等之间不重复。

像Octrees这样的空间数据结构通常甚至不需要具有它们存储的对象的坐标。位置存储在结构本身中节点的相对位置中(可以将其视为一种有损压缩,为了快速查找而牺牲精度)。将位置对象放在Octree中之后,一旦完成查询,便可以在其中找到实际坐标。

或者,如果您使用物理引擎来管理对象位置或两者之间的混合,则Location类应该透明地处理该问题,同时将所有代码都放在一个位置。

现在的另一个优点是位置和水平的参考值存储在同一位置。您可以实现object.TeleportTo(other_object)并使它跨级别工作。同样,AI寻路可以将某些事物带入另一个区域。

关于渲染。您的渲染器可以对位置具有类似的绑定。除非那里有渲染特定的东西。您可能不需要将“对象”或“级别”存储在此结构中。如果尝试进行颜色选择或渲染悬停在其上方的悬停条等操作,则Object可能很有用,但否则渲染器仅关心网格等。RenderableStuff将是一个网格,也可能具有边界框等。

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

您可能不需要每帧都做一次,可以确保拍摄的区域比相机当前显示的区域大。对其进行缓存,跟踪对象的运动以查看边界框是否在范围内,跟踪相机的运动等。但是,在您进行基准测试之前,不要开始弄乱这种东西。

物理引擎本身可能具有类似的抽象,因为它也不需要Object数据,仅需要碰撞网格物体和物理属性。

您的所有核心对象数据将包含该对象使用的网格名称。然后,游戏引擎可以继续以任意格式加载它,而不会给对象类增加很多渲染特定的东西(可能特定于渲染API,即DirectX与OpenGL)。

它还将不同的组件分开。这样一来,更换物理引擎之类的事情就变得容易了,因为这些东西大部分都集中在一个位置。这也使单元测试更加容易。您可以测试诸如物理查询之类的东西,而无需设置任何实际的伪造对象,因为您所需要的只是Location类。您还可以更轻松地优化内容。它使您更清楚地知道需要对哪些类和单个位置执行哪些查询以对其进行优化(例如,如果摄像机移动不大,则可以使用上面的level.getVisibleObject进行缓存)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.