Blog 14 min read

Inside the Unreal Engine 4.5 source code

Share this article
Inside the Unreal Engine 4.5 source code

The Unreal Engine is a game engine developed by Epic Games, first showcased in the 1998 first-person shooter game Unreal. Although primarily developed for first-person shooters, it has been successfully used in a variety of other genres, including stealth, MMORPGs, and other RPGs.

Its code is written in C++, and it’s used by many game developers today. Its source code is available on GitHub, and it’s free for students. Many amazing games were developed using this engine; it makes it possible to produce very realistic renderings like this one.

488-unreal-engine-4

What source code is executed behind the scenes to produce this realistic rendering?

It’s very interesting to look inside this powerful game engine and discover how it’s designed and implemented. C++ developers can learn many best practices from its codebase.

Let's explore its source code using CppDepend and CQLinq to detect some design and implementation choices of its development team.

1- Namespaces

Unreal Engine uses namespaces widely for three main reasons:

  • Many namespaces contain only enums, as shown by the following CQLinq query, which returns those containing only enums.
unreal2

In a large project, you are not guaranteed that two distinct enums don’t both use the same name. This issue was resolved in C++11 with enum class, which implicitly scopes the enum values within the enum’s name.

  • Anonymous namespace: a namespace with no name avoids making global static variables. The “anonymous” namespace you create is only accessible within the file you created it in. Here is the list of all the anonymous namespaces used:
unreal3
  • Modularizing the codebase: Let's search for all the other namespaces, i.e. neither the anonymous ones nor the ones containing only enums:
unreal6

Namespaces are a good solution to modularize the application; Unreal Engine defines more than 250 namespaces to enforce its modularity, which makes the code more readable and maintainable.

2- Paradigm used:

C++ is not just an object-oriented language. As Bjarne Stroustrup points out, “C++ is a multi-paradigmed language.” It supports many different styles of programs, or paradigms, and object-oriented programming is only one of these. Some of the others are procedural programming and generic programming.

2-1 Procedural Paradigm

2-1-1 Global functions

Let’s search for all global functions defined in the Unreal Engine source code:

unreal7

We can classify these functions into three categories:

1 - Utility functions: for example, 6,344 of them are Z_Construct_UXXX functions, which are used to create instances needed by the engine.

unreal8

2 - Operators: many operators are defined, as shown by the result of this CQLinq query:

unreal9

Almost all kinds of operators are implemented in the Unreal Engine source code.

3 - Functions related to the engine logic: many global functions containing engine logic are implemented. Maybe these kinds of functions could be grouped by category, as static methods in classes, or grouped in namespaces.

2-1-2 Static global functions

It is best practice  to declare a global function as static unless you have a specific need to call it from another source file.

unreal10

Many global functions are declared as static, and as specified before, other global functions are defined inside the anonymous namespaces.

2-1-3 Global functions that could be static

Global non-exported functions, not defined in an anonymous namespace and not used by any method outside the file where they are defined: these are good candidates to be refactored into static functions.

unreal65

As we can observe, some global functions are candidates to be made static.

2-2 Object-Oriented Paradigm

2-2-1 Inheritance

In object-oriented programming (OOP), inheritance is a way to establish an Is-a relationship between objects. It is often confused as a way to reuse existing code, which is not a good practice because inheritance for implementation reuse leads to tight coupling. Reusability of code is achieved through composition (composition over inheritance). Let’s search for all classes having at least one base class:

unreal13

And to have a better idea of the classes concerned by this query, we can use the Metric View.

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

  • Projects contain namespaces.
  • Namespaces contain types.
  • Types contain methods and fields.

The treemap view provides a useful way to represent the result of a CQLinq request; the blue rectangles represent this result, so we can visually see the types concerned by the request.

unreal12

As we can observe, inheritance is widely used in the Unreal Engine source code.

Multiple Inheritance: Let’s search for classes inheriting from more than one concrete class.

unreal15

Multiple inheritance is not widely used; only a few classes inherit from more than one class.

2-2-2 Virtual methods

Let's search for all virtual methods defined in the Unreal Engine source code:

unreal19

Many methods are virtual, and some of them are pure virtual:

unreal21

Like the procedural paradigm, the OOP paradigm is also widely used in the Unreal Engine source code. What about the generic programming paradigm?

2-3 Generic Programming

