Skip to main content
GameDev.net gamedev.net
🔒 Locked

Engine Layout Critique

Started by Crypter Jan 23, 2007 at 12:23 AM 21 replies 3k views
Original Post
Crypter
Crypter
Hello everyone, After several design layouts for my new engine, I think I came up with something promising. This engine is a commerically scaled engine, and is fairly large in size. Can others please critique my current layout? Please give me both positive and negative comments, posting suggestions and potental problems as well. This will help me improve the design layout for my upcomming 3d rpg. Heres the current layout... A few things reguarding the design... -The Task Manager is based off the Enginuity series, and works the same way -The OSI ("OS Interface) Layer contains *all* system specific routines and methods. No other part of the engine should contain *any* system dependencies. -The user lets the OSI know what subsystems to initialize at startup (Simular to SDL). This insures if the user has a Playstation2 graphics module, the user could *not* start the graphics subsystem at engine initilization, and use the Module Manager to load it (and OSI to place it in the task manager) -Graphics/Input/Sound Interfaces are *NOT* abstract classes. They provide base functionality for software render/triggered input. that is, if no API is loaded for the Graphics SubSystem, then the base Graphics Interface class uses Software Rendering, and render the 3d world to a "surface". Because software rendering/input capture is system specific, the base interface classes are part of the OSI (OS Interface) layer. -The engine can use the Module Manager to load static and dynamic libraries (dlls) to load Graphics/Input/Sound Drivers if they want. These derive (and hence) override the base interface classes, so OpenGL, DirectX, SDL, FMod, et al hardware emulation would accure. -The *ONLY* singleton class is the engine Core class. Through the Task Manager, the user could create multiple graphics systems for rendering to seperate outputs, same for virtually any other, or even user defined, tasks. -The Misc. Routines contain collision detection, AI, Partical system classes that might not fit anywhere else. It also contains a pure static "EngineState" class which provides all error code information. The user could use this, with information from the Sciene Manager, to test collisions, perform AI, etc.. -Will provide multithreading support in the OSI (OS Interface) layer, and assembly code (if required). ENDIAN macros are defined within primary core object (as I know of a few places they could be usefull).
This is it. I am posting this here for suggestions, ideas, problems (of any kind--even minor, or potental ones) for feedback on my current layout. After about 4-some engine attempts, and studying several engines layouts (NeoEngine, Irrlch(sp?) I (Hope!) This will work very well in design. What do you think? Any response is welcome.
_goat
_goat
I would switch the Singleton from the Core class to the OSI. You'll only ever be on one operating system at a time (and have to store state information), however you may want (in the future) more than one instance of some of your managers, like the ability to log to different things concurrently, or have two sets of settings, etc. The memory and file manager could easily be moved to the OSI, as they are directly dependant on the Operating System. The Settings Manager and Logging Manager could just be regular classes, allowing for more flexibility. Other than that (ha, those aren't insignificant changes), it looks pretty good.
LorenzoGatti
LorenzoGatti
Some things hurt my sense of design:
1) You have nine "Managers", nine more than a tasteful amount: find better names for them and you'll discover better defined classes.
For example, the "Module Manager" is actually a platform independent Module Registry (maybe some of its varied uses call for a somewhat different Plugin Registry), helped by a rather simple platform dependent Module Loader, while the "Scene Manager" is halfway between a Scene Graph and game-specific data.
2) "Misc Routines" is a synonym of "not yet designed subsystems".
3) The diagram has some lines and some arrows, hollow arrows that are used UML to express inheritance, unlabeled boxes around other boxes, a mixture of classes and subsystems, a strange sidebar, shaded boxes, a bizarre and badly drawn layout. What about a plain old UML class diagram?

More substantial questions:
1) What is the boundary between game-specific data and logic and engine services? What is constrained but sophisticated, and what is basic but flexible?
2) What are the platform dependent and platform independent parts of the Graphics Subsystem, Graphics Interface, GUI? How do different capabilities (e.g. OpenGL vs software rendering) come into the picture?
3) How are the Task Manager and the Timer related? What about frame rate and game model update rates? How do you use multithreading?
Omae Wa Mou Shindeiru
Gaenor
Gaenor
What I usually do is think how I'm going to make my calls to the engine. How would I add a model in your design? How would I change the music volume?
Also I don't see a place for non-GUI graphics.
Oh, and how does the system use the file codecs?
Crypter
Crypter
Thank you for the feedback! On with the questions..

