I'm dabbling in writing my own somewhat basic game engine from scratch, and I wanted to ask about some common design configurations for connecting the various parts. I'm particularly interested in the relationship between the "engine" and the "game", and the various ways in which information and function calls cross this boundary (the "control flow", for lack of a better term).
Let me start by describing my understanding of the main parts:
Engine: The engine contains various subsystems related to rendering graphics and audio, inputs, physics solving, networking, memory management and asset/resource loading/unloading, etc. Subsystems may be encapsulated into individual classes or use interfaces in order to support a variety of platforms or libraries.
Game: The game contains a large and complex structure of "entities" which contain all of the data and functions that define the gameplay world and logic. Entities are also associated with visual representations, audio sources, physics bodies, and everything else that determines what the game is and how it is represented.
So, for example, this is my current understanding of 'control flow' when it comes to updating or 'ticking' gameplay logic (pseudocode ahead):
Setup:
Main()
creates and initializes the Engine, which creates a context and initializes various subsystems. (For example, maybe setting up SDL/OpenGL/OpenAL/etc., allocated a big block of memory within the memory manager, etc.)Some kind of game loop will call an
Engine::Update()
, either at a fixed or variable timestep, maybe passing in some kind of 'deltatime'.The Engine contains a object/pointer/reference to a Game, and (if there is a game) will then forward its own updates to the game's
Game::Update()
function.The Game will then call
Entity::Update()
on each of its own Entities, and they will perform some game-specific, per-frame, actions.
This is pretty simple to understand, as it is a straight-forward chain of Update()
calls starting from the main loop, then the engine, then the game, and finally propagating all the way down some structure of entities (and/or components) until everything in the entire system has performed it's per-frame logic. This is a 100% linear relationship, and as such each piece only needs access (pointer/reference) to the next piece in the chain.
Is this a mostly correct understanding of how logic flows between pieces of a game? Are there other common ways of handling this (for example, Main updates Game directly and Engine is used as a service by Game for other things?)
Where things seem to get more complicated and where I get more confused are situations where it seems like everything isn't 'flowing in one direction', as it happens with the logic flow described above. For example, here's my understanding of the "rendering flow".
Setup/Initialization, same as before.
Game loop calls
Engine::Render()
at some timestep, possibly in sync with the Update() but not necessarily.At this point, the Engine could call
Game::Render()
and this call could be passed down to individual Entities that are responsible for rendering themselves. However, then this seems to create a problem where rendering-specific code is now seeping into the "Game Space", and your game's code is getting all mixed up with D3D or OpenGL calls! I don't think that's what we want, Right? As such, my instinct here would be to have the Engine request all of the "renderable" Entities from the Game (for example,Game::GetRenderables()
fucntion).After the Engine has pulled all of the "renderable" Entities from the Game, they are then passed into the the
IRenderSystem::Render( IRenderables )
.
At least, that's my current understanding of how the control flow works when it comes to things like graphics rendering, audio rendering, physics solving, etc. In these situations it seems desirable to encapsulate these tasks within their respective engine subsystems, so that as much of the related code as possible exists in a single place. Of course the Game world and Entities determine the content of the game, including what needs to be seen, heard, collide, where it is, what it looks like, etc. But it should be the Engine that is responsible for actually taking those things from the Game and rendering/solving/processing them.
Is this a decent high-level understanding of the rendering flow between the Engine and the Game? Is this general design suitable for audio and physics systems as well (Engine collects 'primitives' from Game and delegates processing to subsystems)?
Finally, in my design, where the "Engine has a Game" as a "Console has a Cartridge"; updating the game's logic and collecting of all of its renderable/physics primitives each frame. I've run into a situations where the Game depends on the Engine for certain tasks. For example, the creation and destruction of certain Entites/Components/Assets!
It makes sense to me that the Engine should be responsible for allocation and deallocation of system resources (like memory) as well as loading assets from files. As such, in order for the Game to add a Sprite or Mesh, for example, to an Entity, it must be able to request the creation of a Mesh component (or something similar) from the Engine. The Engine will then create that object using a custom allocator, load the asset from a file, do any other preparation, and then deliver the result to the Game.
But now this seems to complicate the relationship between the Engine and the Game. It used to be that the Engine 'owns the Game', updates it, and retrieves primitives from it, while the Game had no knowledge or dependency on the Engine. But now the Game needs to be able to call functions on the Engine as well. Now there are a few ways to resolve this; making the engine a singleton, creating back-pointers to the engine, etc. But I was thinking about a solution where the Engine passes a reference to some kind of EngineUtilities object into the Game and its Entities that gives them some kind of limited interface for calling Engine functions. Kind of an attempt at dependency injection, I guess.
Anyway, I realize how big this question is, but the gist of it is this: What are some common designs for connecting the various parts of a game engine and what are the pros and cons of different approaches? How does the flow of logic updates differ from the rendering flow, physics solving, object creation, etc.? Which approaches are used by popular game engines (Unreal, Unity, IdTech, CryEngine, Source, Godot, etc.), and have popular approaches changed over time?