Jump to content

  • Log In with Google      Sign In   
  • Create Account

C++ Engine Layout


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
10 replies to this topic

#1 victorsouza   Members   -  Reputation: 111

Like
0Likes
Like

Posted 17 December 2012 - 11:04 PM

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!

Sponsor:

#2 L. Spiro   Crossbones+   -  Reputation: 13581

Like
1Likes
Like

Posted 17 December 2012 - 11:30 PM

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
It is amazing how often people try to be unique, and yet they are always trying to make others be like them. - L. Spiro 2011
I spent most of my life learning the courage it takes to go out and get what I want. Now that I have it, I am not sure exactly what it is that I want. - L. Spiro 2013
I went to my local Subway once to find some guy yelling at the staff. When someone finally came to take my order and asked, “May I help you?”, I replied, “Yeah, I’ll have one asshole to go.”
L. Spiro Engine: http://lspiroengine.com
L. Spiro Engine Forums: http://lspiroengine.com/forums

#3 GeniusPooh   Members   -  Reputation: 157

Like
0Likes
Like

Posted 18 December 2012 - 12:18 AM

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 Posted Image


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, 18 December 2012 - 12:22 AM.

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)


#4 Hodgman   Moderators   -  Reputation: 30384

Like
1Likes
Like

Posted 18 December 2012 - 12:34 AM

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.

Edited by Hodgman, 18 December 2012 - 12:37 AM.


#5 Magdev   Members   -  Reputation: 197

Like
1Likes
Like

Posted 18 December 2012 - 12:57 AM

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.

#6 KaiserJohan   Members   -  Reputation: 1156

Like
0Likes
Like

Posted 18 December 2012 - 03:43 AM

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, 18 December 2012 - 03:45 AM.


#7 Hodgman   Moderators   -  Reputation: 30384

Like
2Likes
Like

Posted 18 December 2012 - 07:33 AM

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.

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[i]->Traverse();
  DoRendering();
}
In this case, all you need to do is change DoRendering and Traverse so they take a single argument -- Renderer& renderer.

However, as above, having a single massive class called Renderer 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[i];
  device.Submit( r.draw, r.mesh, r.states );//issue the D3D/GL commands to draw this object
}


#8 victorsouza   Members   -  Reputation: 111

Like
0Likes
Like

Posted 18 December 2012 - 09:12 AM

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

#9 achild   Crossbones+   -  Reputation: 1839

Like
1Likes
Like

Posted 18 December 2012 - 09:29 AM

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 Posted Image

Have fun man! Posted Image

Edited by achild, 18 December 2012 - 09:31 AM.


#10 Hodgman   Moderators   -  Reputation: 30384

Like
2Likes
Like

Posted 18 December 2012 - 11:12 PM

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

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.

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 malloc/new 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.

Edited by Hodgman, 18 December 2012 - 11:15 PM.


#11 VReality   Members   -  Reputation: 436

Like
1Likes
Like

Posted 19 December 2012 - 07:56 PM

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

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.


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.

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);
  ...
};

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 does remotely effect the behavior of Logger::Log(), but then Logger::Log() is generally designed to effect the environment outside 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.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS