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
|