模块化游戏引擎:DLL 循环依赖项

Modular game engine: DLL circular dependencies

本文关键字:循环 依赖 DLL 游戏 引擎 模块化      更新时间:2023-10-16

我想创建一个游戏引擎作为培训和投资组合项目,模块化方法听起来很有前途,但我在模块设计方面遇到了一些问题。

首先,我想创建低级模块,如渲染、应用程序、实用程序等,并在高级模块(如地形(中使用它们。所以依赖关系有点像这个游戏<引擎><地形><渲染。>

我想创建多个渲染"子模块",如 Rendering.Direct3D11 和 Rendering.OpenGL。这就是我将具有循环依赖性的地方。子模块将使用渲染接口,渲染需要管理子模块,对吧?游戏<引擎><地形><渲染><-->渲染.Direct3D11

我可能会创建一个像 RenderingInterfaces 这样的模块并打破循环依赖关系,但这似乎是一个笨拙的解决方法。我计划多次使用"子模块设计",例如:Game<-Engine<-Application<-->Application.Windows

子模块设计难看吗?有没有办法在没有循环依赖的情况下使用子模块设计?

你可以抽象地解决这个问题。假设您有三个 dylib:Game.dllRenderer.dllSubRenderer.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或类似的东西中,并且 Renderer 构造函数和 render 方法可以用Renderer.cpp实现,您可以将其包含在输出Renderer.dll的项目中。

现在在 SubRenderer.dll 中,您可能有一个这样的函数:

// SubRenderer.h
class SubRenderer;
API SubRenderer* create_opengl_renderer();

这可以在编译/链接到输出"SubRenderer.dll的SubRenderer.cpp中实现。它可能看起来像这样:

// 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.dllSubRenderer.dll,但Renderer.dllSubRenderer.dll是完全独立的。

这是有效的,因为这个Renderer可以使用给定其虚拟接口的SubRenderer,而不知道它到底是什么(因此不需要依赖于具体类型的"子渲染器"(。

您可以将Renderer.h放在可从所有三个项目集中访问的位置,并具有公共包含路径(例如:在SDK目录中(。没有必要复制它。

在你的设计中不需要任何反向依赖关系。

这都是关于接口的。呈现模块需要一个本机呈现 API(用你的话来说,是子模块(,但它不应该关心它是 OpenGL 还是 Direct3D11。API 子模块只需要公开一个通用的 API;像CreatePrimitiveFromResource()RenderPrimitive()...这些子模块不应该知道上层,它们只是公开了它们的通用API。

换句话说,唯一需要的"依赖"是渲染模块依赖于渲染子模块(使用通用接口(,而渲染子模块不依赖于任何东西(在您的引擎中(,它们只是公开一个通用接口。


简单的例子:

我们有一个渲染模块"IntRenderer"来渲染整数。它的工作是将整数转换为字符并打印它们。现在我们希望有子模块"IntRenderer.Console"和"IntRenderer.Window",以便在控制台或窗口中打印。

有了这个,我们定义了我们的接口:子模块必须是导出函数void print( const char * );的 DLL。
整个描述就是我们的界面;它描述了我们所有 int 渲染器子模块必须具有的公共面孔。从编程方式上,你可以说接口只是函数定义,但这只是一个术语问题。

现在每个子模块都可以实现接口:

// IntRenderer.Console
DLLEXPORT void print( const char *str ) {
    printf(str);
}
// IntRenderer.Window
DLLEXPORT void print( const char *str ) {
    AddTextToMyWindow(str);
}

有了这个,int 渲染器可以只使用导入子模块,并使用 printf(myFormattedInt); ,而不管子模块如何。

显然,您可以根据需要定义接口,如果需要,可以使用C++多态性。
示例:子模块 X 必须是导出函数CreateRenderer()返回继承类Renderer的类的 DLL,并实现其所有虚函数。