Doom3 is the Proof that "Keep it Simple" Works

Published May 17, 2012 by CoderGears Team, posted by Lahlali Adnane
Do you see issues with this article? Let us know.
Advertisement

If you search on the web for the best C++ source code. The Doom3 source code is mentioned many times, with testimonials like this one.

I spent a bit of time going through the Doom3 source code. It's probably the cleanest and nicest looking code I've ever seen.

Doom 3 is a video game developed by id Software and published by Activision.The game was a commercial success for id Software; with more than 3.5 million copies of the game were sold. On November 23, 2011 id Software maintained the tradition and it released the source code of their previous engine. This source code was reviewed by many developers, here's as example the feedback from fabien (orginal source):

Doom 3 BFG is written in C++, a language so vast that it can be used to generate great code but also abominations that will make your eyes bleed. Fortunately id Software settled for a C++ subset close to "C with Classes" which flows down the brain with little resistance:

  • No exceptions.
  • No References (use pointers).
  • Minimal usage of templates.
  • Const everywhere.
  • Classes.
  • Polymorphism.
  • Inheritance.

Many C++ experts don't recommend any more the "C with classes" approach. However, Doom3 was developed between 2000 and 2004, what could explain the no use of modern C++ mechanisms. Let's go inside its source code using CppDepend and discover what makes it so special. Doom3 is modularized using few projects, here's the list of its projects, and some statistics about their types:

doom5.png

And here's the dependency graph to show the relation between them:

doom2.png

Doom3 defines many global functions. However, most of the treatments are implemented in classes. The data model is defined using structs. To have a concrete idea of using structs in the source code, the metric view above shows them as blue rectangles.

In the Metric View, the code base is represented through a Treemap. Treemapping is a method for displaying tree-structured data by using nested rectangles. The tree structure used is the usual code hierarchy:

  • Project contains namespaces.
  • Namespace contains types.
  • Type contains methods and fields.
doom13.png

As we can observe many structs are defined, for example more than 40% of DoomDLL types are structs. They are systematically used to define the data model. This practice is adopted by many projects, this approach has a big drawback in case of multithreaded applications. Indeed, structs with public fields are not immutable.

There is one important argument in favor of using immutable objects: It dramatically simplifies concurrent programming. Think about it, why does writing proper multithreaded programming is a hard task? Because it is hard to synchronize threads access to resources (objects or others OS resources). Why it is hard to synchronize these accesses? Because it is hard to guarantee that there won't be race conditions between the multiple write accesses and read accesses done by multiple threads on multiple objects. What if there are no more write accesses? In other words, what if the state of the objects accessed by threads, doesn't change? There is no more need for synchronization! Let's search for classes having at least one base class:

doom6.png

Almost 40% of stucts and classes have a base class. And generally in OOP one of the benefits of inheritance is the polymorphism, here are in blue the virtual methods defined in the source code:

doom7.png

More than 30% of methods are virtual. Few of them are virtual pure and here's the list of all abstract classes defined:

doom9.png

Only 52 are defined abstract classes, 35 of them are defined as pure interfaces,i.e. all their virtual methods are pure.

doom8.png

Let's search for methods using RTTI

doom17.png

Only very few methods use RTTI. To resume only basic concepts of OOP are used, no advanced design patterns used, no overuse of interfaces and abstract classes, limited use of RTTI and data are defined as structs. Until now nothing special differentiate this code from many others using "C with Classes" and criticized by many C++ developers. Here are some interesting choices of their developers to help us understand its secret: 1 - Provides a common base class with useful services. Many classes inherits from the idClass:

doom10.png

The idClass provides the following services:

  1. Instance creation.
  2. Type info management.
  3. Event management.
doom11.png

2- Make easy the string manipulation

Generally the string is the most used type in a project, many treatments are done using them, and we need functions to manipulate them. Doom3 defines the idstr class which contains almost all useful methods to manipulate strings, no need to define your own method as the case of many string classes provided by other frameworks.

3- The source code is highly decoupled with the GUI framework (MFC)

