Building and Project Organization

To build the engine, you can download it or browse the source on Github. Inside the Source directory, you will find a number of directories:

  • Engine: source files for the engine itself, broken out by module type (base, scripting, rendering, etc.).
  • Game: source files for the “stub” game executable which loads the files saved by the editor into a playable copy of the game.
  • Meta: the metadata generator used in building our game engine’s scripting and serialization modules
  • Server: source files for the server in a client-server networked game.
  • ThirdParty: source files for all of our third-party libraries.
  • Workspace: workspace and solution files for Xcode and Visual studio respectively.

You’ll also find executables, resources the editor uses, and tools.

Building Cross-Platform

In the Source/Engine folder, you’ll find the main giga-engine.h. This contains the most basic includes we’ll need for our engine. While I have tried to keep the number of #ifdef WIN32 preprocessor statements to near zero, it’s not quite there yet so you’ll see one of those at the top of this file:

1
2
3
4
5
6
7
#ifdef WIN32
// Windows definitions - NOMINMAX defaults to STL; WIN32_LEAN_AND_MEAN minimizes Windows' includes
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN         1
 
#include 
#endif

If you’ve ever worked with Windows much before, you’ll probably be familiar with WIN32_LEAN_AND_MEAN. This makes the Window’s header files slightly smaller. Also, defining NOMINMAX on Windows tosses the Windows definitions and leaves them to STL.

Another oddity you will see throughout the engine that you’ll be familiar with if you’ve worked with Windows is the definition of GIGA_API:

1
2
3
4
5
6
7
8
9
10
// Windows DLL export definition
#ifndef GIGA_API
	#ifdef GIGA_EXPORTS
	// We are building this library
		#define GIGA_API __declspec(dllexport)
	#else
	// We are using this library
		#define GIGA_API __declspec(dllimport)
	#endif
#endif

On macOS, this is defined as blank in our project files “GIGA_API=”. On Windows, this tells the compiler whether to export or import the symbols from the files for dynamically loaded libraries. Since we build our engine as a DLL on Windows, these are required. If you build the engine as a static library, these definitions can also be blanked out on Windows.

Next, you’ll find a number of standard system includes available on both platforms and the STL library headers:

1
2
3
4
5
6
7
8
9
// STL
#include <vector>
#include <map>
#include <string>
#include <algorithm>
#include <thread>
#include <mutex>
#include <typeinfo>
#include <memory>

We make fairly extensive use of STL throughout the engine. In the olden days, this would have been frowned upon, since the Standard Template Library was at least considered to be (if not actually) slower than rolling a more optimized version of basic containers. As a result, you’ll see many engines like Unreal have their own containers. However, if there is a performance hit now, it is negligible compared to the effort expended writing and maintaining your own code for these pieces.

And that’s it. Each module will also have it’s own basic header which will include relevant third-party libraries.

Welcome

So you want to build a 3D game engine? Well, you’ve come to the right place. Here, you will learn about game engine architecture, objects, scripting, rendering, networking, physics, IO, audio, building an editor, and putting it all together into a game (or at least a series of demos). We will do both a deep dive in each category and show how they all fit together.

Goals

The goals for the engine are:

  1. Make the engine data-driven. This places a strong emphasis early-on on serialization, both for storage and networking. It also makes the editor easier to build — if everything in the game is based on data, it’s easy to have a predefined list of data types and expose them all to the editor.
  2. Make scripting a 1st-class citizen. Almost everything should be exposed through scripting so that we don’t end up in the spot of having to do some things in C++ only and some thing in JavaScript.
  3. Reduce coupling of modules (ie. rendering and scripting or physics and rendering). The ultimate version of this is is that no one system or component calls another engine module directly. We’ll see how successful we are in this.
  4. Keep a mind towards multi-threading. This is especially true for scripting where most of the game logic will happen, but also for systems (ie. instead of iterating over all component in one function, use a callback on each component that can be executed in parallel).

The Basics

The engine is 3D and uses OpenGL 3+ (nothing from OpenGL 4, but 3.1 – 3.3); however, the graphics library is built in such a way as to be extendible so you could use DirectX 11. It is cross-platform and will be tested on macOS and Windows. It should also compile on Linux, though I haven’t taken the time to try that myself yet. Most of my background before putting this all together is in graphics. That will be evident when we build an entire graphics framework from the ground up, but use middleware for almost everything else.

The engine uses JavaScript as a scripting language. I wanted something OO (which Lua does not provide naturally) and was more familiar with JS than C# (that said, I have looked at integrating Mono several times to add C# support and will make notes in the scripting engine documents on how it could be ported to another framework). This is accomplished through Google’s V8 engine, which was the most performant JS engine at the time I started implementing.

