I am trying to redesign a home-grown hobby engine. It became apparent that it was far too tightly coupled and so I'm trying to redesign it in a more modular way. Unfortunately, I'm baffled by the practicalities of implementing some of the advice I've been reading, specifically by how little knowledge the rendering module is "supposed" to have of the rest of the solution. In my existing tightly coupled design I have a triangle of sorts between assets, instances and the renderer, and every time I try to break a link in it I feel I am losing something important. How do I break the following couplings without losing functionality or creating greater problems for myself?.
A) Between assets and the rendering module. Supposedly, the rendering module shouldn't have any knowledge of assets (or just about anything else). This is something I can accept in principle, but any attempt I make in practice, e.g. to feed geometry to the renderer, seems to follow one of two paths:
- Creating a file format- and graphic API- agnostic intermediate representation. This has the disadvantage that I must open a file, load it's contents into memory, convert them into the intermediate representation and then convert that into a vertex buffer. This seems wasteful on the face of it, but is even worse if the file format and graphic API happen to have complementary representations - the modularity would mean I cannot take advantage of this.
- Construct the vertex buffer outside of the rendering module and pass that in directly, trading one module's reliance on the existence of a particular interface for another module's reliance on, and detailed knowledge of, a particular implementation. This feels like madness to me.
B) Between instances and assets. This is basically to keep unnecessary state changes to a minimum. In my tightly coupled design there is a bidirectional link between instances and assets. Not every scene will make use of every asset, but for any given scene I know what instances are present. Whenever a specific "dirty" flag is set I run through the instances, make a list of required assets and make sure I don't load resources or bind buffers I don't need. Similarly, I don't want to be binding and unbinding buffers to draw a cat, then a dog, then another cat. This time I loop through the assets, then the instances. If I break the link in either direction I lose something. If I refactor the link out into a separate class, it feels like a lie to say I've decoupled them - I now have a third class that is every bit as dependent on two others as these would have been on each other.
C) Between instances and the rendering module. Connected to the point above. Instances contain data that the rendering module needs to render the geometry in the right place, in the right animation state, etc. If I can't pass my instances (or scene object) into the renderer then I have to supply the required information in some implementation agnostic format one instance at a time, setPosition, setRotation, etc. But as I mentioned in B) I want to be drawing all of my cat instances at once, before I start drawing the dogs. To make this work I have to either:
Buffer the instance data inside the rendering module somehow and then sort the resulting list. Of course, the logical way to store this information would be in a structure of some kind... except... that structure looks suspiciously (exactly) like the instance I (for some reason) couldn't just pass to my renderer. I now have two essentially identical instance classes on either side of a module divide - this can't be what decoupling is all about, surely?. Not to mention whatever the sort might cost (and the games I'm most interested in making are fairly deep simulations, very heavy on the CPU already).
Sort by asset outside of the renderer. Sure, this seems easy enough. But this is an optimisation that depends upon the implementation within the rendering module itself. This can't be what decoupling means either. If the renderer (about which I am to make no assumptions) happens to be a fall-back, immediate-mode OpenGL renderer intended for older hardware, then this might actually cost me more than it saves.
So this is where I am. I want to be clear, I am not doubting the advice, I am just at a complete loss to come up with a practical (and sane) implementation. A message/event system seems like a good fit for most communication between other modules, and I've used similar patterns to produce modular solutions in my real-work programming (physics analysis). But the sheer volume and flow of data needed by the renderer would seem to require something else. Which links would you break? And how would you claw back the lost functionality? Are there good "third options" to issues A and C?