In many projects using MFC, the code is highly coupled with their types, and you can find types from MFC everywhere in the code. In Doom3, The code is highly decoupled with MFC, only GUI classes has direct dependency with it. As shown by this following CQLinq query:

doom3.png

This choice has a big impact on the productivity. Indeed, only the Gui developers must care about the MFC framework, and for the other developers it's not mandatory to waste time with MFC.

4- It provides a very good utility library (idlib)

In almost all projects the most used types are utility classes, as shown by the result of this following query:

doom4.png

As we can observe the most used are utilities ones. If C++ developers don't use a good framework for utilities, they spend most of their developement time to fight with the technical layer. idlib provides useful classes with all needed methods to treat string, containers, and memory. Which facilitate the work of developers and let them focus more on the game logic.

5- The implementation is very easy to understand

Doom3 implements a hard coded compiler, and as known by C++ developers, it's not an easy task to develop parsers and compilers. However, the implementation of the Doom3 is very easy to understand and its code is very clean. Here's the dependency graph of the classes used by the compiler:

doom16.png

And here's a code snippet from the compiler source code:

doom15.png

We already study the code source of many parsers and compiler. But it's the first time we discover a compiler with a source code very easy to be understood, it's the same for the whole Doom3 source code. It's magic. When we explore the Doom3 source code, we can't say: WOW it's beautiful!

Summary

Even if the Doom3 design choices are very basic, but its designers make many decisions to let developers focus more on the game logic, and facilitate all the technical layer stuff. Which increase a lot the productivity. However when using "C with Classes", you have to know exactly what you are doing. You have to be expert like Doom3 developers. It's not recommended for a beginner to take risk and ignore the Modern C++ recommendations.

Cancel Save
0 Likes 15 Comments

Comments

snake5

It's not recommended for a beginner to take risk and ignore the Modern C++ recommendations.


Are you saying that it's not recommended for a beginner to write code that is easier to fully comprehend? I would say that it should be the opposite, otherwise, if just hiding behind abstractions, there's little hope for ever becoming an expert.

And regardless of the coding style, I don't think a beginner could take on a project like this anyway. Obviously, one has to be some kind of expert to work on a game like this. Whether you use 'auto', initializer lists or lambdas or not isn't going to change that.

April 01, 2015 08:07 AM
maxest

In my opinion, this article contains too many opinions, many of them I find invalid (and yes, that is also [my] opinion ;)).


Doom3 implements a hard coded compiler, and as known by C++ developers, it's not an easy task to develop parsers and compilers

Probably not easy but with tools like flex and bison not that hard either.


Until now nothing special differentiate this code from many others using "C with Classes" and criticized by many C++ developers.

Which developers? Some reference to some C++ (game) veteran, maybe? Because I think that the less "expert C++" stuff used the better. In fact, I'm surprised that Doom 3 relies so heavily on stuff like abstract classes and even has such nasty base classes like idClass. But, after all, that's probably Carmack's choice and he is a credible person so it probably worked well for him.

April 01, 2015 09:39 AM
ericrrichards22

This could really benefit from an editing pass to clean up the language, there are a number of places where it appears to be trying to say one thing, inferred from the context, but the actual words in the sentence say the opposite.

April 01, 2015 01:47 PM
Punika

As we can observe many structs are defined, for example more than 40% of DoomDLL types are structs. They are systematically used to define the data model. This practice is adopted by many projects, this approach has a big drawback in case of multithreaded applications. Indeed, structs with public fields are not immutable.


There is one important argument in favor of using immutable objects: It dramatically simplifies concurrent programming. Think about it, why does writing proper multithreaded programming is a hard task? Because it is hard to synchronize threads access to resources (objects or others OS resources). Why it is hard to synchronize these accesses? Because it is hard to guarantee that there won’t be race conditions between the multiple write accesses and read accesses done by multiple threads on multiple objects. What if there are no more write accesses? In other words, what if the state of the objects accessed by threads, doesn’t change? There is no more need for synchronization!

I don't see why "Classes" help here? Ok you can protect your member variables from access and then add sync primitves into your member functions. But this is a bad way to do concurrent programming. So a better way would be to divide your data to "threads, work, task" -> you name it.

