模块化游戏引擎:DLL循环依赖
我想创建一个游戏引擎作为培训和组合项目,模块化方法听起来很有前途,但我在模块设计方面存在一些问题。
首先,我想创建低级模块,如渲染,应用程序,实用程序等,并将其用于高级模块(如Terrain)。 所以依赖会看起来像这个游戏<-Engine <-Terrain <-Rendering。
我想要创建像Rendering.Direct3D11和Rendering.OpenGL这样的多个渲染“子模块”。 这就是我会有循环依赖的地方。 子模块会使用Rendering和Rendering的接口来管理子模块,对吧? 游戏< - 发动机<-Terrain <-Rendering < - > Rendering.Direct3D11
我大概可以创建一个像RenderingInterfaces这样的模块,并打破循环依赖,但这看起来像是一个拙劣的解决方法。 我打算多次使用“子模块设计”,比如:Game <-Engine <-Application < - > Application.Windows
子模块设计是否丑陋? 有没有办法使用没有循环依赖的子模块设计?
你可以抽象地解决这个问题。 假设您有三个dylib: Game.dll
, Renderer.dll
, SubRenderer.dll
。
渲染器界面可能如下所示(简化):
// Renderer.h
class SubRenderer
{
public:
virtual ~SubRenderer() {}
virtual void render() = 0;
};
class API Renderer
{
public:
explicit Renderer(SubRenderer* sub_renderer);
void render();
private:
SubRenderer* sub_renderer;
};
你能坚持,在Renderer.h
或类似的东西,和渲染器的构造和render
方法可以实现Renderer.cpp
您包括输出项目Renderer.dll
。
现在在SubRenderer.dll
,你可能有这样的功能:
// SubRenderer.h
class SubRenderer;
API SubRenderer* create_opengl_renderer();
这可以在SubRenderer.cpp
中实现,它被编译/链接以输出SubRenderer.dll。 它可能看起来像这样:
// SubRenderer.cpp
#include "SubRenderer.h"
#include <Renderer.h>
class OpenGlRenderer: public SubRenderer
{
public:
virtual void render() override {...}
};
SubRenderer* create_opengl_renderer()
{
return new OpenGlRenderer;
}
最后但并非最不重要的一点,在Game.dll的某个源文件中,你可以在Game.cpp
做这样的事情:
// Game.cpp
#include <Renderer.h>
#include <SubRenderer.h>
int main()
{
SubRenderer* opengl_renderer = create_opengl_renderer();
Renderer renderer(opengl_renderer);
renderer.render(); // render a frame
...
delete opengl_renderer;
}
......当然希望采用符合RAII的更安全的设计。
有了这种系统,你有这些头依赖关系:
`Game.cpp->Renderer.h`
`Game.cpp->SubRenderer.h`
`SubRenderer.cpp->Renderer.h`
就模块依赖性而言:
`Game.dll->Renderer.dll`
`Game.dll->SubRenderer.dll`
就是这样 - 在任何地方都没有循环依赖。 Game.dll
依赖于Renderer.dll
和SubRenderer.dll
,但Renderer.dll
和SubRenderer.dll
完全相互独立。
这是有效的,因为这个Renderer
可以使用一个SubRenderer
给予它的虚拟接口,而不知道它是什么(因此不需要依赖于'sub-renderer'的具体类型)。
你可以把Renderer.h
放在一个可以从所有三个项目集中访问的地方,并且有一个公共的包含路径(例如:在SDK
目录中)。 没有必要重复它。
在你的设计中不应该有任何需要逆向依赖的东西。
这是关于接口的。 你的渲染模块需要一个本地渲染API(根据你的条件,子模块),但它不应该在意它是OpenGL还是Direct3D11。 API子模块只需公开一个公共API; 像CreatePrimitiveFromResource()
, RenderPrimitive()
...这些子模块不应该知道上层,他们只是公开他们的通用API。
换句话说,唯一需要的“依赖”是渲染模块依赖于渲染子模块(使用通用接口),渲染子模块不依赖于任何东西(在引擎中),它们只是暴露一个通用的界面。
简单的例子:
我们有一个渲染整数的渲染模块“IntRenderer”。 它的工作是将整数转换成字符并打印出来。 现在我们想要在控制台或窗口中打印子模块“IntRenderer.Console”和“IntRenderer.Window”。
因此,我们定义了我们的接口:子模块必须是一个导出函数的DLL void print( const char * );
。
这整个描述是我们的接口; 它描述了我们所有int呈现器子模块必须具有的普通公众的面孔。 在编程上,你可以说接口只是函数定义,但这只是术语的问题。
现在每个子模块都可以实现这个接口:
// IntRenderer.Console
DLLEXPORT void print( const char *str ) {
printf(str);
}
// IntRenderer.Window
DLLEXPORT void print( const char *str ) {
AddTextToMyWindow(str);
}
这样,int渲染器就可以使用import一个子模块,并使用printf(myFormattedInt);
,而不管子模块。
你可以很明显地根据你的需要定义你的接口,如果你想用C ++多态。
示例:子模块X必须是导出函数CreateRenderer()
的DLL,该函数返回继承类Renderer
的类并实现其所有虚函数。