
There is no other way to put it: The architecture of this engine is a mess. The graphics engine relies on the asset manager to load in shaders, and it relies on components from the game engine to render meshes. The Asset Manager relies on the graphics engine to load meshes, and the game engine to load assets like NavMeshes. And of course the game engine relies on both of these to do anything with assets or rendering.
Circular dependencies such as these make a program harder to understand and more difficult to debug, which is why I’ll do my best to cut most of these dependencies out, starting with…
The Graphics engine
Currently all render commands that render meshes or send object-specific data to the GPU does this by taking in a related Component – a type defined in the Game Engine module – such as a Model-component or Camera-component. This creates a hard dependency between the Graphics Engine and the Game Engine, and since the Game Engine needs to depend on the Graphics Engine to render stuff we can’t have the Graphics Engine depending on the Game Engine as well.
Having the render commands take in a Component lets them access any data related to that game object like transform-matrices, meshes or materials. But we can just send this data directly to the command instead to fix this issue. Meshes and Materials are types defined in the Graphics Engine, and our Matrix-class has been moved to an external Math-module instead of being defined in the Game Engine.
The Graphics Engine also relies on the Asset Manager to load in all the default shaders and pipeline state objects that the engine needs to function normally. This means that if these files are missing or for whatever reason the Asset Manager fails to load them, the graphics engine will not work! We can’t have that happening, so any mandatory graphics objects should be created in the Graphics Engine. Neither should anything in the graphics engine use the Asset Manager to fetch assets; If the Graphics Engine needs to operate on an assets content, like drawing a mesh, it should be passed to it from another source (Usually the Game Engine in this case).
The asset manager
When it comes to the Asset Manager I ended up doing more than just isolating it from the other modules. Previously the Asset Manager would iterate through the Assets-folder and register/load all supported files, but I decided to split asset registration and asset loading into different functions, so that all assets are still being registered (Saving the path to a collection to be accessed later) but won’t be loaded until the asset is asked for.
This has the positive side effect of removing any loading issues where one asset depends on another, like for example a material asset needing a texture asset to already be loaded. Assets also get unloaded when they are no longer in use to save memory.
Previously all asset types were built into the Asset Manager, so when it goes to load an asset it will check the file extension and then call the respective member function that handles the loading of that asset. By moving the asset-loading function into each corresponding Asset Type-class the Asset Manager no longer needs to know about any Asset Types or how to load them, instead letting the Game Engine or the Application register a lambda-function handling the loading of the unknown type and connecting it to a file extension. This moves the dependency on any classes from the Game Engine or Graphics Engine from the Asset Manager to wherever you register the Asset Type, which is fine.




One detail I neglected to mention due to thinking it wasn’t big enough to deserve it’s own segment, was the Utilities-module. Before this any Utility code I had was baked into the Game Engine module, including Math-classes like vectors and matrices. Those are now in their own Utilities-library so that every module can use them without creating any weird dependency issues.
So this is what our project dependency diagram ended up looking like after our changes. Much better! We have a clean one-directional flow of dependencies instead of the tangled cord situation we had before. While I will still have to be mindful of how I add new features to the engine, this foundational overhaul has made it a lot easier to do so, and I believe it will save me much time and headache.
For the next devlog I’ll be diving deeper into profiling the runtime and optimizing parts that take up a lot of the frametime.