I started my own route in 2009 when I started at university first in C# using an OpenGL wrapper and later changed to C++. I had a lot of try and throw away prototypes up to my current stable version and also tried the same ammount of project architecture. Having a look at certain features of other engines like Unreal Engine 4, Urho3D, Cry Engine and anything I found on the web is a good approach when you are at a state where you have basic knowledge of what you do and especially why you do something.
First and most important point that I think when looking back today is your general project structure. This sounds redundant but as your project grows you will end up with a mess of code and script/ config files quickly if you don't take care about that early. As I wrote in this post, I'm currently on the go to analyse the dependency structure when building and print those diagrams for project analysis in order to keep the project clean.
Second thing that pointed out in past years is your building pipeline. There is nothing more annoyng than a bunch of plain script files and/or the need to maintain those or call some magic make/nmake every time you want to setup or compile your code. This is why I started to dissect Unreal's build tool and 'm now on the go to write my own one. I figured out that there are little information you need to setup project files for different Visual Studio, Visual Studio Code and whatever IDE you prefer while most of those informations like platform settings are always the same for the whole project. I'm also now on the road to quit the use of config files for my build completely after reading this article about automated build systems.
My build tool currently performs a complete project folder analysis using certain hard coded rules like
-
Every subfolder in my project root is an own project
-
For every project found in root collect all files and decide what kind of project it is
-
Every project in root can contain multiple projects (mixing C# and C++ files result in two project files on the same project)
-
Use code analysis to find relations between projects in the same root
So what should I say, it works suprisingly well :D
Another clue about my build tool is a Javascript kind of mechanic, mixins. I use the same method Unreal does using .Build.cs files all over my project root and sub folders to control my build tool's behavior. This is what Gigi Sayfan did with his Python scripts. But no, those aren't "just another build file" you need to maintain, those are micro plugins. My program detects all of them and loads them into the runtime to add/extend or override single functions that stay in specially marked classes; just as Javascript mixins work.
I also have the chance to provide plugins but they are limited to predefined interfaces to provide build profiles (local build, remote build, cloud build ...), build systems (C#, C++ and whatever or even different platform exports) and I use C# for any of my tools because it pointed out that C# is a good language for rapid tooling and tool prototyping.
The last important point in my opinion is modularity. I seperated my engine into several modules each maintaining it's own sub-project. This has the reason that I wanted my design to be flexible to the needs of the game it is used for and one could easily work in parallel on different features. This comes with nearly no overhead in C++ because of static linking. Any module I have is a static library.
Another advantage of the modular approach is also the project setup. I have a package manager tool that is able to download features and their dependencies from GitHub for example (and also may include additional .Build.cs files if needed)
Now I want to tell you something about how I started and what my modules contain, you may take or don't take this as a suggestion for a roadmap.
I first started with my current implementation (after several reworks since 2009) to think about basics. New and delete wasted my early code and also leaded to heavy memory leaks at some of past professional prohjects I participated to so I found and read this blog post about memory management and implemented a similar approach including my own STL like container structures for better control over memory consumption. It pointed out that a lot of classes could just inherit from my Array class (dynamic resizeable continious memory management class that behaves like a plain C array but could dynamically resize to certain element count) like Vector (as I come from the C# world, some more convinience was needed like the C# List<T> equivalent), Queue, Stack, hashTable and Map for example are such classes.
I force any function that operates on blocks of memory to have the Allocator& parameter pointing to the default allocator set once at startup. Driving this route I never had hidden memory leaks again in my code because the Allocator instantly complained about unreleased memory.
Then I added some file and asset management because this is the most used topic in every modern game and every modern engine. Assets are always present and have to be loaded, cached and cleaned frequently while playing. Anything working with data in my engine uses streams because they provide some features like the position pointer that benefit in my opinion against plain arrays of data. One of those features for example is caching (in case of files) or the position pointer (very usefull on parsing/processing data).
Memory Mapped Files also turned out to be a basic technology you won't want to miss inc ase of threaded asset loading.
RTTI/ Reflection and dynamic calling was a research topic just for interest but I don't want to renounce nowdays. Driving my own implementation tooks some research but especially in case of serialization and editor to engine code it is very usefull. I also added a huge delegate and dynamic calling feature into my engine that is used for example in the in-game console to parse and call methods in game code. Another Javascript/ Typescript mechanic I used here for my editor interface are dynamic interfaces defining an interface class and convert a matching object to that interface even if it dosent belong to the inheritance chain is quite usefull to bind functionality to editor components and also enables hot reloading.
I havent used it that much but you could also write a wrapper struct that behaves like an C# object, assigning any kind of class/ struct and convert it back to whatever it was when needed.
Last but not least ECS. It is an overused buzzword these days that is true but it is also usefull and you again don't want to miss it once tasted. The principles of ECS are to provide objects that define their existence of components that could interact with static business code in systems. So far one could say that ECS is the most flexible way to define data that is used in certain way to reach an expected result.
Using systems for rendering, UI and gameplay elements is an easy alternative to OOP because you know the data to expect and could write logic once on a centrally place instead of spreading the logic all over classes and class specializations. This also prevents the so called god object to occure.
ECS is for example used in the render graph, a feature that decides of those objects to render, performs object parent/ child relation chaining and transformation updates. In Unity or Unreal, a scene contains of a render graph.
This are just my thoughts so feel free to take whatever you need