C++ provides unique abilities to express the ideas of Generic Programming through templates. Templates provide a form of parametric polymorphism that allows the expression of generic algorithms and data structures. The instantiation mechanism of C++ templates ensures that when a generic algorithm or data structure is used, a fully-optimized and specialized version will be created and tailored for that particular use, allowing generic algorithms to be as efficient as their non-generic counterparts.

2-3-1 Generic types

Let’s search for all generic types defined in the engine source code:

unreal23

Only a few types are defined as generic. Let’s search for generic methods:

unreal26

More than 40,000 methods are generic; they represent more than 25% of the methods implemented.

To summarize, the Unreal Engine source code mixes the three paradigms.

3- PODs to define the data model

In object-oriented programming,  plain old data (POD)  is a data structure that is represented only as passive collections of field values (instance variables), without using object-oriented features. In computer science, this is known as passive data structure

Let’s search for the POD types in the Unreal Engine source code.

unreal28

More than 2,000 types are defined as POD types; many of them are used to define the engine data model.

4- Gang of Four Design Patterns

Design Patterns are a software engineering concept describing recurring solutions to common problems in software design. Gang of four patterns are the most popular ones. Let's discover some of them used in the Unreal Engine source code.

4-1 Singleton

The singleton is the most popular and the most used one. Here are some singleton classes defined in the source code:

unreal29

TThreadSingleton is a special version of the singleton: only one instance is created for each thread. Calling its Get() method is thread-safe.

4-2 Factory

Using factories is interesting to isolate the instantiation logic and enforce cohesion; here is the list of factories defined in the source code:

unreal30

And here's the list of the abstract ones:

unreal31

4-3 Observer

The observer pattern is a software design pattern in which an object maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

There are some observers implemented in its source code; FAIMessageObserver is one of them.

Here's a dependency graph to show the call of the OnMessage method of this observer:

unreal70

4-4 Command

The command pattern is a behavioral design pattern in which an object is used to represent and encapsulate all the information needed to call a method at a later time.

Four terms always associated with the command pattern are command, receiver, invoker and client. A command object has a receiver object and invokes a method of the receiver in a way that is specific to that receiver's class.

Here are, for example, all the commands inheriting from the IAutomationLatentCommand:

unreal33

5- Coupling and Cohesion

5. 1 Coupling

Low coupling is desirable because a change in one area of an application will require fewer changes throughout the entire application. In the long run, this could save a great deal of time, effort, and cost associated with modifying and adding new features to an application.

Low coupling can be achieved by using abstract classes or using generic types and methods.

Let’s search for all abstract classes defined in the Unreal Engine source code:

unreal34

Only a few types are declared as abstract. Low coupling is better enforced by using generic types and generic methods.

Here are, for example, the methods using at least one generic method:

unreal27

As we can observe, many methods use generic methods; low coupling is enforced by the function template parameters. Indeed, the real type of these parameters can change without changing the source code of the called method.

Cohesion

The single responsibility principle states that a class should not have more than one reason to change. Such a class is said to be cohesive. A high LCOM value generally pinpoints a poorly cohesive class. There are several LCOM metrics. The LCOM takes its values in the range [0-1]. The LCOM HS (HS stands for Henderson-Sellers) takes its values in the range [0-2]. An LCOM HS value higher than 1 should be considered alarming. Here are  to compute LCOM metrics:

LCOM = 1 – (sum(MF)/M*F) LCOM HS = (M – sum(MF)/F)(M-1)

Where:

  • M is the number of methods in class (both static and instance methods are counted, it includes also constructors, properties getters/setters, events add/remove methods).
  • F is the number of instance fields in the class.
  • MF is the number of methods of the class accessing a particular instance field.
  • Sum(MF) is the sum of MF over all instance fields of the class.

The underlying idea behind these formulas can be stated as follows: a class is utterly cohesive if all its methods use all its instance fields, which means that sum(MF)=M*F, and then LCOM = 0 and LCOMHS = 0.

An LCOMHS value higher than 1 should be considered alarming.

unreal36

Only a few types are considered non-cohesive.

6- Immutability, Purity, and Side Effects

6-1 Immutable types

Basically, an object is immutable if its state doesn’t change once the object has been created. Consequently, a class is immutable if its instances are immutable.

There is one important argument in favor of using immutable objects: it dramatically simplifies concurrent programming. Think about it — why is writing proper multithreaded code a hard task? Because it is hard to synchronize threads’ access to resources (objects or other OS resources). Why is it hard to synchronize these accesses? Because it is hard to guarantee that there won’t be race conditions between the multiple write and read accesses made 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? Then there is no more need for synchronization!

