C++ Engine Layout

Started by
9 comments, last by vreality 11 years, 3 months ago
Hello citizens!

I'm finally writing an simple Engine in C++ to get practice in Computer Graphics, but anyway, my concern now is the Design of the Engine itself.
At first, I thought a simple design: an Engine baseclass that the Game class would derive. The Engine class would have an bunch of pointer to other modules, such as Configs, Logs, Graphics, Files, Managers and all sort of things. Then, the Game class would derive access to it.
The problems comes when Modules have to interact with other Modules. The solution seems to give each module a reference to the Engine in the constructor, but then, I have cyclic dependency.

[source lang="cpp"]// Engine.hpp

#include "Module.hpp"
class Module;
#include "OtherModule.hpp"
class OtherModule;

class Engine
{
public:
Module *module;
OtherModule *othermodule;
...
};

// Module.hpp

#include "Engine.hpp"
class Engine;

class Module
{
private:
Engine *engine;
public:
Module(Engine *eng)
{
engine = eng;
// Now can play with engine->othermodule
}
};[/source]

To get the desired effect, I have to do forward declarations, witch don't seems good (elegant), with all the Modules.

To make it simple, I want some layout ideas... how do you design your engines?
How do you make many subsystems interact? without please making tons of Singletons.
Ideas please!
Advertisement

The problems comes when Modules have to interact with other Modules. The solution seems to give each module a reference to the Engine in the constructor, but then, I have cyclic dependency.

The problem is not that modules need to interact with each other, it is how you have chosen to handle it.
Anything that should know about something else should know only about that something else. Handling off a bucket-load of miscellaneous pointers is just reshaping the same problem behind singletons and globals—you just want access to that global but you gain unwanted access to ten other systems.

Give out pointers when and where needed, and only give out the needed pointers.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

the most important thing is you must not depend on C++ books!

theres is not priciple like Object oriented..

It's just for convience that's all..

and it's come from MSI. not extremely depend on hardware situation...

I sujest conisder more coponent type hierachy..

every object stand basic concept of data...

in my own engine .. I use mesh . mesh is just mesh he doesn't have any parents. D3D device is also just device.

It's very depend on hardware and it's concept 3D engine designer cant abstract it's nature..

so ... we must admit we treat hardware and don't make any parent or abstract heirachy.

Object orient side is just by game programmer

3D engine programmer can use Object oriented method..Cause we dealing with hardware not human mind and processing

any help plz lets me know smile.png


I think good 3D engine is ... simple. easy to use. enough performance. that's all..

so you must divide concept as simple as.. make it as singlton and keep them as independent. don't save data already DirectX or OpenGL contain. minimize wrapping API(It's usless if don't you making real big comercial 3D engine)

and save your effort for making 3D engine as possible as

I hope it help you.

Beauty is only skin deep , ugly goes to bones

World's only 3D engine tunner and 3D engine guru.

and real genius inventor :) but very kind warm heart .. and having serious depression for suffering in Korea

www.polygonart.co.kr ( currently out dated and only Korean will change to English and new stuff when I get better condition :) sorry for that)

so you must divide concept as simple as.. make it as singlton
Before anyone jumps on that to turn this into yet another singleton thread, read these first, and then start a new discussion about it if there's something not already covered by the other 100 discussions on that pattern.
Assuming I'm fully understanding your issue, the "extern" keyword should help you out.

After the Engine class declaration I just do
[source lang="cpp"]extern Engine engine;[/source]
Now you can reference the engine, and all of the modules it contains from anywhere. I had pretty much the same issue you had when making my first engine in C++. extern made my life way easier.
Quite interested in this aswell. For example a scenegraph that contains nodes that needs to use the engine renderer? Where/How would you pass the render instance to the nodes?
(Any change for some skeleton code example showing the relationship too?)
The problem with [font=courier new,courier,monospace]class Engine[/font] or [font=courier new,courier,monospace]class Module[/font] (e.g. [font=courier new,courier,monospace]Renderer[/font] or [font=courier new,courier,monospace]Physics[/font] or [font=courier new,courier,monospace]Audio[/font]) is that those classes have waayyy too many responsibilities, so much so that you're not really using "OO" design any more.

When you've got such big classes, it does become very tempting to just make a single global instance that every other part of the code can connect to, because you've never broken it up enough in the first place in order to create sensible, small connections.