Quote:

1) You have nine "Managers", nine more than a tasteful amount: find better names for them and you'll discover better defined classes.

The layout I posted is abstract. That is, they do not represent the classes in the engine, just the way they communicate. Using the "Module Manager", as an example, I was thinking of this..
class Module {// Abstract class interface};class StaticModule : public Module {...};class DynamicModule : public Module {...};class ModuleManager {   std::vector <Module*> m_kvpModules; //mabey a list would be better};

This actually seems kind of what you described..

Quote:

3) The diagram has some lines and some arrows, hollow arrows that are used UML to express inheritance, unlabeled boxes around other boxes, a mixture of classes and subsystems, a strange sidebar, shaded boxes, a bizarre and badly drawn layout. What about a plain old UML class diagram?

The layout uses those arrows to represent how the systems communicate with each other. ie..

-The Core class has a static LogManager object. This is the only engine class that will ever use LogManager directly.

Any other classes that require access will use Core::log to access the logging manager.

The log manager can create multiple LogLocations (where to log to), multiple log levels, and even thresholds. The engine (or user) could add more locations, so a single LogManager << LogLevel (ERROR) << "Text" will output to all locations with threshold set below ERROR level.

..The Log Manager is already created from a previous engine, and is very versital [smile]

The boxes help seperate major engine sections (for cohesion)--they are for layout purposes only, aand do not represent anything.

The side bar represents tha game and/or application using the engine.

Quote:

1) What is the boundary between game-specific data and logic and engine services? What is constrained but sophisticated, and what is basic but flexible?

I am trying to make everything as flexable as possible. Unless I do not understand your question?
Quote:

2) What are the platform dependent and platform independent parts of the Graphics Subsystem, Graphics Interface, GUI? How do different capabilities (e.g. OpenGL vs software rendering) come into the picture?

This is the heart of the design...
// Display could represent HWND (For Win32), or a buffer ptr for unkown systems // ie, mabey it represents ptr to video memory? We dont know, so keep it void*typedef void* DisplayPtr;// Abstract Video class (Software render) SYSTEM SPECIFIC, so its part of OSI// Layerclass VideoDriver {   DisplayPtr   m_pkPrimary;  // primary displaypublic:   virtual int SetDisplay (DisplayPtr display) {      if (display)         m_pkPrimary = display;      return 0;   }   virtual inline void DrawPixel (int x,int y, int z, int c) {//OSI layer is only set of classes that could have system-specific code..if (m_pkPrimary) {#ifdef _WIN32   // m_pkPrimary represents window handle..   ::SetPixel (GetDC ((HWND)m_pkPrimary),x,y,c);#else   //dont know platform. Assume m_pkPrimary is byte* buffer to memory block   m_pkPrimary [x+y*iDisplayWidth] = c;#endif   }}};


If the user wants hardware acceleration, they will need a "Graphics Driver" library. This library contains API specific code, such as OpenGL. The user could either use the Module Manager, and load it an externel library, or link it in.

Heres an example openGL implimentation:
// OpenGL Driver class for Graphics SubSystem. If the API is system specific // (DirectX), we dont care if there are system-specific code in that library.// For other APIs (ie, OpenGL), the OSI (In this case, the base VideoDriver // class) initializes WGL to work with OpenGL// Library...class VideoDriverOgl : public VideoDriver {   DisplayPtr   m_pkPrimary;  // primary displaypublic:   VideoDriverOgl () : VideoDriver () {} //initialize base class   virtual int SetDisplay (DisplayPtr display) {      if (display)         m_pkPrimary = display;      return 0;   }   virtual inline void DrawPixel (int x,int y, int z, int c);}};// ALL Video drivers are REQUIRED to have this initialize routine.// This hides the API-specific implimentation behind our "VideoDriver" classVideoDriver* InitGraphics () {   return new VideoDriverOgl;}

