Sign in to follow this  

Problems With Game Engine Architecture

This topic is 398 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

As the title says I'm having a lot of problems with the architecture of my game engine.

Every time I write a game engine all different components (Graphics, Audio, Input) get really messy.

Input needs to have access to Graphics in case for example the player shoots, and the Graphics might need access to Audio because clicking a button should play a audio file, and so on.

 

I can't find a good way to do this without everything looking like spaghetti.

 

I've considered these things, but I don't know if any of them are actually good ways to do it : 

 

  • Making every single component of the engine global, and putting them in a globals.h.
  • Event/Messeging system (this one seems pretty good but I can't wrap my head around on how to implement it)
  • Declaring pointers to components in each class that needs it.
  • Passing the components to each function that uses it (I dislike this one since if a function needs several of the components it looks really messy)

 

Basically I want my engine parts (Graphics, Audio, Input, GUI, MapManagers, Debugging Classes and so on) to be able to access eachother without having to create a sphagetti mess everywhere.

Share this post


Link to post
Share on other sites

Maybe a bit to obvious, but there's good book on this calles exactly "Game engine architecture". It's a good guideline for exactly what you're doing (currently I'm at chapter 4 :))

Edited by cozzie

Share this post


Link to post
Share on other sites

 

Input needs to have access to Graphics in case for example the player shoots, and the Graphics might need access to Audio because clicking a button should play a audio file, and so on.


My suggestion to handle this is to extract the "game logic" into its own systems and have it communicate with the other systems. "Platform-level" components like input, graphics, and audio should never need to know about one another. Instead, try this:
- when the player shoots, input notifies the game system that the player has shot
- the game system takes care of telling the graphics system to spawn particles (the graphics system shouldn't know what "bullets" are - only things related to rendering)
- UI should not be in the graphics layer - UI should be in its own system that takes input and tells the graphics and audio layers what to do
 
As much as possible, you want dependencies to flow in one direction. Either platform components should not need to know about gameplay components at all, or gameplay components should not need to know about platform components at all. Platform components definitely shouldn't need to know about each other.

Passing the components to each function that uses it (I dislike this one since if a function needs several of the components it looks really messy)


This is my preferred option, most of the time. It may look messy, but it also lets you see exactly which function of each component depends on which other component. That's something that can come in handy when refactoring. If your dependencies are explicit, and they look messy, that's probably because they are messy. If your dependencies are implicit, they might look less messy, but that doesn't take the mess away.

 

 

I checked out the source on CryEngine, they seem to use static objects for all their managers, considering they're using it I assume it's not that bad?

I really, really dislike the looks of sending them in to every function though. I'll probably only do that if I have no other options tbh.

 

 

Maybe a bit to obvious, but there's good book on this calles exactly "Game engine architecture". It's a good guideline for exactly what you're doing (currently I'm at chapter 4 :))

 

I'll check that out. :)

Share this post


Link to post
Share on other sites

Input should never have access to graphics. This doesn't make much sense to be honest.

This is where you come to abstraction layers. Abstraction layers will prevent things from spiraling down into insanity as long as you keep abstractions going in one direction.

 

For example

Input -> Logic -> World -> Representation.

Your input should only influence the logic. Where the logic influences the world. And the world influences the representation. To make things simple.

Your world consists of models and sounds. And your representation consists of how things needs to be rendered.

Between those, you'll have data that is goes back and forth between one another. Or goes in just one single direction.

Share this post


Link to post
Share on other sites

A few things I do:

 

- I use globals for all my managers.  There are a few, but I also cluster some of the sub managers into a manager for that area. EG - Special effects manager, has explosions, particles, etc...all the fluff.  I do have a static and dynamic entity manager which should be managed together also. BUT 

- General rules you make for yourself.  Mine, don't call a manager from an entity in another manager.  That's my rule, so it means that the entity has to have strong status controls in it.  For example, if my bullet hits something, I set a status on what it hit (material).  My manager orchestrates call from the top level.  it means I dont have to follow into entities all the extra game logic.

- If you think something is complicated in your mind, then comment it in your code.  You come back 2 weeks later and go WTF.

- Don't be afraid to start with creating too many managers (yes, its bad practice though), you will soon realise patterns in your code and refactor.  Experience will play a part.

 

As mentioned before, major game engines have some pretty bad code in them.  Read Unreal has some epic classes in its engine, making it hard to pick up and learn.

 

And reading books is good also, its old fashioned, but works 

Share this post


Link to post
Share on other sites

Input needs to have access to Graphics in case for example the player shoots, and the Graphics might need access to Audio because clicking a button should play a audio file, and so on.

 

What if I want to make an ASCII-based game that has no audio?  What if I want to create a networked game where an input also needs to be sent across the internet?

 

Write the engine as engine, keeping the components completely separate from each other.  Then write another layer, your game, on top of those that connect things together.

Share this post


Link to post
Share on other sites

- I use globals for all my managers.  

 

Heheh. I remember when I made that exact statement about my game project almost 5 years ago. I also remember 6 months ago when I suddenly realised why it was a bad idea and spent a few months refactoring everything to remove all globals.

 

