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

Modular game engine: DLL circular dependencies

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

首先,我想创建低级模块,如渲染、应用程序、实用程序等,然后在 Terrain 等高级模块中使用它们。 所以依赖关系看起来像这样 Game<-Engine<-Terrain<-Rendering.

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

我可能会创建一个像 RenderingInterfaces 这样的模块并打破循环依赖,但这似乎是一个棘手的解决方法。我打算多次使用 "sub module design" ,例如: 游戏<-引擎<-应用程序<-->Application.Windows

子模块设计丑吗?有没有办法使用没有循环依赖的子模块设计?

您的设计中应该不需要反向依赖。

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

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


简单的例子:

我们有一个呈现整数的呈现模块"IntRenderer"。它的工作是将整数转换为字符并打印出来。现在我们想要子模块 "IntRenderer.Console" 和 "IntRenderer.Window",以在控制台或 window.

中打印

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

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

// 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() 的 DLL,该函数 returns 和 class 继承 class Renderer,并实现其所有虚拟函数。

你可以抽象地解决这个问题。假设您有三个动态库: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 中实现,您将其包含在输出 [= 的项目中17=].

现在 SubRenderer.dll,你可能有这样的功能:

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

可以在SubRenderer.cpp中实现,即compiled/linked输出`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的更安全的设计。

对于这种系统,您有这些 header 依赖项:

`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 而无需确切知道它是什么(因此不需要依赖于 'sub-renderer' 的具体类型)。

您可以将 Renderer.h 放在可以从所有三个项目集中访问的位置,这些项目具有共同的包含路径(例如:在 SDK 目录中)。无需复制。