This can be very usefull. For example, A Playstation2 library can be written ontop of the GraphicsDriver class, and voila. No game code, no other code needs to be changed because a)Everything works through VideoDriver*, and b)The core base class is overrided by the PS2 implimentation.

For systems that do not have an API (DOS, Gameboy Advance, et al..) just add the system-specific code in the OSI's base VideoDriver class, wrapping it in a simular fashion as _WIN32, _APPLE, etc. Thats all their is to it.

Granted, as you can see the VideoDriver base class can get ugly, however this design encapsulates all graphical system dependencies within a single set of classes, rather then all throughout the engine code. --This is the goal of this layout.

Quote:

3) How are the Task Manager and the Timer related? What about frame rate and game model update rates? How do you use multithreading?

The timer is the only task that is automatically put in the task manager. Every call to Update() the engine, the OSI will tell the Task Manager to Update () all of its members--updating the timer by 1 frame.

The user specifies what extra subsystems (graphics/input/sound) to initialize when the engine initializes. These tasks are updated through the task manager.

For claifcation, The OSI *is* a class that manages the creation/killing of its subsystem interface classes. The layout shows the OSI class is the only class that uses the Task Manager..
class OSI {   TaskManager*  m_kpTaskMngr;   ModuleManager* m_kpModuleMngr;   OSI () {}  //made private for singletonpublic:   virtual inline int CreateNew (Task& rkTask) {      m_kpTask->Add (rkTask);   }   virtual inline OSI* Get () {      static OSI* pkOSI = new OSI;      return pkOSI;   }};

The OSI is an interface class for the most part. It also provides the interface to access each of the tasks within the TaskManager -- This allows the Engine Core to recieve the timer, Graphics/Input/and Sound SubSystems. The user could get direct access (if they want) through this.


I hope this clarifies my engines design a little more.

Can I please get positive (or negative) critique on the layout and points I posted here? All comments are welcomed, and are appreciateated.

Thanks!
Crypter
Crypter
Wow ...did not realize how big my previous post was! The preview box makes it look so small [grin]

Anyways, I wanted to post how the engine will be used...
int main ()  //Engine defines WinMain() to call standard main() routine{   Core* pkCore = new Core;   // Core has singleton OSI ptr object. Sends flags to OSI class.   // OSI initialzes base (software HEL) graphics/input/sound tasks,   // and put them in its task manager...   pkCore.Initialize (OSI_GRAPHICS|OSI_INPUT|OSI_SOUND);   // Core stores a ptr to OSIs Module Manager for easy access.   // The Load() method looks for secial "Initialize()" method each   // library/DLL is required to have, and executes it. In this case, the    // OpenGL Graphics Driver implimentation is simular to my previous posts   //  InitGraphics() method..   GraphicsDriver* gfxDriver = pkCore->GetModuleManager ()->Load ("OpenGL.dll");   if (! gfxDriver) {      // No OpenGL. We are using Windows, and Win32 is implimented in OSI, so       // use software renderer instead. The software renderer is already       // initialized because of the OSI_GRAPHICS flag.      gfxDriver = pkCore->GetOSI ()->GetTask (OSI_GRAPHICS);   }   else {      // OpenGL Driver initialized, so add it. Because the OSI_GRAPHICS flag       // was set, the software driver is automatically added to the task       // manager. Here, we have the option of keeping both renderers, or just      // use our loaded one.      pkCore->GetOSI ()->AddTask (gfxDriver);   }   // setup primary display (Creates window in fullscreen mode)   gfxDriver->SetDisplay (640,800,32,0,0, true);   // Load bitmap from disk. The file codecs are seperate library files,   // to allow easier extensibility. All file codecs derive from a base   // File class, handles by the Cores File Manager. So..   // All graphical entities are accessed through Graphics Interface.   // That is, there are classes such as Palette, Surface, Vertex2d/3d,   // polygons, light.. There is also a base "ImageCodec" class that all image    // codecs derive from. This allows a standard way of loading image files.   // These codecs are NOT files themselves, instead they are file loaders (ie,    // use the File Manager)   ImageCodec* pkImageCodec = new BmpCodec;   Image* pkImage = pkImageCodec.Load ("Image.bmp");   while (1) {      // update engine. Calls PSI->Update() which will handle our message pump,      // and updates all our tasks (Graphics/Input/Sound)      pkCore->Update ();      // render our image (Only 2d textures--locks frame buffer (or primary       // display surface if software rendered)      gfxDriver->Begin (GFX_TEX_2D);      // blit image...      gfxDriver->Blt (0,0,640,480,pkImage);      // release lock..      gfxDriver->End ();   }}   // gfxDriver is shared, so we dont need to delete it   pkCore->Shutdown ();   return 0;}

All calls to new/delete automatically envoke the memory manager. The engine, by default, only logs ERRORs and WARNINGs to stdout/stderror for portability.

The engine will work somewhat of what is posted above (I added detailed descriptions of how everything is peiced together)


What do you think? I am certain this will clarify the overall engine layout, and how it works together.

Can anyone now be able to fully critique my engines design? Any comments will be extremily helpful here--whether good or bad.

Thank you!!
_goat
_goat
If you're really concerned about portability, you're going to have to find a way to deal with platforms that can't use DLLs. Furthermore, at the moment, having #ifdefs all through your functions like that, will lead to trouble. It's far easier from a maintenance perspective to have the #ifdefs cover entire files, (ie, write out all the functions over again). This may seem extreme, but not only does it allow more optimisations, but it makes it easier to support other platforms in the future.

And, uh, from a code perspective, the Hungarian Notation has to go. I'm not even sure what it's signifying (which is a really bad sign). What a pk? Or a kp, or an rk? Are those prefixes really necessary? The short answer is no - if you name your variables with sensible names, those little things only add more time to your development cycle - especially when you have to change the type or scope of the variables, and such the variable name becomes "incorrect".
Crypter
Crypter
I use the prefex "k" as a way to easily seperate our own data types with other library's. So the prefex "pk" refers to a pointer to an object [we] made, "pkv" referrs to a vector of pointers containing one of our class data types.

And yes--I know a way to support both DLLs and static library builds [smile]

Nontheless, I decided to design a UML diagram for the engine, so that I can take a better perspective at what I am trying to achieve, and better set our goals for my next project.

I will probably post reguarding my UML designs later, as I want this to be easy, yet provide powerful features for virtually any machine.

I might have taken it a little to far reguarding an "OSI" layer, but I will be trying different designs until it is as flexable as I want it to be.

Thank you all for your input!
_goat
_goat
Quote:
Original post by Crypter
I use the prefex "k" as a way to easily seperate our own data types with other library's.


That's what namespaces are for. :)
LorenzoGatti
LorenzoGatti
Quote:
Original post by Crypter

Quote:

1) What is the boundary between game-specific data and logic and engine services? What is constrained but sophisticated, and what is basic but flexible?