For example a scenegraph that contains nodes that needs to use the engine renderer? Where/How would you pass the render instance to the nodes?
"Scene graph" is a very vague term that refers to a lot of different designs, so I'll pretend we've got:
struct Node { std::vector<Node*> children; virtual void DoRendering(); void Traverse(); };
void Node::Traverse()
{
for( int i=0, end=children.size(); i!=end; ++i )
children->Traverse();
DoRendering();
}
In this case, all you need to do is change [font=courier new,courier,monospace]DoRendering[/font] and [font=courier new,courier,monospace]Traverse[/font] so they take a single argument -- [font=courier new,courier,monospace]Renderer& renderer[/font].

However, as above, having a single massive class called [font=courier new,courier,monospace]Renderer[/font] might not be a good idea. I personally like to keep the "renderer" (whatever that is) and the "scene" seperate, so they don't need to communicate. Instead you ask the scene to give you a list of objects that need to be draw (and after that you don't need the scene at all). Then you can do whatever you need to with that list and a graphics device, in order to draw them.
struct Renderable { Mesh* mesh; std::vector<RenderState*> states; DrawCall draw; }
std::vector<Renderable*> renderables;
scene.Traverse( renderables ); // find all visible objects, put them into the above vector
std::sort( renderables.begin(), renderables.end(), someOptimisationCondition );//sort by depth/material/etc if required
for( int i=0, end=renderables.size(); i!=end; ++i )
{
Renderable& r = renderables;
device.Submit( r.draw, r.mesh, r.states );//issue the D3D/GL commands to draw this object
}
Well, interesting insights!


The problem with class Engine or class Module (e.g. Renderer or Physics or Audio) is that those classes have waayyy too many responsibilities, so much so that you're not really using "OO" design any more.


Hmm.. but as far as my interests go, I don't really want to make an 100% pure theoretically correct OO design so my teacher would give me a trophy. For me OO is a Tool that makes it easier to write and maintain complex systems.

Maybe it's time for some experiments, I'll try to break the dependencies to a bare minimum as suggested by L. Spiro.

But there are some "Modules" that virtually every class/module should have access, for instance, I have a class called Config that reads configurations from a LUA file. Almost all other module uses it, so now my solution is to pass it a reference in the constructor, witch seems right now. A friend suggested a Singleton but I think that this is completely misplaced. Another matter of global access is a Log file that I have. I managed it to get fully static, but I don't know if that`s a good idea either, but I definitely don't want to pass it around always...

Maybe it's time for some experiments, I'll try to break the dependencies to a bare minimum as suggested by L. Spiro.

This is the most important part. Now that you have a few different advices from people, there is really no way for you to know which is the best advice until you try them. As you pick one and implement it, you will inevitably encounter issues and see problems with your solution. At this point you may realize how to tweak it, in some cases, or you may realize that a totally different solution was much better. More importantly, you will better understand why some things are bad and why some things are good. A few years later, that understanding may change too rolleyes.gif

Have fun man! smile.png
OO is a Tool that makes it easier to write and maintain complex systems
That's why it and ideas like SOLID and separation of concerns were invented wink.png
These guidelines weren't invented to impress teachers (who, in my experience, aren't often good programmers); they were invented so that large projects could be easily managed and reasoned about.
I have a class called Config that reads configurations from a LUA file. Almost all other module uses it, so now my solution is to pass it a reference in the constructor, witch seems right now.[/quote]That's a decent solution, but if you wanted to break it up more, so that there isn't this single "Config" entity that forges links between your entire code-base, you can break it up too, e.g.
/* foo.h */ class FooConfig { /* just foo stuff */ };
/* bar.h */ class BarConfig { /* just bar stuff */ };
/* config.h */ class FooConfig; class BarConfig;
class GlobalConfig { public: const FooConfig& GetFooConfig(); const BarConfig& GetBarConfig(); };
//cpp
#include all 3
int main()
{
GlobalConfig& cfg = ...;
Foo foo( cfg.GetFooConfig() );
Bar bar( cfg.GetBarConfig() );
}
Now foo/bar don't have to have any connection between them at all, and instead at the next layer up in the code-base, the global-config composes them together into a useful whole.
Another matter of global access is a Log file that I have.[/quote]The "log file" example is always one that sparks a lot of discussion when it comes to globals/singletons/param-passing, and how best to deal with it...
Personally, I like to treat it specifically as an exception to the rule, because I don't see it so much as part of your application, but instead it's a part of the environment in which you're building your application. Things that form a part of your coding environment are ok to break the rules, because they're you're foundations and walls that need to be everywhere, holding up your code.
e.g. the connection to your IDE's debugger, or [font=courier new,courier,monospace]malloc[/font]/[font=courier new,courier,monospace]new[/font] or the call-stack, or the currently executing thread, are all 'environmental' features, like a debugging log. Every single bit of your code needs to run inside the environment created by these features.

This topic is closed to new replies.

Advertisement