Sign in to follow this  
victorsouza

C++ Engine Layout

Recommended Posts

victorsouza    111
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!

Share this post


Link to post
Share on other sites
L. Spiro    25622
[quote name='victorsouza' timestamp='1355807089' post='5011913']
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.
[/quote]
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

Share this post


Link to post
Share on other sites
GeniusPooh    161
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 [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]


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. Edited by GeniusPooh

Share this post


Link to post
Share on other sites
Hodgman    51234
[quote name='GeniusPooh' timestamp='1355811507' post='5011926']so you must divide concept as simple as.. make it as singlton[/quote]Before anyone jumps on that to turn this into yet another singleton thread, [b][url="http://www.google.com/search?q=site%3Agamedev.net+Singleton"]read these first[/url][/b], and then start a new discussion about it [b]if[/b] there's something not already covered by the other 100 discussions on that pattern. Edited by Hodgman

Share this post


Link to post
Share on other sites
Magdev    197
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.

Share this post


Link to post
Share on other sites
KaiserJohan    2317
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?) Edited by KaiserJohan

Share this post


Link to post
Share on other sites
Hodgman    51234
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 [url="http://en.wikipedia.org/wiki/Single_responsibility_principle"]waayyy too many responsibilities[/url], 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.
[quote name='KaiserJohan' timestamp='1355823823' post='5011971']
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?
[/quote]"Scene graph" is a very vague term that refers to a lot of different designs, so I'll pretend we've got:
[code]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[i]->Traverse();
DoRendering();
}[/code]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" ([i]whatever that is[/i]) 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 ([i]and after that you don't need the scene at all[/i]). Then you can do whatever you need to with that list and a graphics device, in order to draw them.
[code]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[i];
device.Submit( r.draw, r.mesh, r.states );//issue the D3D/GL commands to draw this object
}[/code]

Share this post


Link to post
Share on other sites
victorsouza    111
Well, interesting insights!

[quote name='Hodgman' timestamp='1355837622' post='5012034']
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.
[/quote]

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...

Share this post


Link to post
Share on other sites
popsoftheyear    2194
[quote name='victorsouza' timestamp='1355843573' post='5012073']
Maybe it's time for some experiments, I'll try to break the dependencies to a bare minimum as suggested by L. Spiro.
[/quote]
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 [img]http://public.gamedev.net//public/style_emoticons/default/rolleyes.gif[/img]

Have fun man! [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img] Edited by achild

Share this post


Link to post
Share on other sites
Hodgman    51234
[quote name='victorsouza' timestamp='1355843573' post='5012073']OO is a Tool that makes it easier to write and maintain complex systems[/quote]That's why it and ideas like [url="http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)"]SOLID[/url] and [url="http://en.wikipedia.org/wiki/Separation_of_concerns"]separation of concerns[/url] were invented [img]http://public.gamedev.net//public/style_emoticons/default/wink.png[/img]
These guidelines weren't invented to impress teachers ([i]who, in my experience, aren't often good programmers[/i]); they were invented so that large projects could be easily managed and reasoned about.
[quote]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.
[code]/* 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() );
}[/code]Now foo/bar don't have to have any connection between them at all, and instead at the next [i]layer[/i] up in the code-base, the global-config composes them together into a useful whole.
[quote]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 [i]the environment in which you're building your application[/i]. 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 [i]everywhere[/i], 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 [i]inside the environment created by these features[/i]. Edited by Hodgman

Share this post


Link to post
Share on other sites
vreality    436
Logging is the classic example of the thing you make that everything needs access to, because it's the only example (unless you're re-implementing other environmental features, like the memory manager, etc.)

[quote name='Hodgman' timestamp='1355893950' post='5012334']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 [i]the environment in which you're building your application[/i].[/quote]

I don't know how Hodgman manages to be right every single time, but once again, Hodgman's right. Logging is the exception because it's diagnostic scaffolding which is unrelated to our code's actual functionality.

But even so, it's possible to come up with a reasonable design for it.

Our design goals might be:
- Support multiple logging channels, locally configurable to be sent to various destinations.
- No logger object needed at point of logging (i.e. no passing loggers to every single function).
- No globally accessible state.

[CODE]
class Logger
{
public: // "Object part"
void SendChannelToFile (string const & Channel, string const & FileName);
void SendChannelToDebugConsole (string const & Channel);
void SendChannelToOnScreenConsole (string const & Channel);
...

public: // "Global part"
static void Log(string const & Channel, string const & Message);
...
};
[/CODE]

The basic idea is that Logger::Log() has some sort of safe default functionality (like ignoring all log messages), but if a Logger object exists, then the function uses that object's configuration information to route any configured channels.

Note that the globally accessible Logger::Log() function neither sets nor gets state.

The appropriate part of the codebase can create a Logger object, and load a local configuration file to configure logging channels according to the local user's current preference. Setup, access, lifetime, and cleanup of the object and its configuration data are all managed as with any normal object. The Logger class can prevent more than one instance from being instantiated, to avoid mishaps.

The Logger object's internal state [i][b]does [/b][/i]remotely effect the behavior of Logger::Log(), but then Logger::Log() is generally designed to effect the environment [i][b]outside [/b][/i]of the application (like printf() and std::clog() ).

And yes, the connection between the global and object parts would be accomplished through some static pointer, and Logger::Log() may need to be thread-proofed. But again, this system is the exception, and all of the above functions can be conditionally compiled to no-ops in published versions.

Share this post


Link to post
Share on other sites

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