I am trying to make everything as flexable as possible. Unless I do not understand your question?


Some of the things that a game needs to do will be done in engine code and some in game specific code.
The game adapts to available engine features, so different engines are suitable for different types of game.
For example an engine could expose blank OpenGL windows (e.g. JOGL or LWJGL) and another could expose a scene graph (e.g. Java3D): the former requires significant amounts of design and code but is general, the latter is specialized and efficient (we can say it has some "added value" over no scene graph), but limited since some things cannot be done easily with a scene graph (or with a certain type of scene graph).

If you want to make everything flexible, you'll make an engine that does almost nothing beyond imposing an API learning curve: there must be something that makes your engine better than do-it-yourself solutions for some use, even if you make other uses unsuitable.
The usual approach to this dilemma is vertical layering (e.g. scripted animation over a scene graph over OpenGL over cross-platform window creation) to offer multiple choices in the generality vs specialization tradeoff and horizontal peaceful coexistence of alternative modules (e.g. loaders for many different image, audio and 3D model formats, or combining drawing calls from different scene graph, sprite, particle, GUI libraries into the same OpenGL window).
Omae Wa Mou Shindeiru
tstrimp
tstrimp
Quote:
Original post by _goat
Quote:
Original post by Crypter
I use the prefex "k" as a way to easily seperate our own data types with other library's.