Globals are seductive because they are easy to use and let you share information between different objects very easily. But you eventually end up in dependency hell, where to test a small system you need to recreate the entire game state, since object A references systems B and C, and they rely on D and E and F.. etc

 

The best thing about the mindset of passing dependencies instead of referencing them through globals, is that poor design stands out like a sore thumb.

Share this post


Link to post
Share on other sites

 

- I use globals for all my managers.  

 

Heheh. I remember when I made that exact statement about my game project almost 5 years ago. I also remember 6 months ago when I suddenly realised why it was a bad idea and spent a few months refactoring everything to remove all globals.

 

Globals are seductive because they are easy to use and let you share information between different objects very easily. But you eventually end up in dependency hell, where to test a small system you need to recreate the entire game state, since object A references systems B and C, and they rely on D and E and F.. etc

 

The best thing about the mindset of passing dependencies instead of referencing them through globals, is that poor design stands out like a sore thumb.

 

I do have globals, but I dont sprinkle them much throughout my code.  The point is to use the conservatively.  At the end of the day, my managers only accessed through say a core render functions.  I enforce that game entities themselves don't jump in and out of Globals.  Creates spaghetti code.  Its just down to discipline.

Share this post


Link to post
Share on other sites

 

I checked out the source on CryEngine, they seem to use static objects for all their managers, considering they're using it I assume it's not that bad?


What other engines do is irrelevant to you. Just because a large engine does things one way, or that way is common, does not mean that it is the best way to do it, or even a not bad way to do things. You might be surprised at some of the atrociously ugly and unmaintainable code (especially legacy code!) in major engines. Just because an approach is common does not mean it's the best approach for your own circumstances.

You should do what is best for your code. I am suggesting that making your dependencies explicit and avoiding global state is what is best for your code.
 

I really, really dislike the looks of sending them in to every function though. I'll probably only do that if I have no other options tbh.


It doesn't matter whether you like the way it looks. What matters is that your code is maintainable and easy to reason about. You're shooting yourself in the foot otherwise.

You should write your code in such a way that bad code looks bad. If you're passing too many dependencies around, that's a sign that your code probably needs to be rethought. Implicitly passing those dependencies around via globals/singletons makes that fact a lot less obvious and can allow dependency spaghetti to grow.

 

 

But how do I know whats bets for my code though?

I always end up asking somewhere what I should be using.

Share this post


Link to post
Share on other sites

You need to practice using object oriented design.
 

Input needs to have access to Graphics in case for example the player shoots, and the Graphics might need access to Audio because clicking a button should play a audio file, and so on.

"Input" / "Graphics" / "Audio" sound way too large to be OO classes - they sound like full modules / collections of many classes. See ISP/SRP.
 
"Input" / "Graphics" / "Audio" should not depend on each other at all -- they should all be completely segregated.
In the example of a GUI button, let's say:
Button an OnClick member function.
The input module knows how to call a std::function in response to a mouse-click, but doesn't care what that std::function does in turn.
In this case, you can register a std::function that will call the OnClick member function of the Button.
Button can have a renderable mesh object, which is part of the Graphics layer. The renderable mesh doesn't know anything about clicks or events; it only knows how to draw triangles to the screen. The button acts like a controller, telling the renderable mesh what to do and when -- but the renderable mesh has no knowledge that it's part of a GUI.
Likewise the button can have an audio source, which it controls at the right times.
 
