Obviously writing a full game engine is a big endeavour and there are some design decisions that should be made up-front. Of course, you can change these decisions later, but most of them mean a very large refactoring effort. The decisions outlined here are based on my experience refactoring my own engine many, many (many) times.
I will add a caveat here: all of these design decisions are generally adhered to. However, if there’s one thing I’ve learned over the years of iterating on this code base, it’s that you will not be able to adhere to all of your standards 100% of the time. The Entity-Component-System model and favouring composition is great… until it doesn’t work. We’ll never make function calls between systems… unless it’s for debug data. So consider the below a list of general guidelines, not perfect standards.
We want to aim for minimal coupling between the modules of our engine. The physics system should never need to call the rendering system (except maybe for debug data), for example. This naturally leads us to events and message systems for interaction between systems, so this will be our preference. As a general rule, we should avoid including files from modules other than the Core and IO modules. If you find yourself including a file from the Render module in the Physics module, stop and think about how we could replace that with a message instead.
A thought should always be kept towards networking and multi-player first. For example, data serialization should be at the forethought - sending string data like JSON or XML is much less preferable than sending a binary format. This will contrast with our scripting integration, where strings and names are king. Integer identifiers should also be preferred (or used in addition to) for things that may be serialized. More on this in the Networking section.
Most things in our engine should be accessible in the scripting language. Since we also want to reduce coupling, we’ll need to find an easy way to make functions and objects accessible. The first time I built the engine with scripting included, I duplicated all of my scripted function calls in minimal wrapper functions inside the Scripting module. Obviously that did not make scripting “1st class”, so I moved to a model where each class also defined its own scripting interface and function. However, this did not solve the code duplication and required coupling everything to the scripting module. In this version of the engine, you’ll see we make liberal use of metadata to dynamically generate those same wrapper functions; this concept is borrowed largely from the Unreal Engine’s amazing meta and reflection systems.Next