Another benefit of immutable classes is that they can never violate the LSP (Liskov Substitution Principle); here’s a definition of LSP quoted from its wiki page:

Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).

Here's the list of immutable types defined in the source code:

unreal38

6-2 Purity and side effects

The primary benefit of immutable types comes from the fact that they eliminate side effects. I couldn’t say it better than Wes Dyer, so I quote him:

We all know that generally it is not a good idea to use global variables.  This is basically the extreme of exposing side-effects (the global scope). Many of the programmers who don’t use global variables don’t realize that the same principles apply to fields, properties, parameters, and variables on a more limited scale: don’t mutate them unless you have a good reason.(…)One way to increase the reliability of a unit is to eliminate the side-effects. This makes composing and integrating units together much easier and more robust.  Since they are side-effect free, they always work the same no matter the environment.  This is called referential transparency. Writing your functions/methods without side effects - so they're pure functions, i.e. they do not mutate the object - makes it easier to reason about the correctness of your program.

Here’s the list of all the methods without side effects.

unreal41

More than 125,000 methods are pure.

7- Implementation quality

7-1 Methods that are too big

Methods with a large number of lines of code are not easy to maintain and understand. Let’s search for methods with more than 60 lines.

unreal44

The Unreal Engine source code contains more than 150,000 methods, so less than 1% could be considered too big.

7-2 Methods with many parameters

unreal45

Few methods have more than 8 parameters; most of them are generic to avoid defining variadic functions, as in the case of the TCString::Snprintf methods.

7-3 Methods with many local variables

unreal46

Less than 1% have many local variables.

7-4 Methods too complex

Many metrics exist to detect complex functions; NBLinesOfCode, number of parameters and number of local variables are the basic ones.

There are other interesting metrics to detect complex functions:

  • Cyclomatic complexity is a popular procedural software metric equal to the number of decisions that can be taken in a procedure.
  • Nesting Depth is a metric defined on methods that is relative to the maximum depth of the most nested scope in a method body.
  • Max Nested Loop is equal to the maximum level of loop nesting in a function.

The maximum value tolerated for these metrics depends mostly on the team’s choices; there are no standard values.

Let’s search for methods that could be considered as complex in the Unreal Engine codebase.

unreal49

Only 1.5% are candidates to be refactored to minimize their complexity.

7-5 Halstead complexity

Halstead complexity measures are software metrics introduced by Maurice Howard Halstead in 1977. Halstead made the observation that metrics of the software should reflect the implementation or expression of algorithms in different languages, but be independent of their execution on a specific platform. These metrics are therefore computed statically from the code.

Many metrics were introduced by Halstead; let’s take as an example the TimeToImplement one, which represents the time required to program a method, in seconds.

unreal50

1,748 methods require more than one hour to be implemented.

8- RTTI

RTTI refers to the ability of the system to report on the dynamic type of an object and to provide information about that type at runtime (as opposed to at compile time). However, RTTI has become controversial within the C++ community. Many C++ developers choose not to use this mechanism.

What about the Unreal Engine developer team?

unreal60

No method uses the dynamic_cast keyword; the Unreal Engine team chose not to use the RTTI mechanism.

9- Exceptions

Exception handling is also another controversial C++ feature. Many known open source C++ projects do not use it.

Let’s search whether an exception is thrown anywhere in the Unreal Engine source code.

unreal62

Exceptions are thrown in some methods; let’s take the RaiseException one as an example:

unreal61

As specified in their comments, exceptions could be generated for the header tool, but in normal runtime code they don’t support exception handling.

10- Some statistics

10-1 Most popular types

It’s interesting to know the most used types in a project; indeed, these types must be well designed, implemented and tested. And any change to them could impact the whole project.

We can find them using the TypesUsingMe metric:

unreal71

However, there’s another interesting metric to search for popular types: TypeRank.

TypeRank values are computed by applying the Google PageRank algorithm on the graph of types’ dependencies. A homothety of center 0.15 is applied to make it so that the average of TypeRank is 1.

Types with high TypeRank should be more carefully tested because bugs in such types will likely be more catastrophic.

Here’s the result of all popular types according to the TypeRank metric:

unreal52

10-2 Most popular methods

unreal54

10-3 Methods calling many other methods

It’s interesting to know the methods using many other ones; it could reveal a design problem in these methods, and in some cases a refactoring is needed to make them more readable and maintainable.

unreal57
Share this article