This has been done on the render code i think, and as you can see this can be done equally in C too.

April 01, 2015 02:16 PM
ChrisChiu

I disagree with a few implications here. For instance, in this article inheritance is seen as something good, and virtual functions too. But nowadays, virtual functions are frowned upon because they can actually become a performance bottleneck (some platforms such as PS3 are really bad at virtual functions and it's best to avoid them if possible). Favor composition over inheritance. Use polymorphism only when really needed.

Virtual functions also seduce programmers to code in the OOP-style, which often leads to code that is not very data oriented (with lots of cache misses, and general memory layout not optimal for cache utilization - which is bad because while both CPU and Memory performance have increased in the past 10 years, the CPU speeds have increased by tenfold compared to memory access speeds, so relative to the CPU, memory has actually become slower - so cache utilization is more important than ever before). What I'm saying is, with virtual functions and classes deriving from one base class, you are tempted to do an update loop like "for each object: object.DoStuff()", with "DoStuff()" being virtual. This is essentially a beginner's mistake nowadays. Especially for entity/game object stuff, one should favour structs of arrays (SoA) rather than arrays of structs (AoS).

Doom 3's code is certainly good for its time and the kind of machines from back then, and good readability/understandability is ALWAYS a plus, but I don't think one should design code in the same way nowadays.

April 01, 2015 02:48 PM
Servant of the Lord

This seems to be almost a defense of "C with Classes" as a justification of our own poor habits because an expert knowingly chose to use similar habits.

It's seems to be saying, "This one expert I saw made something really awesome and he only used X and Y. Therefore it must mean that only using X and Y is the expert thing to do."

Or to put it another way, "I was watching professional racecar drivers, and in the middle of a race, they never used the car brakes! Therefore, not using brakes while driving my car to work must be the professional."

The article also seems to contradict itself:

To resume only basic concepts of OOP are used, no advanced design patterns used, no overuse of interfaces and abstract classes,
But a few sentences before, it says:
Almost 40% of structs and classes have a base class. And generally in OOP one of the benefits of inheritance is the polymorphism, here are in blue the virtual methods defined in the source code:
More than 30% of methods are virtual. Few of them are virtual pure and here's the list of all abstract classes defined:

If 40% of all the structs and classes have a base class, and 30% of the base class methods are virtual, that sounds exactly like "overuse" of of interfaces and abstract classes.

Now, I don't agree that all abstract classes are bad (only overuse of it), but this article says one thing and then contradicts itself a short while later.

To resume only basic concepts of OOP are used, no advanced design patterns used

Design patterns shouldn't ever be "used", they are ways of describing existing code. I'm too lazy to do so, but I bet if I went through Doom 3's sourcecode, dozens of design patterns would be found.

They just wouldn't be labeled, "cFactoryDesignPrototypePatternInterface". A design pattern is a description of what a class is, but variable names describe what classes are for.

Also, avoiding the use of references and only using pointers? That's laziness (or, at best, an intentional sacrificing of correctness for consistency - even that's stretching it), not some skilled improvement or wise decision. Unless for some reason references weren't available to them, in which case the lack of usage is still not a plus, but working within limitations.

April 01, 2015 04:52 PM
Ragnarork

The "No references" part seems strange to me as well. I don't see how it could be of an advantage here, as they are quite easily understandable (which would be consistent with the "clean and readable codebase"), and do not add any overhead compared to a pointer.

Also, I quite fail to see what represents the "Advanced C++ features" they didn't use, as this was developed in C++98 (not that C++03 adds much to the standard), which was basically what they used plus exceptions.

Same remark as well for the "Many C++ experts don't recommend anymore the _C with Classes_". Which experts? And how can they speak generally for many domain with different needs/constraints? I bet many game studios (based on echoes I have from a few ones) still want to avoid as much as possible virtual functions and exceptions.

April 02, 2015 08:52 AM
c6burns

This article would be much more valuable as a study of how C++ programming for maximum performance in games has changed over the last decade. Instead it reads like "here's what I found out from one evening with doom3 and cppdepend" without much context beyond.

April 02, 2015 11:30 AM
n3Xus