That's what namespaces are for. :)


It really doesn't matter as long as he uses it consistently. It's probably much easier to manage adding a k to the variable name then to wrap every declaration in a namepsace.

namespace xyz{  Core* pCore = new Core;}xyz::pCore->doSomething();
Or
Core* pkCore = new Core;pkCore->doSomething();


Radan
Radan
The namespace approach is more readable, even if it is easier to just add a k in the variable name.

Statistically , code maintance is more time consuming than code creation, so I think a coding style should cater more to the code maintance part of the lifecycle.

IMHO time "wasted" writting longer more readable code should pay off significantly when you return to alter the code a year later.
-----------------Always look on the bright side of Life!
tstrimp
tstrimp
Hell, then why bother with the p prefix either. All pointers should then be in a pointer namespace.

namespace pointer{  namespace xyz  {    Core* core = new Core;  }}pointer::xyz::core->doSomething();


Lets extend it a bit further and say that all hungarian notations should be namespaces instead of prefixes.


int cItems;
namespace count{  int items;}



std::string m_name;
namespace member{  std::string name;}


int maxItems
namespace max{  int items;}
Crypter
Crypter
Quote:

For example an engine could expose blank OpenGL windows (e.g. JOGL or LWJGL) and another could expose a scene graph (e.g. Java3D): the former requires significant amounts of design and code but is general, the latter is specialized and efficient (we can say it has some "added value" over no scene graph), but limited since some things cannot be done easily with a scene graph (or with a certain type of scene graph).

What I was thinking is hiding the concept of a "window" behind a "Suface" data type. This way, the primary surface could represent a window, or even video memory itself through a void*. Also, you can create multiple primary surfaces for multiple dispalys (*just* thought of that).

The scene graph uses this "surface" abstraction by creating/redndering through the Graphics SubSystem.

I just think--Why limit it to one or the other? Why not let the user use what ever they want?

ie..
//Graphics SubSystemclass Surface {   void*  m_pData;   int    m_iWidth, m_iHeight;  //etc..   operator void* () {return m_pData;}};// Create windowint GraphicsDriver::Create (int iWidth, int iHeight) {// create our primary surface#ifdef _WIN32   // create and register window..   // now store window under surface abstraction..   m_pDisplay = new Surface ((void*) HWND);#endif}// Scene Manager..class SceneManager {   GraphicsDriver* m_pGfx;   Surface*        m_pDisplay;   int Create (GraphicsDriver& rK) {      m_pGfx=rK;      m_pDisplay = m_pGfx->GetDisplay ();   }};

Here we could provide get methods so the user can access the Graphics SubSystem directly, or even just primary display.

Of course, because we have a Scene Manager abstraction, they could also just access through the scene manager.
Radan
Radan
Quote:
Original post by tstrimp
Hell, then why bother with the p prefix either. All pointers should then be in a pointer namespace.

...



That is less readable, it makes no contextual case. Subsystems they write would contextually fit into the same namespace that can have a signature stating it is their's.

IMHO, it makes more sense than adding k to every single variable's name. Putting all pointers in the same namespace makes absolutely no contextual sense. You were just blowing my suggestion out of proportion to make it appear invalid.

If you disagree, point me wrong. I would be more than gratefull to correct my mistakes.
-----------------Always look on the bright side of Life!
sirGustav
sirGustav
Some told me that hungarian notation shouldn't be used to indicate the type of the variable, but rather the unit it is in:
float fSpeed; // is wrong, butfloat mphSpeed, // (Miles Per Hour)      mpsSpeed, // (Meters Per Second)      upsSpeed; // (world-Units Per Second)// are all correct.
The abbreviation's should of course be explained in some src readme so users and fellow coders understand what you mean when you type mps :)
Crypter
Crypter
Quote:

The abbreviation's should of course be explained in some src readme so users and fellow coders understand what you mean when you type mps :)

What if "mps" represents different things among multple source files?
_goat
_goat
Quote:
Original post by tstrimp
Hell, then why bother with the p prefix either. All pointers should then be in a pointer namespace...