While the engine itself (without the editor) ranges from 20k – 30k lines of code, it is built on backs of giants – in this case, on top of many other third party libraries (as you’ll see below).

How is it built?

The engine uses a number of pieces of middleware, listed below (alphabetically):

  • Assimp – Model loading (editor) and conversion to our own format
  • Bullet – Physics
  • FMOD – Audio
  • FreeType – Font rendering
  • GLew – OpenGL extension handling
  • GLFW – Cross-platform windowing
  • GLM – Vector and quaternion math.
  • cURL – HTTP(S) requests
  • MySQL – Remote database storage
  • OpenSSL – Encryption (both in cURL and our own networking)
  • Qt – Editor UI
  • Recast/Detour – Pathfinding
  • SOIL – Texture loading
  • SQLite – Local database storage
  • V8 – Google’s JavaScript engine, for scripting

What is included?

I’ve been working on this engine in various forms for over 10 years, but it’s just me. It is far from perfect. There are sections that are not as optimized as they could be, not as clever as the could be, and certainly not as modern as they could be. The point of this site is not to help you build the next Unreal engine; it is meant to bridge the gap between all of the individual tutorials and articles and showing how it all works together.

As mentioned above, we will go in-depth into real world game components: real-time server-authoritative networking, frustum and tree culling, scene graphs, lighting and shadows, custom 3D file formats, bone and mesh animation, building an editor, and integrating scripting with examples and documentation. All of these together will form our engine, so they will also all work together.

Where possible, I do my best to modularize components (ECS, rendering engine, scripting language, etc.) so that they could be easily replaced in the future. However, I’m sure you’ll find several instances in this code base and others where it was “good enough”. At the end of the day, the goal was to “finish” this site and make sure I covered as much ground as possible.

What is not included?

Of course, we cannot be all things to all people. First, the engine uses C++, so you should know how to read and write code. It uses bits and pieces from C++98 all the way through C++11, although the newer stuff is more out of convenience than any sort of idealism. Knowledge of C++ itself isn’t strictly required, but will definitely help.

Second, this is not a tutorial on OpenGL. I will explain how and why I use most OpenGL functions, but before starting you should be familiar with DirectX or OpenGL. If you are not, you should read the Getting Started and Lighting sections of Joey de Vries’ excellent learnopengl.com.

Third, while documentation is good, for third party libraries Stack Overflow is almost always more useful once you’re past “Hello World” and into Hello Real World. Get your Googling fingers ready and be prepared to start searching for answers to all of the Weird Stuff you’ll find as you build your own tools.

Organization

I’ve laid the site out into sections representing modules of the engine (base, rendering, scripting, pathfinding, etc.), so that you can either start at the beginning and follow along, or jump to specific sections you’re interested in (rendering, scripting, etc.). Either way, I recommend you read or review the Base classes, as they will be used throughout the site and are the mostly likely classes you would re-write yourself or use drop-in replacements for.

GigaObjects

As with most other game engines you’ll see out there (Unreal, Godot, etc.), it is easiest to have a single base class that all other objects derive from. In addition to building in some basic functionality (like ToString functions, locking for multi-threading, etc.), it also comes in handy for things like scripting, where you can pass around pointers to the base object class until you need to statically or dynamically cast it up to its actual type. You’ll see examples of this in our scripting engine and in our Variant class, which allows us to hold a value of almost any type.

In our engine, we call this the GigaObject. Its only purpose, other than providing a base class, is to provide a mutex, which can be used to lock or unlock the object for multi-threading purposes later on.

Let’s look at the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Base object class for most objects in the instance
 */
class GIGA_API GigaObject {
public:
	virtual ~GigaObject() = default;
 
	/**
	 * Lock/unlock object for multi-threading
	 */
	void Lock();
	void Unlock();
 
protected:
	// Protected default constructor (no direct creation)
	GigaObject() = default;
 
protected:
	// Mutex
    std::shared_ptr m_mutex = std::make_shared();
};

As you can see, there’s not much here. We provide a simple Lock and Unlock function, which locks and unlocks our mutex for multi-threading:

1
2
3
4
5
6
7
void GigaObject::Lock() {
	m_mutex->lock();
}
 
void GigaObject::Unlock() {
	m_mutex->unlock();
}

And that’s it. You may also have noticed that we declare a protected constructor. This is a common strategy used in the classes of the engine that cannot be created directly.

Next, we’ll look at our Variant class, which allows us to pass around different data types and values (including GigaObjects) without inherently knowing what’s inside. This will come in handy later for generating metadata and for our scripting engine for passing parameters to functions.