Jump to content

  • Log In with Google      Sign In   
  • Create Account

Engine architecture questions


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
7 replies to this topic

#1 Crypter   Members   -  Reputation: 689

Like
0Likes
Like

Posted 15 May 2014 - 10:07 PM

Hello everyone,
 
I am currently in the process of restructuring our current engine into multiple projects/library modules to improve compilation time and to better allow different modules to be selectively reused as needed in engine utilities. This posed a few questions in general architecture design however :
 
1. Where does resource loading actually occur? In the file module? In a special module dedicated to resource factories (like a "modelLoader" or "audioStream" etc.? Currently the file module only provides a basic file manager that allows loading files from ZIP/PAK and disk directories as it were a single abstract file system. Under the Single Responsibility Principle I would expect the actual loaders to be elsewhere in their own dedicated modules (note that by "module" here I am referring to a project in a Visual Studio solution that compiles as a static or dynamic library.)

 

That is, what I was considering was having different projects for different loaders; such as imageLoaderTGA, imageLoaderDDS, modelLoaderHG etc. These would inherit from some base class defined in another more abstract module, such as hgImage or hgModel. For example,

 

Project : LoaderDDS

#include <hgimage/loader.h>
 
class HgLoaderDDS : public HgImageLoader {
   //...
};

Project : HgImage

class HgImageLoader {
  // abstract class for load,save for creating HgImage objects.
};

This of course works I am just not sure if its good nor where it fits in with resource management.

 

2. Is it common to have a single module that is itself a library that links all the major libraries together? That is, I currently have an Hg module that has the HgKernel class. This would provide system startup and shutdown as well as the main loop; it is the only singleton in the entire engine that allows a single access point to all systems. In order for this design to stay (I can rewrite it if needed) we need a single library that imports all major other engine libraries/projects and basically provides a very thin interface like so,

class HgKernel {

private:

  static HgKernel* singleton;
  gfx::HgVideoDriver* pkVideo;

public:

HgKernel ();

/*** KERNEL ACCESSOR METHODS ***/

virtual ~HgKernel () {
  if (singleton) {
    shutdown ();
  }
}

static inline HgKernel* get () {
  if (! singleton)
    singleton = new HgKernel;
  return singleton;
}

/*** ACCESSOR METHODS ***/

inline gfx::HgVideoDriver* getVideoDriver () {return pkVideo;}
inline double getFramesPerSecond ();

/*** SYSTEM METHODS ***/

virtual bool initialize ();
virtual bool initialize (std::string caption, int w, int h, int bpp=16, bool fullscreen=false);
virtual void shutdown ();
virtual bool run ();
};

The concern is that HgVideoDriver is implemented in a completely different library. If we extend this example to include an inputManager, audioManager, scriptManager, networkManager, etc., etc., all implemented in other libraries. Although the editor and game only need to link to this one HgKernel class, the HgKernel class would need to be able to link to all other main systems. To make it worse, the whole point of this setup was to allow any major system to call any other major system as needed; this would thus make this design impossible anymore - for example, the kernel object cant depend on the videoManager when the videoManager also needs to depend on HgKernel::get() -- we would have a circular dependency.

 

So the question for (2) is, how do you allow the major components to communicate with each other as needed without using a central setup like the above? Or is there a better alternative or way to implement the above that does not result in circular dependencies?

 

Thanks for any help!



Sponsor:

#2 Dave   Members   -  Reputation: 1527

Like
1Likes
Like

Posted 16 May 2014 - 08:39 AM

The topic of whether it is a good idea to have global pointers to each manager in your engine is an open one. I worked on a commercial game for PC, PS3 and Xbox that had a global static pointer to each subsystem manager, so g_pRenderer, g_pScene etc. I now currently work on a commercial engine that doesn't have subsystem managers but does have lots of static data and a few singletons dotted around for good measure. There are many ways to skin a cat.

 

In some regards I would say don't worry about. Don't lose sleep over unnecessary architecture that you don't require. When writing an engine its easy to fall into the trap trying to write it too perfectly but at the end of the day no engine, no matter how well written or otherwise, is any good if it isn't being used to make games.

 

Anyways, to give you an idea for what I do in my engine is have it broken down into to two areas and these are separate libraries. I have Kernel and Engine. Kernel contains a the main interface that Main() deals with and contains the Command, Task, Core (utilities for platform access) and threading. The kernel class allows access to each of its subsystems. The second library has the engine code in it so resource loading, gui, scene management, physics, rendering etc. The engine class provides access to those subsystems. Nothing in the kernel area depends on the engine area, only the other way around.

 

Circular dependancies do happen and these are more easily identifable if each subsystem requires a pointer to its subsystem dependancies upon construction, for example:

 

GuiManager* guiManager = new GuiManager(pSceneManager, pRenderManager, pInputManager, pDataManager);

 

Being forced to pass in pointers rather than just using globals makes you realise something might be wrong more easily.

 

 

In short, it sounds like you're on the right track but just focus on it meeting your needs and not some unattainable goal of perfection that noone ever really achieves.


Edited by Dave, 16 May 2014 - 08:40 AM.


#3 L. Spiro   Crossbones+   -  Reputation: 14263

Like
5Likes
Like

Posted 16 May 2014 - 11:42 AM

Under the Single Responsibility Principle I would expect the actual loaders to be elsewhere in their own dedicated modules (note that by "module" here I am referring to a project in a Visual Studio solution that compiles as a static or dynamic library.)

You are correct. A “file” module shouldn’t know anything but what a file is, what directories are, and perhaps via extensions what ZIP and other common compressions are.
 

That is, what I was considering was having different projects for different loaders; such as imageLoaderTGA, imageLoaderDDS, modelLoaderHG etc. These would inherit from some base class defined in another more abstract module, such as hgImage or hgModel.

Now you are going too far. There is no reason they need to be entirely separate projects. Make an image library and call it a day.

There is no reason to get more complex than this:
EngineOrg.png


Is it common to have a single module that is itself a library that links all the major libraries together?

That would be LSEngine in my image.


it is the only singleton

If you are using singletons you are doing it wrong.


it is the only singleton in the entire engine that allows a single access point to all systems.

how do you allow the major components to communicate with each other as needed without using a central setup like the above? Or is there a better alternative or way to implement the above that does not result in circular dependencies?

There is no reason everything has to go through one specific library to access the other libraries.
If I am LSPhysicsLib, I don’t need LSEngine in order to access LSMathLib (see image above).
All of the libraries are organized logically and hierarchically. If I am LSPhysicsLib, I simply link directly to LSMathLib, LSMemLib, and LSStandardLib.


Your first problem is using singletons. If you’d not have gone that route you likely wouldn’t have had any organizational questions to ask; singletons invite this kind of confusion where there otherwise would be none.

The second problem is trying to create some kind of hub where all the modules connect. This is clearly a problem.
Why would LSMemLib in my image ever access or need to know anything about LSPhysicsLib?
There is a very clear hierarchy between libraries. Keep it that way. Never put something in on a level higher than it needs to be.
Since LSMemLib overrides global new and delete, there is a global instance of it somewhere hidden in the engine (no choice in that). It’s inside the LSMemLib library itself. Why would it be anywhere else? Why would it be in some “central location where everything can be accessed”?


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

#4 Crypter   Members   -  Reputation: 689

Like
0Likes
Like

Posted 16 May 2014 - 03:06 PM

Thanks for the responses. smile.png

The topic of whether it is a good idea to have global pointers to each manager in your engine is an open one.

Technically the only statically allocated pointer was HgKernel::singleton. All other pointers were run time allocated on the heap. To be fair however, despite the design it has almost never been used since there really was no need (except in a few places discussed below.)

In short, it sounds like you're on the right track but just focus on it meeting your needs and not some unattainable goal of perfection that noone ever really achieves.

Really good advice there. The goal of the redesign was to aid in the following,

 

1. Improve build times. The original design had the entire engine in its one module; a change in one source file typically resulted in an entire build. This I blame MSVC; it would rebuild source files that are completely unrelated. This would resolve this and any future annoyances from the MSVC build system since the projects are smaller.

 

2. Improve structure. The only reason I originally designed the HgKernel::get() method and design was for safety reasons - one cannot foresee what might be needed by systems that have yet to be written. In writing a lot of the system though I realized that this is rarely used and most probably indicates a design flaw since it opens up any system to any other system; it is easy to couple systems together this way that should not be if not careful.

 

I do completely agree though; the goal of this is to simplify the current design rather then make it more complicated then needed.

Now you are going too far. There is no reason they need to be entirely separate projects. Make an image library and call it a day.

I think I will do this; all image related material can go into that same module (HgImage class, HgImageCodec class, all image codecs for loading and saving the different images, HgImageColorFormat etc.) This way anything related to images are in the same project and not scattered between projects; the image library would define what an HgImage is -- not the engine or kernel or even the graphics manager that would link to the image library. It also makes it easy to use in engine tools as well that can just link to the library as needed.

There is no reason everything has to go through one specific library to access the other libraries.

Very true. I realized this in the original design. There was a few cases however that I wanted to address for possible feedback - possible there is something that I don't see here.

 

Window events - Graphics manager or Input manager?

 

Currently I am using SDL to abstract the operating system specific API; I can easily just use SDL_PollEvent in the HgInput module and set up SDL OpenGL support in the HGOpenGL module separately. This of course won't work on standard Win32/Win64 or other system API's which the original designed used.

 

That is, lets say the system is using Win32. The graphics system creates a new render target (a Win32 Window.) Windows of course will send events to this Window, such as mouse, key, and system events (like Window close.) Do you typically handle this in the Graphics or Input manager?

 

The way I originally handled this (since I used Win32 not SDL) was to have the graphics manager handle these Windows events and pass them off to the Input manager. This means though that the Graphics manager needed access to the Input manager.

 

How would you handle cases like this?

 

Again please note that since I currently am using SDL I don't currently have this problem - its more so an interest.

 

Thanks for the help and suggestions!


Edited by Crypter, 16 May 2014 - 03:08 PM.


#5 L. Spiro   Crossbones+   -  Reputation: 14263

Like
0Likes
Like

Posted 16 May 2014 - 04:32 PM

That is, lets say the system is using Win32. The graphics system creates a new render target (a Win32 Window.) Windows of course will send events to this Window, such as mouse, key, and system events (like Window close.) Do you typically handle this in the Graphics or Input manager?
 
The way I originally handled this (since I used Win32 not SDL) was to have the graphics manager handle these Windows events and pass them off to the Input manager. This means though that the Graphics manager needed access to the Input manager.

Why would the graphics module have any idea what a keyboard is? There is absolutely no connection between these things.
Input goes to an input manager, after which it gets translated and handled by the game logic, which tells the player to jump or super-fart. If the graphics changed it is because the character’s position and animation changed, not because the graphics module was listening to input.


Again please note that since I currently am using SDL I don't currently have this problem

No, you have another problem. Since you can only poll events, you don’t know when to stop polling.
Input can’t be handled this way. Your input thread and game thread need to be separate and when you poll inputs you never poll beyond the current logical simulation time.
That is, in a standard game loop you will have a fixed logical update time and a variable rendering time (renders are per-frame).
This means if you lag for 65 milliseconds and your logical update rate is 30 milliseconds, next time you run your game loop you will perform 2 logical updates.
Let’s say you were smashing keys during that lag. Without the lag, some of that input was supposed to go to the first logical update and some to the second. You need timestamps and the ability to use them in polling for this to work regardless of lag.
With SDL_PollEvent(), you will consume both sets of input on the first logical update and nothing on the second.


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

#6 Crypter   Members   -  Reputation: 689

Like
0Likes
Like

Posted 16 May 2014 - 07:49 PM

Thanks for the response. smile.png

 

I would like to clarify some things if possible; it would help greatly. 

Why would the graphics module have any idea what a keyboard is? There is absolutely no connection between these things.

That is correct; the problem lies with how operating systems send event messages to the event callbacks responsible for a particular window. While the window should be within the graphics system (initialized with OpenGL compatibility) the event processing and dispatching should be within the input system.

 

That is, who should be responsible for the window procedure to process events?

 

If it is the input manager, then it must be in the same thread as the same thread that created the window via a call to the Win32 RegisterClassEx and CreateWindowEx (as far as I know; please correct me if wrong.) This would also require the graphics system to initialize the window with the window procedure callback found in the input module.

 

If it is the graphics system, the graphics system would need to pass those events to the input manager to be processed.

 

Is there an alternative?

No, you have another problem. Since you can only poll events, you don’t know when to stop polling.

Sorry, I am not entirely sure how that is related to the use of SDL. Under the standard Windows event loop, messages are polled via GetMessage or PeekMessage and the window procedure is only called when the main thread executes DispatchMessage in the event loop. That is, polling for system events always occurs. SDL_PollEvent is just a wrapper around the event dispatching for the underlying operating system in this manner.

 

That is, I think the problem you mentioned would occur regardless of the use of SDL or not.

 

Thanks for the suggestions so far and for clarifying some things.



#7 L. Spiro   Crossbones+   -  Reputation: 14263

Like
0Likes
Like

Posted 16 May 2014 - 08:21 PM

While the window should be within the graphics system (initialized with OpenGL compatibility) the event processing and dispatching should be within the input system.

The window is in the highest-level module (LSEngine in my case), not the graphics module. The graphics module in OpenGL for Windows needs only an HDC, which you can easily pass as a void * to an abstract graphics-engine interface.


	/**
	 * Initialize the operating system graphics API.  Must be done after
	 *	a window has been created.  Should not be called by the users.  Let the engine handle this.
	 *
	 * \param _pvParm The meaning of this value changes depending on the graphics API and operating system.
	 */
	LSVOID LSE_CALL CGraphicsLib::InitGraphicsLibApi( LSVOID * _pvParm ) {
#ifdef LSG_OGL
		COpenGl::InitOpenGl( _pvParm );
#elif defined( LSG_OGLES )
		COpenGlEs::InitOpenGlEs( _pvParm );
#elif defined( LSG_DX9 )
		CDirectX9::InitDirectX9( reinterpret_cast<CDirectX9::LPCLSG_DIRECTX9_INIT>(_pvParm) );
#elif defined( LSG_DX11 )
		CDirectX11::InitDirectX11( reinterpret_cast<CDirectX11::LPCLSG_DIRECTX11_INIT>(_pvParm) );
#endif	// #ifdef LSG_OGL
	}
	/**
	 * Initialize OpenGL.
	 *
	 * \param _lpdiInit Data required to initialize the device.
	 * \return Returns false if there are any problem during initialization.
	 */
	LSBOOL LSE_CALL COpenGl::InitOpenGl( LSVOID * _pvInit ) {
		ResetInternal( false );

#ifdef LSE_WINDOWS
		m_hDc = static_cast<HDC>(_pvInit);
		if ( !m_hgrcContext ) {
			m_hgrcContext = ::wglCreateContext( m_hDc );
		}
#endif	// #ifdef LSE_WINDOWS
		…

Sorry, I am not entirely sure how that is related to the use of SDL. Under the standard Windows event loop, messages are polled via GetMessage or PeekMessage and the window procedure is only called when the main thread executes DispatchMessage in the event loop. That is, polling for system events always occurs. SDL_PollEvent is just a wrapper around the event dispatching for the underlying operating system in this manner.
 
That is, I think the problem you mentioned would occur regardless of the use of SDL or not.

I’ve never used SDL but when I checked the documentation it appeared as though it buffered the events and then expected you to poll them later, as apposed to acting as the main-loop message dispatcher.

If it is the main-loop dispatcher, then it is a problem you have regardless of SDL…until you decide to stop having this problem and fix it.
http://www.gamedev.net/topic/650640-is-using-static-variables-for-input-engine-evil/#entry5113267

Luckily OpenGL is the easiest to move to a second thread.


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

#8 Crypter   Members   -  Reputation: 689

Like
0Likes
Like

Posted 17 May 2014 - 05:07 AM

Thanks again for the suggestions. smile.png

 

I really liked the idea of moving the window management code to a higher level and have been considering the problem with polling and waiting for system events in the event loop. I myself am not all that familiar with SDL either; only know a little bit - though I did eventually learn online that the main event loop cannot be in a separate thread which poses a potential problem.

 

Although none of this is yet official in the design, this is what I came up with. Please note that in this design I have dropped SDL in favor of Win32:

 

Project : HgKernel

Implements the real entry point such as main or WinMain depending on operating system and platform. All the entry point does is the following,

extern int HgApplicationEntryPoint(int argc, char** argv);
 
namespace hg {
static DWORD WINAPI HgApplicationThreadEntry (void* Param) {
    if (!wglMakeCurrent (GetDC(_hwnd), _hglrc))
       cout << "ERROR" << endl;
    if (HgApplicationEntryPoint (__argc, __argv))
       return 0;
    return 1;
}
class HgKernelCore : public HgKernel {
public:
   void run () {
      startup();
      HANDLE h = CreateThread(0,0,HgApplicationThreadEntry,0,0,0);
      SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_ABOVE_NORMAL);
      eventLoop();
   }
};
} //namespace hg

int main () {
   hg::HgKernelCore core;
   core.run ();
   return 0;
}

HgKernel::startup creates a default hidden window and enters the event loop. HgKernel::eventLoop is the event loop; it sets its own default thread priority before entering it. It also creates and runs HgApplicationEntryPoint as a separate thread -- this is defined in the actual game software.

 

Engine application

Since engine start up is automated yet still configurable, all the game program linked with the engine needs to do is define HgApplicationEntryPoint which acts as an operating system independent entry point. Note the initial window is not displayed until the engine is initialized by the game program. The engine can provide static access methods for the game program for all major sub systems; for example, HgKernel::GetVideoDriver or HgKernel::GetInputManager, etc.

int HgApplicationEntryPoint(int argc, char** argv) {
 
   HgKernel::initialize("My Game",800,600,32,false);
 
   /* simple test for OpenGL in this game/render thread. */
 
   glViewport(0,0,800,600);
   glDisable(GL_CULL_FACE);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   while (true) {
      glColor3f(1,0,0);
      glRectf(-0.25,-0.25,0.25,0.25);
      HgKernel::endFrame();
   }
   return 0;
}

Thanks again for all the suggestions and feedback; it is greatly appreciated. smile.png

 

As far as I could see, this design resolves all current concerns - no singletons; no global access point. Although HgKernel provides access methods; it is only for use by the game software since no other system links to it, the game/render loop runs in a separate thread from the event loop; the event loop thread runs with a configurable priority level and can send the events to the input manager using the input managers event classes to be buffered and time stamped, and also hides the entry point for operating system independence from the game programs perspective.


Edited by Crypter, 17 May 2014 - 05:11 AM.





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