I like this article.

In the past I personally always started mixing up references and pointers and ended up with a mess where once there was a pointer and once there was a reference.

Since the only real difference between references and pointers is that references can't be NULL we have a question: should you use references only when you want to explicitly tell "this must NOT be a null pointer"? You obviously can't use just references since you may need NULL pointers.

But if you start mixing up references and pointers you sometimes have to write '&x' and sometimes just 'x' (if x is a pointer), which also looks strange in the code, eg is myFunction(&x) now a double pointer or a dereferenced pointer because the function takes a reference?

So now I ask myself: should the name of the function paramater tell you if the param can be nulled or not?

Eg: myFunc(int* Car) vs myFunc(int* CarOptional)?

(ofcourse you could also write is as 'myFunc(int* CarOptional=null)' to make it really clear).

Here we ask ourselfs: if we know exactly what 'myFunc(...)' does then we don't need to put "nullable" in the paremter name, since we know how it will behave. So I personaly concluded that writing "optional" is useless in the parameters name, because I should know exactly what each parameter does and if it can be NULL, plus I can write CarOptional=null.

So anyway I came to the conclusion that using just pointers is more clear, you know that if something is a pointer you can mess up a larger global state. If you have a reference and access it with myObj.something you may think it's a local stack variable but in reality you just messed up a some global state because myObj was a reference.

TL/DR: I like their "keep it simple, stupid" approach. I find most of the new C++ features too arcane or too much academical. In practice many of these thing make your eyes bleed and make the code look too complitcated.

April 02, 2015 01:49 PM
ferrous

I kind of like references myself, I get tired of seeing a bunch of assert(pWhatevers) or if(pFoo == null) doErrorHandling; type stuff. With a reference, you can turn a runtime check into a design time check, and the higher up you can push up those kind of things, the better.

That said, I dislike that when I'm looking at a function being called that takes references. With a reference, I don't know if it's being modified or not. ie ModifyFoo(foo); You can counter it a little by sticking "/*ref*/" comments, sort of like C#, which explicitly requires ref or out keywords on function calls, but it's not quite the same.

April 02, 2015 09:32 PM
Servant of the Lord

But if you start mixing up references and pointers you sometimes have to write '&x' and sometimes just 'x' (if x is a pointer), which also looks strange in the code, eg is myFunction(&x) now a double pointer or a dereferenced pointer because the function takes a reference?


That said, I dislike that when I'm looking at a function being called that takes references. With a reference, I don't know if it's being modified or not. ie ModifyFoo(foo);


Yes, this is the only downside of references. I wish references required & notation as well (except for const-ref...).

To be fair, pointers also suffer from this problem, if you have 'ModifyFoo(foo);', with ModifyFoo taking a pointer and 'foo' is also a pointer.

So yea, it'd be nice if C++ as a whole enforced some kind of In / Out / InOut syntax using symbols (i.e. '&' always means Out or Inout, and the absence of it always means 'In' only).
April 02, 2015 10:07 PM
Servant of the Lord

since the only real difference between references and pointers is that references can't be NULL


Another difference is that pointers can be re-assigned, whereas references are set for life. This is an important difference.

A third important difference is that references as parameters can silently be references, instead of full copies.

I dislike that writable (non-const) references are silent, but I love read-only (const) references being silent.

we have a question: should you use references only when you want to explicitly tell "this must NOT be a null pointer"? You obviously can't use just references since you may need NULL pointers.


Well, if you used an 'optional' type wrapper, and were consistent in doing so, it'd be more clear (from outside the function), and still provide compile-time checking instead of run-time checking.


DoSomething(value, ref(variable), nullptr);

DoSomething(int blah, ref<int> foo, optional_ref<int> bar);

You'd be able to enforce optionality as well as see in/out semantics from the call-point. I'd just want it to be consistently used across the codebase.

So I personaly concluded that writing "optional" is useless in the parameters name, because I should know exactly what each parameter does and if it can be NULL, plus I can write CarOptional=null.


Your 'plan' is memorizing what each function parameter does in your entire codebase? I know what functions are for about six months after I wrote them, and I only know their parameters for about a week, unless I use them regularily. When I need to use or modify code a year later, I don't have them memorized.