This gives you a two-layer code base.
The bottom layer contains three completely independent modules (Graphics/Input/Audio) that do not connect to each other whatsover.
The next layer contains the GUI module, which is aware of the Graphics/Input/Audio modules in a one-way relationship (it knows about them, but they don't know about it).
 
Code-bases should always be made up of layers of modules, with one-way relationships propagating down through the layers. When dealing with large modules, you should often also pay attention to the DIP.

For an example that even when two modules are being used together to build a bigger component, they can still have no direct relationship with each other, let's say that an AudioSource has an event that occurs when it finishes playing, and we want to disable a RenderableMesh when this occurs.
Higher level components, such as A GUI class here, often act as 'glue' that assembles several lower level components together, creating the dependencies between them.

struct AudioSource
{
...
  std::function<void()> onFinished;
  void OnFinished() { onFinished(); }
  void Play();
};
struct RenderableMesh
{
...
  void Hide();
  void Show();
};
struct GuiButton
{
  GuiButton()
  {
    audio.onFinished = [this]()
    {
      this->mesh.Hide();
    };
    mesh.Show();
    audio.Play();
    //when audio finishes, the mesh will be hidden
  }
  AudioSource audio;
  RenderableMesh mesh;
}

There you've got an interaction between Audio/Graphics, but neither of them are directly aware of the existence of the other. A higher level component has injected this dependency into them.

 

As well as doing this with std::functions, you can also do it with interfaces/virtual functions, templates, C function pointers, etc. The concept transcends the mechanism.

Share this post


Link to post
Share on other sites

Another example : I have a class called "Debug", it currently just creates error messages, warning messages etc in the console.

I want all of my engine, including the game itself to be able to display warnings, logs and errors.

 

Do I pass the Debug object into every single function of my entire engine? Like this : 

CreateWindow(uint width, uint height, uint x_pos, uint y_pos, std::string title, Debug debugger);

Or do I use return codes for everything and then check which error it is inside of the games initialization function? Like this : 

int code = InitEngine(params);

switch (code)
{
    -1:
    debug.PrintError("Some error message goes here");
    break;

    0:
    debug.Print("Engine Initialization Successful!");
    break;
}
Edited by BiiXteR

Share this post


Link to post
Share on other sites

Another example : I have a class called "Debug", it currently just creates error messages, warning messages etc in the console.
I want all of my engine, including the game itself to be able to display warnings, logs and errors.


If every module might need to make use of the debug module, then this is one case where a global would be acceptable. But there should be very few modules that really, truly need to be global as logging and debugging features are. Edited by Oberon_Command

Share this post


Link to post
Share on other sites

 

Another example : I have a class called "Debug", it currently just creates error messages, warning messages etc in the console.
I want all of my engine, including the game itself to be able to display warnings, logs and errors.


If every module might need to make use of the debug module, then this is one case where a global would be acceptable. But there should be very few modules that really, truly need to be global as logging and debugging features are.

 

 

Alright.

 

 

You need to practice using object oriented design.
 

Input needs to have access to Graphics in case for example the player shoots, and the Graphics might need access to Audio because clicking a button should play a audio file, and so on.

"Input" / "Graphics" / "Audio" sound way too large to be OO classes - they sound like full modules / collections of many classes. See ISP/SRP.
 
"Input" / "Graphics" / "Audio" should not depend on each other at all -- they should all be completely segregated.
In the example of a GUI button, let's say:
Button an OnClick member function.
The input module knows how to call a std::function in response to a mouse-click, but doesn't care what that std::function does in turn.
In this case, you can register a std::function that will call the OnClick member function of the Button.
Button can have a renderable mesh object, which is part of the Graphics layer. The renderable mesh doesn't know anything about clicks or events; it only knows how to draw triangles to the screen. The button acts like a controller, telling the renderable mesh what to do and when -- but the renderable mesh has no knowledge that it's part of a GUI.
Likewise the button can have an audio source, which it controls at the right times.
 
This gives you a two-layer code base.
The bottom layer contains three completely independent modules (Graphics/Input/Audio) that do not connect to each other whatsover.
The next layer contains the GUI module, which is aware of the Graphics/Input/Audio modules in a one-way relationship (it knows about them, but they don't know about it).
 
Code-bases should always be made up of layers of modules, with one-way relationships propagating down through the layers. When dealing with large modules, you should often also pay attention to the DIP.

For an example that even when two modules are being used together to build a bigger component, they can still have no direct relationship with each other, let's say that an AudioSource has an event that occurs when it finishes playing, and we want to disable a RenderableMesh when this occurs.
Higher level components, such as A GUI class here, often act as 'glue' that assembles several lower level components together, creating the dependencies between them.

struct AudioSource
{
...
  std::function<void()> onFinished;
  void OnFinished() { onFinished(); }
  void Play();
};
struct RenderableMesh
{
...
  void Hide();
  void Show();
};
struct GuiButton
{
  GuiButton()
  {
    audio.onFinished = [this]()
    {
      this->mesh.Hide();
    };
    mesh.Show();
    audio.Play();
    //when audio finishes, the mesh will be hidden
  }
  AudioSource audio;
  RenderableMesh mesh;
}

There you've got an interaction between Audio/Graphics, but neither of them are directly aware of the existence of the other. A higher level component has injected this dependency into them.

 

As well as doing this with std::functions, you can also do it with interfaces/virtual functions, templates, C function pointers, etc. The concept transcends the mechanism.

 

Hmm, if Input shouldn't need to know about Graphics (which contains the window), how will I be able to poll the window for events?

Edited by BiiXteR

Share this post


Link to post
Share on other sites

Hmm, if Input shouldn't need to know about Graphics (which contains the window), how will I be able to poll the window for events?


Separate your window from your graphics. Allow window to push input messages to the input system and the graphics system to draw on the window.

Share this post


Link to post
Share on other sites

What I do in my engine framework is to make a difference between classes and static functions. When a class needs to have its own instanc as for example a TCPSocket does then it needs to stay as class/struct

class TCPSocket : public Socket
{
   public:
      TCPSocket() ...
};

But when something is a pure static collection of functions it will be handled as it is. This is for example the case of my Log pseudo class

namespace Log
{
   void WriteMessage(...);
}

So it seems in the code as if it is a class but it isnt and you could call

Log::WriteMessage("Hello World");

in a consitent way. The second thing I did is to seperate each module into an own project so that TCPSocket exists in its Network project (and subfolder) and Log in its Debug project (and subfolder). Anything is compiled as static library so linking dependencies between modules are eliminated until linking into the final executable is done

Share this post


Link to post
Share on other sites

This topic is 398 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this