Modular game engine: DLL circular dependencies
I want to create a game engine as a training & portfolio project and the modular approach sounds promising but I have some problems with the module design.
First I want to create low level modules like Rendering, Application, Utility etc. and use them in high level modules like Terrain. So the dependency would kinda look like this Game<-Engine<-Terrain<-Rendering.
I want to create multiple Rendering "sub modules" like Rendering.Direct3D11 and Rendering.OpenGL. That's where I would have circular dependencies. The sub modules would use interfaces of Rendering and Rendering would need to manage the sub modules, right? Game<-Engine<-Terrain<-Rendering<-->Rendering.Direct3D11
I could probably create a module like RenderingInterfaces and break the circular dependency but that seems like a hacky workaround. I was planning to use the "sub module design" multiple times like for: Game<-Engine<-Application<-->Application.Windows
Is the sub module design ugly? Is there a way to use the sub module design without circular dependencies?
You can solve this abstractly. Let's say you have three dylibs: Game.dll
, Renderer.dll
, SubRenderer.dll
.
The renderer interface might look like this (simplified):
// 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;
};
You can stick that in Renderer.h
or something like that, and the Renderer constructor and render
method can be implemented in Renderer.cpp
which you include for the project that outputs Renderer.dll
.
Now in SubRenderer.dll
, you might have a function like this:
// SubRenderer.h
class SubRenderer;
API SubRenderer* create_opengl_renderer();
That can be implemented in SubRenderer.cpp
which is compiled/linked to output `SubRenderer.dll. It might look like this:
// SubRenderer.cpp
#include "SubRenderer.h"
#include <Renderer.h>
class OpenGlRenderer: public SubRenderer
{
public:
virtual void render() override {...}
};
SubRenderer* create_opengl_renderer()
{
return new OpenGlRenderer;
}
Last but not least, in some source file in Game.dll, you can do something like this inside some 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;
}
... of course hopefully with a safer design that conforms to RAII.
With this kind of system, you have these header dependencies:
`Game.cpp->Renderer.h`
`Game.cpp->SubRenderer.h`
`SubRenderer.cpp->Renderer.h`
In terms of module dependencies:
`Game.dll->Renderer.dll`
`Game.dll->SubRenderer.dll`
And that's it -- no circular dependencies anywhere. Game.dll
depends on Renderer.dll
and SubRenderer.dll
, but Renderer.dll
and SubRenderer.dll
are completely independent of each other.
This works because this Renderer
can use a SubRenderer
given its virtual interface without knowing exactly what it is (thus requiring no dependencies to the concrete type of 'sub-renderer').
You can put Renderer.h
somewhere that is centrally accessible from all three projects with a common include path (ex: inside an SDK
directory). There is no need to duplicate it.
There shouldn't be any need for a reverse dependency in your design.
This is all about interfaces. Your rendering module need a native rendering API (sub-module, in your terms), but it shouldn't care if it is OpenGL or Direct3D11. The API sub-modules just have to expose a common API ; something like CreatePrimitiveFromResource()
, RenderPrimitive()
... These sub-modules shouldn't be aware of the upper layer, they just expose their common API.
In other words, the only "dependencies" needed is that the rendering module depends on a rendering sub-module (using the common interface), and the rendering sub-modules don't depend on anything (in your engine), they just expose a common interface.
Simple example :
We have a rendering module "IntRenderer" that renders integers. Its job is to convert integers to characters and print them. Now we want to have sub-modules "IntRenderer.Console" and "IntRenderer.Window", to print in a console or in a window.
With that, we define our interface : the sub-module must be a DLL that exports a function void print( const char * );
.
This whole description is our interface ; it describes a common public face that all our int renderers sub-modules must have. Programmatically, you could say that the interface is just the function definition, but that's just a matter of terminology.
Now each sub-module can implement the interface :
// IntRenderer.Console
DLLEXPORT void print( const char *str ) {
printf(str);
}
// IntRenderer.Window
DLLEXPORT void print( const char *str ) {
AddTextToMyWindow(str);
}
With that, the int renderer can just use import a sub-module, and use printf(myFormattedInt);
, regardless of the sub-module.
You can obviously define your interface as you want, with C++ polymorphism if you want.
Example : sub-modules X must be a DLL that exports a function CreateRenderer()
that returns a class that inherit the class Renderer
, and implements all its virtual functions.
下一篇: 模块化游戏引擎:DLL循环依赖