我非常喜欢拥有一个能够适应能力的游戏引擎,不仅仅是它能做什么,还有它如何处理新代码。最近,对于我的 graphics 子系统,我编写了一个class
来覆盖,其工作原理如下:
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
对接。我确实意识到这里有一层间接。人们是否因此而被关闭?
这是一种广泛使用的方法吗?还有什么我可以/应该做的事情吗?可以选择使用预处理器,但这对我来说似乎很麻烦。有人还向我提到了模板。你们觉得怎么样?
答案 0 :(得分:8)
如果虚函数调用成为问题,则有一个编译时方法,它使用少量预处理器和编译器优化来删除虚拟调用。一种可能的实现是这样的:
使用纯虚函数声明基础渲染器:
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
,在cpp文件中包含Renderer.h
。
每个渲染器实现都应该在不同的项目中。然后创建不同的构建配置,以使用您要使用的任何渲染器实现进行编译。例如,您不希望在Linux配置中包含DirectX代码。
在调试版本中,可能仍会进行虚函数调用,但在发布版本中,它们会被优化掉,因为您永远不会通过基类接口进行调用。它仅用于在编译时为渲染器类强制执行公共签名。
虽然这个方法确实需要一些预处理器,但它很小并且不会干扰代码的可读性,因为它是隔离的并且仅限于某些typedef和includes。一个缺点是您无法使用此方法在运行时切换渲染器实现,因为每个实现都将构建为单独的可执行文件。但是,无论如何都不需要在运行时切换配置。
答案 1 :(得分:0)
我将带有抽象基类的方法用于我的应用程序中的渲染设备。工作正常,让我动态选择在运行时使用的渲染器。 (如果不支持前者,我用它来从DirectX10切换到DirectX9,即在Windows XP上)。
我想指出虚函数调用不是成本性能的部分,而是涉及的参数类型的转换。要真正通用,渲染器的公共接口使用自己的一组参数类型,例如自定义IShader和自定义Matrix3D类型。在DirectX API中声明的类型对于应用程序的其余部分是不可见的,因为OpenGL将具有不同的Matrix类型和着色器接口。这样做的缺点是我必须将所有Matrix和Vector / Point类型从我的自定义类型转换为着色器在具体渲染设备实现中使用的类型。这远比虚拟函数调用的成本贵得多。
如果使用预处理器进行区分,则还需要映射不同的接口类型,如下所示。 DirectX10和DirectX11之间的许多相同,但DirectX和OpenGL之间不相同。
编辑:请参阅c++ Having multiple graphics options中的答案以获取示例实现。
答案 2 :(得分:0)
所以,我意识到这是一个老问题,但我无法抗拒。想要编写这样的代码只是试图应对面向对象灌输的副作用。
第一个问题是你是否真的需要换出渲染后端,或者只是认为它很酷。如果可以在给定平台的构建时确定适当的后端,那么问题就解决了:使用一个普通的非虚拟接口,并在构建时选择一个实现。
如果您发现确实需要将其交换出来,仍然使用非虚拟接口,只需将实现加载为共享库即可。通过这种交换,您可能希望将引擎呈现代码和一些性能密集型游戏特定呈现代码考虑在内并进行交换。这样,您可以使用通用的高级引擎呈现界面来完成主要由引擎完成的任务,同时仍然可以访问后端特定代码,以避免PMF提到的转换成本。
现在,应该说,在使用共享库进行交换时会引入间接,1。您可以轻松地将间接转换为&lt; 〜= =虚拟呼叫和2.高级间接是从不在任何实质性游戏/引擎中的性能问题。主要好处是保持死代码(卸载)并简化API和整体项目设计,提高可读性和理解力。
初学者通常不会意识到这一点,因为这些天有很多盲人OO推,但这种“OO优先,永远不问问题”的风格是不而没有成本。这种设计具有征税代码理解成本,并导致代码(比本例低得多)本身就很慢。面向对象当然有它的位置,但是(在游戏和其他性能密集型应用程序中)我发现的最好的设计方法是尽可能少地编写应用程序,只有当问题迫使你的手时才会丢失。随着您获得更多经验,您将对在哪里划线做出直觉。