So anyway I came to the conclusion that using just pointers is more clear, you know that if something is a pointer you can mess up a larger global state.

If you have a reference and access it with myObj.something you may think it's a local stack variable but in reality you just messed up a some global state because myObj was a reference.


In theory, yes, in practice I find this to never be the case in my own programming adventurers. You're talking about being inside a function. If I'm inside a function, I know what the parameter types are - they're just twenty lines higher up the page. The only time I wouldn't know what they are, is if I had a monstrously convoluted 300+ line function... and if that's the case, my problem is my very large and messy function and not the use of references.

TL/DR: I like their "keep it simple, stupid" approach.
I find most of the new C++ features too arcane or too much academical.

I'm not an intellectual, but once I got used to them, I found many of them to be very useful in some circumstances, and not useful in others. Almost any C++ feature, new or old, can be abused or overused. We should avoid creating 'rules' that say feature X is evil, or feature Y is dangerous.

I'm a big fan of simplicity myself, and introduced to many of the new C++ features, I got confused and worried that they were too complicated ("perfectly forward the universal reference as an rvalue using move semantics"... What? Makes me want to 'move semantics' my fist into someone's face tongue.png). After pressing through the initial confusion (About a week worth of headaches and confusion of focused trying to understand them), I found them easy to understand and use.

In practice many of these thing make your eyes bleed and make the code look too complitcated.

But, if you find them arcane and don't use them, how do you know what they are like "in practice"?
Yes, their abuses can make your eyes bleed - just like abuse of inheritance or pointer arithmetic or abusive casting can do. But properly leveraged, the non-abusive uses can also make your code simpler, and also save alot of work.

You also have to define what you mean by 'old' and 'new'. Do 'templates' fall under the 'old' or the 'new'? Functors? for-range? Smart pointers?

These can make code cleaner and safer for the call, but they can also be abused and make things more complicated and ugly.

Anything used badly is bad. Some features more frequently lean towards to being abused... but until you learn them and use them enough, we don't have the experience to declare a feature as 'bad'. All we can say is, "I have several times seen people abusing them".

April 02, 2015 10:41 PM
Brain
I recommend moving the images on site rather than remotely linking them from your Blog. What if you change software or your domain, or your site goes down? The article is heavily reliant on images to get the message across so this should be fixed. I left a feedback note but wasn't able to point this out as my actual reason for rejection, if you can please fix this it would go some way to me reverting that vote. Thanks for taking the time to write the article, I enjoyed reading it!
April 03, 2015 11:43 PM
Brain

So yea, it'd be nice if C++ as a whole enforced some kind of In / Out / InOut syntax using symbols (i.e. '&' always means Out or Inout, and the absence of it always means 'In' only).


Unfortunately I've even seen this abused. Have you ever seen a function that just takes a const non-reference, non pointer int, then proceeds to modify an array entry in a global array, using that as an index value? I have and it sucks when it doesn't say that the function will do that... Ugh.
April 03, 2015 11:49 PM
Servant of the Lord

Unfortunately I've even seen this abused. Have you ever seen a function that just takes a const non-reference, non pointer int, then proceeds to modify an array entry in a global array, using that as an index value? I have and it sucks when it doesn't say that the function will do that... Ugh.

Oh wow, that would be terrible. Somewhat related, I occasionally use 'mutable', very rarely, but I have to make sure that I'm not violating the 'concept' of const even when violating the actual language rules of const

Usually my mutables are for implementation-detail hidden optimizations - but even that can be a crumbling bridge to walk on, especially if it accidentally makes calling the same function twice do slightly different things from the perspective of the caller.

The safest and easiest rule, is "const means const means const - under penalty of death", which is what I usually try to adhere to, trying to err on the side of making something non-const rather than making something improperly const.

April 04, 2015 05:48 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

A look at the source code from popular iD title Doom 3.

Advertisement
Advertisement

Other Tutorials by Lahlali Adnane

Lahlali Adnane has not posted any other tutorials. Encourage them to write more!
Advertisement