Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
101 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Enginuity, part 4
The Entry Point and Task Pool; or, Swim Your Way In

Still reading these? I must be better than I thought. This article we'll take all the code we've produced so far - the Foundation Tier of the Enginuity engine - and actually make an executable program with it. Then we'll put together some of the 'system tasks' that any game will need.

But of course, if I'm going to take the code we've produced so far, you'll need to know about it. So go read the other articles, if you haven't already.

Entry Point

The Application Entry Point is the place in your program where it all begins. Traditional C/C++ programs have an entry point called 'main' - Win32 programs have 'WinMain,' and so on and so forth. If you were to represent your program as a tree, where nodes are functions that call other functions, the entry point would be the very root of the tree. Before now, Enginuity didn't have an entry point, so we couldn't build it into an executable.

We're not about to give Enginuity an entry point, either. As an engine, it shouldn't have one; as it is now, we can build it as a library, and have a proper program start the engine whenever it wants. We give extra control to whoever wants to use the engine - perhaps an anti-piracy system needs to be initialized before the engine starts, for example.

So what we'll be doing here is looking simply at a sample program that makes use of the engine. The engine could be in a library or DLL, or it could be simply build as part of the project; it doesn't matter. Personally, I'm building the whole thing in one project, and just splitting the source files into 'Engine' and 'Game' folders.

Given that we're aiming for a relatively cross-platform engine here, we have a problem. I already mentioned a discrepancy between entry point functions on Win32 and other systems - they have different names (and different parameters). Do we provide both main() and WinMain() functions? Do we use some kind of conditional-compilation trick?

Neither. SDL has already solved the problem for us. It contains the code to 'insulate' us from the underlying system - so that when it gets to us, we always use a main() function. SDL provides the 'translation' from WinMain() to main() under Win32, and so on for other platforms. All we have to do is make sure we link to sdlmain.lib, and that we're including sdl.h. Here's the main() function we're going to use:

int main(int argc, char *argv[])
{
  new CApplication();
  CApplication::GetSingleton().Run(argc,argv);
  delete CApplication::GetSingletonPtr();

  //clean up any remaining unreleased objects
  IMMObject::CollectRemainingObjects(true);

  return 0;
}

Before we get into the body of the function itself, I'll just say this: make absolutely sure that the main() function has a header like the one above. Same return type, same name, same argument types. If you get errors about 'sdl_main is undefined,' check here. (The truth is that sdl_main.h includes a macro to turn any function named main() into one named sdl_main(), so that it doesn't get confused with the main() function that SDL provides. As far as I can tell, an unfortunate side effect of this is that you shouldn't use the name 'main' for any functions or variables; but frankly, I consider it a small price to pay).

OK. You're probably wondering what this CApplication class is. You've probably gathered that it's a Singleton; it represents your program. It's often useful to encapsulate (wrap up in a class) the 'application' itself; you get the benefits of construction/destruction, as well as extra control over lifetimes (as we'll see in a moment). The CApplication class, then, is the 'meat' of the program.

So the first thing we do is to create a new CApplication object (because that's how the Singleton mechanism works - check back to part 2 if you don't remember). We then pass argc and argv straight into the CApplication's Run() function. When it's done, we delete the CApplication object. So that's the whole of the 'application itself' done.

Then we do a last call to IMMObject::CollectRemainingObjects. This is where one of the major advantages of having the CApplication object comes into play. When CollectRemainingObjects() is called, all IMMObject-derived objects will be deleted; but after that, if there are any CMMPointers still around, they'll try calling Release() on their pointers - which will cause an access violation. In the end, we see that we can't call CollectRemainingObjects while there are any IMMObjects alive (and assigned). This means that keeping global CMMPointers is unsafe - they don't get killed till after the main() function is done - so instead, we can keep them in the CApplication object, and they get destroyed when the CApplication object is destroyed. Thus, when we reach CollectRemainingObjects we can release all still-allocated objects to avoid memory leaks completely, without worrying that anything is still latched onto them. When the CApplication object has been shut down, nothing should still be running, no CMMPointers should still be alive.

The CApplication object only needs to provide a Run() function; a constructor and destructor are optional (because for the most part, we'll only be adding CMMPointers to the CApplication object, and they have their own constructors), so I'm not going to show you the class definition here. Just remember that it derives from Singleton<CApplication>. Instead, let's skip straight to the Run() function:

void CApplication::Run(int argc, char *argv[])
{
  //open logfiles
  if(!CLog::Get().Init())return;

  //create a couple of singletons
  new CSettingsManager();
  new CKernel();

  //parse the 'settings.eng' file
  CSettingsManager::GetSingleton().ParseFile("settings.eng");
  
  //parse command-line arguments
  //skip the first argument, which is always the program name
  if(argc>1)
    for(int i=1;i<argc;i++)
      CSettingsManager::GetSingleton().ParseSetting(std::string(argv[i]));
  
  //set up the profiler with an output handler
  CProfileLogHandler profileLogHandler;
  CProfileSample::outputHandler=&profileLogHandler;

  //main game loop
  CKernel::GetSingleton().Execute();
  
  //clean up singletons
  delete CKernel::GetSingletonPtr();
  delete CSettingsManager::GetSingletonPtr();
}

Fairly self-explanatory. This is where many of the systems we've built up over the past articles tie together.

First, the logfiles. We want to have these available to us throughout the startup process, so that if something goes wrong and the game can't start at all, the logfiles are around for the user to find out why.

As soon as possible, we create the singletons - creating the CSettingsManager first is probably a good idea because the kernel may have some settings that should be in place before it gets constructed.

Next, we parse the 'settings.eng' file. This is totally optional, and the name is arbitrary, but you're probably going to need to load in a configuration file at some point, and now is as good a time as any. It's particularly useful when testing - you can set the screen mode so that you don't have to wait for the mode to change each time (and if you're on a multiple-monitor system, mess up your window layout ;-) ).

Then, the command-line arguments. We do these after settings.eng so that the command-line can 'override' the stored settings.

We set the profiler up to output to the logs (using our already-setup ProfileLogHandler). It's far from being the best output mechanism - ideally, we should be able to see stats on-screen while the game is running - but that's something we'll do later.

Then we start the main game loop itself (with CKernel::Execute()). Because we've not registered any tasks, this will return almost immediately.

Lastly we clean up our singletons.

If you build the project now, you should find that there are no unresolved dependencies, so it builds ok - running it will have the program start up and then exit. If you want to see for certain that it's running ok, add a log message in there (before CKernel::Execute(), probably). Let your mouth fall open in wonder and amazement; this is the blank slate of an engine upon which we build...



The Task Pool

Contents
  Entry Points
  The Task Pool
  Putting it all together

  Source code
  Printable version
  Discuss this article

The Series
  Part I
  Part II
  Part III
  Part IV
  Part V