Coding in the SDK

From The DarkMod Wiki
Jump to navigationJump to search

...also known as greebo's random notes about coding for The Dark Mod.

This article is constant WIP. When coding in the SDK, there are thousands of little lessons to be learned - some of them include things I wish I would have known earlier, some might be common knowledge. Anyway, I plan include all the miscellaneous things in this article.

General

When the id people designed Doom 3, they roughly divided their code into two parts: the engine and the gameplay code. The engine code is closed source, i.e. we don't have the code, only the compiled binary in the form of DOOM3.exe.

The engine contains all the "important" stuff which id software is making money with, by licensing it to third party companies. This is where the real brainpower is in, so we don't get that part (yet - John Carmack repeatedly stated that he wants to release all the source under the GPL soon).

The gameplay code is fully available. This includes the entities, game logic, AI, physics, weapon and animation code. This leaves a whole lot of headroom for modders like you and me, and we probably should thank id software for that. There are all kinds of mods out there which change the gameplay code to create their own game so to say. Some of them are "mini-mods" (introducing a new weapon or double-jump-mods), some of them are larger, aiming to change the nature of the game and finally there's The Dark Mod, which changed almost everything that can be changed in the first place. The term Total Conversion comes close - when playing TDM, you won't believe it's the same game as Doom 3.

What parts of the code are inaccessible?

All of the code id are earning money with by licensing their engine to others. I don't know all the parts, but these are the ones from the top of my head:

  • the rendering engine
  • the sound engine
  • the collision model manager
  • the Map and AAS compilers
  • the declaration manager (although we can implement our own parsers)
  • the network system
  • the keyboard and mouse handlers
  • anything platform-specific
  • the command system
  • the virtual file system
  • the CVAR system plus some special CVARs
  • the editor (D3Ed), but we have DarkRadiant

What parts of the code are left for us to change?

A whole lot, and in most cases it is sufficient:

  • gameplay code (entities, logic)
  • physics
  • AI
  • custom declarations
  • custom CVARs
  • the scripting system
  • the animation code
  • plus everything outside the SDK like scripts, defs, materials and models.

Coding Style

This deserves its own article: TDM Coding Style

It's easy to see that the gameplay code has been written by a number of different programmers at id software. It's also easy to see that several of them have strong roots in C, whereas others have used C++ coding paradigms more strongly. At any rate, almost all of those programmers need a heads up that the possibility of placing comments in the code has not been removed from C++ - only a few of them seem to know about comments at all, which is a shame, especially when it comes to the animation code.

Another thing that a newcomer might notice is the mere absence of STL or other standard stuff in the id code. I'm not in the position of judging whether this is actually necessary (which I doubt), but it seems that id rewrote every little low-level library on their own, including strings, lists, hashtables, vectors, etc. Some of the classes are admittedly quite handy, but sometimes it's clear that they did it just because they could. Overall, there is strong NIH smell all about the place, but the code is there and stable already, so we might as well use it for TDM coding.

Something to keep in mind is also the custom memory manager, which (according the comments in the code) is multiple times faster than the one provided by the CRT. As a consequence of this, everything that is allocated in the game DLL must be freed again before the DLL is unloaded, otherwise you'll be running into weird crashes at shutdown. Most classes have Clear() methods in them, and whenever you write custom (global or standalone) classes you need to make sure that these are destroyed in idGameLocal::Shutdown() at the latest.

id software has been using the following coding style:

  • class member variables are lowercase, plain notation (without the m_bVar hungarian stuff)
  • class names start with the "id" prefix
  • class methods are uppercase with mixed case, like this: idDict::MatchPrefix()
  • One True Brace Style, i.e. the opening { brace is in the same line as the if ().

STL vs. idLib

The most common idLib member is the idList<> template, which is the counter-part of std::vector<>. Whenever you need a variable-sized array, use idList. This class has some nice convenience functions and supports the definition of granularity, which allows you to specify the minimum size of memory blocks which are allocated by idList. This is useful to prevent idList from calling the new operator each time you push an element into that list.

Another popular member is idStr. This is mostly equivalent with std::string. Both classes provide the well-known c_str() method which can be used to retrieve the const char* pointer from the object. Two gotchas about idStr:

  • void idStr::Empty() is an imperative method, i.e. it clears the string. Do not confuse this with bool std::string::empty(), which returns true when the string is empty. If you want to check whether an idStr is empty, use idStr::IsEmpty()...
  • idStr has an operator cast to const char*, so you can pass idStr to everything that expects a const char*. Where's the gotcha? Don't expect this to work when passing idStr to functions with variable-sized argument lists, like this
idStr mystr = "test";
idGameLocal::Printf("String is %s", mystr); // this will crash!

The compiler cannot know which operator cast to call for this type of function call, so use the c_str() function instead:

idStr mystr = "test";
idGameLocal::Printf("String is %s", mystr.c_str());

There are more container classes in idLib:

  • idLinkList, a bit similar to std::list - I don't use that much, because it's inconvenient.
  • idHashTable, ditto
  • idDict - this one is useful. The most common use for this is to contain all the spawnargs of an entity.
  • idKeyValue - used to define a spawnarg pair
  • idStrList - just a shortcut for idList<idStr>

Another thing I had to learn the hard way was using idStr in combination with std::map. Due to the built-in operator cast to const char*, an idStr cannot be used as index in a std::map. The std::map will try to sort the given idStr into the correct place, but the comparison operator is actually invoking the idStr::operator const char*() const which traps the std::map into comparing pointers instead of strings. If you want to use std::map, use std::map<std::string, someothertype>.

Performance Considerations

The code might not win a beauty contest, but most of id's programmers know the ins and outs of how to code a performing game, no doubt about that.

Why boost? Why not boost?

The codebase is so friggin' huge!