... are you a troll, hiding in a respectable person's clothing?

As far as I can tell, there are been four different versions of "hungarian notation" (I use quotes for a reason) presented in this thread.
  • The first is the infamous "iItems" type, which shows that Items is an int. This has many disadvantages, not least that when you change the type of variable, you have to change the variable name for every time you used it. For example, if I pointed out your Surface class should use size_t (or unsigned int) for its width and height, instead of int (because a surface can't have a negative area), you'd have to change everything. Whether or not you agree that it needs changing isn't the issue - the point is that if there ever was (and often is) a case where you need to change something, it quickly becomes rather time-consuming. Especially as the project grows.


  • The second is hungarian notation, or isn't hungarian notation, depending on who you ask. Using m or m_ before member variables avoids collisions inside the class. So m_height = height works inside functions which have a temporary variable called height. Interestingly enough, before Hungarian Notation was popularised by that guy at Microsoft, people used a trailing underscore to signify the same thing. I.e., height_ = height. Seeing as it predates HN, and is actually necessary (no namespaces inside classes), unless you want to use this->height = height all over the place, it's considered okay. (Yes, I use m, as in mCaption)


  • The third is what sirGustav mentioned, which is Applications Hungarian Notation (the first mentioned was Systems Hungarian Notation). It is effectively just shortening a possible variable name, (counter examples include: miles_per_hour, metres_per_second, WU_per_second). This has its place, because it adds semantics to the code, but fails for international things (it is easy to translate words, but difficult to translate abbreviations unless you know the language flawlessly).


  • The fourth, and final one, is Crypter's original usage, which is to simply avoid collisions between names at a global level, basically across his entire library. I.e., if he made a 2d vector, and called it vector, it would clash with std::vector. However, kVector won't. It is basically the same thing as namespacing, but with the added disadvantage of not working if someone else calls their class kVector (however unlikely that may be). The namespace was included in C++ for this reason. So Crypt::vector is unambiguous.


If you're interested, a quick overview of the history of Hungarian Notation can be found here. Yes, it's Joel on Software, and he can be a bit patronising to beginners... but it's still a decent read. PLEASE note he advocated Applications Hungarian Notation, not Systems (and didn't mention internationalisation). tshrimp used both Apps and Systems in his crazy post.
tstrimp
tstrimp
Quote:
Original post by _goat
... are you a troll, hiding in a respectable person's clothing?


Some days I feel that way!

Quote:
The fourth, and final one, is Crypter's original usage, which is to simply avoid collisions between names at a global level, basically across his entire library. I.e., if he made a 2d vector, and called it vector, it would clash with std::vector. However, kVector won't. It is basically the same thing as namespacing, but with the added disadvantage of not working if someone else calls their class kVector (however unlikely that may be). The namespace was included in C++ for this reason. So Crypt::vector is unambiguous.


I'm well aware of the different types of hungarian notation and I think you're wrong about the last point. It wasn't just to avoid collisions between names. He wasn't adding it to the class name to prevent collision, but to the local variable to signify to him what library the variable was created from. If he had been using

Core* pCore = new kCore;

instead of
Core* pkCore = new Core;


I would agree with you completely. Core should be in it's own namespace. But the idea of putting the variable itself within a namespace doesn't seem quite appropriate, like some of the examples I've shown before. If used consistently throughout the application and there is some useful base class that is shared among all classes in the library (like functions for releasing resources before deletion or what-have-you) then someone making changes could easily see (as long as it's documented) what variables need to call release() before deletion. As long as the k actually represents something useful to the programmer and isn't just something they put on to show off how often they use the library, and it's used consistently then I don't think there is any problem with that convention.
tstrimp
tstrimp
Quote:
Original post by Crypter
Quote:

The abbreviation's should of course be explained in some src readme so users and fellow coders understand what you mean when you type mps :)

What if "mps" represents different things among multple source files?


Then you're doing it wrong! The whole system falls apart if you don't use them consistently. What good is mpsVariable if mps can mean many different things? Then it's not longer a matter of just looking at the variable name to determine what it is. You now have to browse through your comments and hope the previous programmer (or you) described what "mps" means in this instance.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.