Jump to content

  • Log In with Google      Sign In   
  • Create Account

Banner advertising on our site currently available from just $5!


1. Learn about the promo. 2. Sign up for GDNet+. 3. Set up your advert!


This article is under review by the community - Current moderation totals:
Mark as peer reviewed: 1 votes (Servant of the Lord)
Still needs work: 1 votes (Dave Hunt)

Editor Feedback:
  • Unclear or Incomplete


Like
9Likes
Dislike

Doom3 is the proof that "keep it simple" works. UNDER REVIEW

By CoderGears Team | Published Mar 31 2015 09:46 PM in Game Programming

c++ doom3 designpattern

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.



License


GDOL (Gamedev.net Open License)




Comments

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.

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.

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.

 

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.

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.

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.


Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS