Enginuity, Part 2
Memory Management, Error Logging, and Utility Classes;
or, How To Forget To Explode Your Underwear
Download the source code for this article here.
Hello! Welcome to part 2 of my silly-named series. This article we're going to cover the layout of the engine as a whole, and then move on to two of the most vital parts to an engine: Memory Management and Error Logging.
If you haven't sat down and look at any of the pre-packaged libraries I mentioned last time yet (SDL, SDL_Net, FMOD, and OpenGL), don't worry. We won't be going near them this time, with the exception of a cursory glance when I show you the design of the monster we're going to create.
Yup, that's it. Take a step back and reflect.
The first thing that should jump out at you is the grey bar down the right-hand side. That represents the Application itself - while the rest of the engine will stay the same between projects, the Application part changes - that's how each game is made to be different. The program starts at the App Entry Point (you may know it as main() or WinMain()), and passes control out to the Kernel (almost immediately). Later on, various calls are made back into the application itself, to request the specific bits of data that the engine works with.
Hopefully, you'll also see how it breaks down into some fairly obvious chunks. At the bottom is the double-outlined 'KERNEL' layer - that's our 'foundation layer,' and provides services to the rest of the engine. Next up is the 'task pool,' which contains the tasks to render the screen, update input devices, etc - and also, the 'Appstate' task. The AppState system (or 'Gestalt,' as I like to call it) allows you to switch the 'mode' that your program is in - for example, changing from being in-game to being in a menu would most likely be a change in application state. The AppState system calls back to the AppState factory in the application, allowing you to provide the states for the engine to use.
Next up is the CLIENT/SERVER system. Now, just to get something straight - Client/Server doesn't have to mean *network* Client/Server. Anything providing a 'service' is a Server, and anything using that 'service' is a Client - in truth, the relationship between the Kernel and the rest of the engine is a client/server relationship, in that the Kernel provides 'services' to the engine. In this particular situation, the Server in the C/S system provides a 'common gamestate' - so all clients using that server will effectively be 'in the same game.' Even for a single-player game, this works - it just means that the common gamestate is only being used by one client. The Server will be the point from which AI code is called, and game rules are checked up on - it doesn't make conceptual sense to have the client do this.
Having said all that, the C/S system *does* provide the network support in the engine. If the Client and Server in a game are on the same machine, then the network system picks it up and uses the LocalComm boxes (which move messages from A to B directly in system memory, rather than sending them out to the network drivers, round the loopback, and in again). If they aren't - that is, the player is joining a network game - then it uses the RemoteComm boxes to handle connections to other computers. Both RemoteComm and LocalComm can be used simultaneously (i.e. when hosting *and* playing on one machine).
The C/S boxes also handle common networking tasks - checking that a client is running the same version of the app as the server, enumerating games on the LAN, and so on. They do this through the use of 'messages,' which are simply a numerical code attached to a blob of data. As such, certain message codes are handled by the boxes themselves, but for everything else, there's 'handlers.' And you'll like this: you can pick a number for your message type (assuming it's not already in use), and register a 'handler' for it. That handler gets called whenever a message with that code is received - so you can register IDN_CHAT_MESSAGE to call the handler RecieveChatMessage() (which, clientside, would display the message or something, while serverside it might filter it for profanity or server commands). Because the message handlers are in the app itself, it's more or less totally extendable.
Finally, there's the gamestate itself. The gamestate (as you probably already know) is the blob of data that describes everything you could need to know about the game - not just things like player scores or elapsed time, but player positions or world collision data. It comes in two flavours - both of which are usually pretty similar - one for the client, and one for the server. The information that each part of the game needs to work with is often different (albeit, not by much). An example: in a multiplayer game, the Server will need information on all players, while the Client may only need information about it's own player. A more important example would be bots and AI - all a bot's AI variables should be stored as part of the game state, but there's no point sending that info to the client (as they'll only need the bot's position, say). In any case, you're responsible for creating each gamestate (through the Gamestate Factory), so it's more or less up to you.
Setting up the Build Environment
You're probably going to need some kind of coherent 'project' to keep all your files together (unless you're some kind of hardcore kernel hacker / masochist). I'm familiar with MSVC6 so that's what I'll use, but most of this is applicable to you if you use something else (such as Borland C++ Builder or dev-c++). And although this is all meant to be cross-platform, I'm going to assume we're building under Win32.
Firstly, creation of the project itself. The project type should be 'Win32 Application,' and should be an 'Empty Project.' MSVC generates the project files in the place you picked, and then we dive into the project settings: SDL demands that we use the multithreaded version of the runtime library (Project -> Settings -> C/C++ -> Category: Code Generation -> Use run-time library: Multithreaded DLL).
You also need to set up the linker to link the engine with the required libraries. Either using #pragma commands, or going through the 'Linker' tab in project settings, add sdl.lib, sdlmain.lib, opengl32.lib, glu32.lib, fmodvc.lib, and sdl_net.lib to the list. I assume you already set up the locations of the libraries to be included in the search path (Tools->Options->Directories), along with the include files?
The last thing I'd recommend is to set up the debugging environment a little; specifically, the working directory. Given that you're going to be working with both Debug and Release builds over time, and each build is going to share the same resources, you want to put those resources in a common place. It's also quite useful to keep assets separate from your code. I create a 'runtime' folder as a subdirectory of the project folder, and build up my 'install' of the project's assets in there; the debugger gets set up to use the 'runtime' folder as the working directory (Project->Settings->Debug->Working Directory).
Now that we've got that out of the way, let's move on to the first of our topics du jour - Memory Management.