Voxel Game Series

How to write a game engine from scratch

by

In this article series we’re going to write a small game engine for a voxel minecraft-esque game. We’re going to use Vulkan, Khronos’s successor to the cross-platform OpenGL APIs. These articles are meant to primarily target Microsoft Windows, I will point out platform-specific code, so that people on Linux should be able to follow along as well.

Help wanted!

A game engine is a complex piece of code and I’m not an expert in all areas. I will try my best to give you accurate information and to link to more resources such that if you’re interested you can always check out more.

If I got something wrong or you want to contribute, please hit me up on twitter (@current_thread). I would be interested in having someone who’s experienced with DirectX 12 help me out, but any help is more than welcome.

What we’re building

In this series, we’re going to build a small 3d engine. It should be render-backend agnostic (that means it should - in theory - support different graphics APIs). The game engine needs to be heavily multi­threaded. We’re going to use C++20 with modules1. In the future, we might venture into VR and ray tracing, but I don’t want to make any promises.

Project set-up

First, let’s look at the project structure and then drill into the details: Our project is going to have two projects, Engine and Game. The first project is a dynamic library that is consumed by the second project. The second project is our executable.

We’re going to develop our game engine with Visual Studio. In my opinion, Visual Studio is the best IDE for C++ development. You can also have a look at Resharper C++, which elevates VS to a new level. Furthermore, you should check out if you can get access to GitHub Copilot, because its completions make you type even less.

Obligatory Microsoft Employee Disclaimer

Here is the obligatory disclaimer that I’m – at the time of writing – employed by Microsoft. I’m writing this blog post in my free time and it really is my opinion that Visual Studio is the best (C++) IDE.

We’re going to rely on many external libraries (e.g., boost). Dependency management in C++ is hell, so we’re going to use the Visual Studio integrated package manager vcpkg.

But you're not using ${user.favoriteTool}!

I understand that some of these choices are highly opinionated. They are merely suggestions how you could set up your dev environment. You could also use CMake with your distro’s package manager.

Folder structure

Here’s my proposed folder structure. It’s based on the folder structure in Game Coding Complete 4th Edition by McShaffry et. al.2

.
├── assets/
├── bin/
│   ├── Game_Debug/
│   └── Game_Release/
├── doc/
└── src/
    ├── Engine/
    │   └── Engine.vcxproject
    ├── Game/
    │   └── Game.vcxproject
    ├── .clang-format
    ├── Voxels.sln
    └── vcpkg.json
  • assets contains all of our game’s assets.
  • bin contains the game’s executables.
  • doc contains the generated documentation (if we set up something like Doxygen in the future).
  • src contains all source files.
    • Each project gets its own subfolder with the project file in it.
    • We can also find the vcpkg manifest here.
    • Lastly, we can use clang-format so we have a proper coding style3.

Creating the Visual Studio Solution

Now, we’re going to actually create the Visual Studio Solution. Open up Visual Studio and create a new DLL:

The visual studio wizard for new project creation. "Dynamic Linking Library (DLL)" is selected.
The visual studio wizard for new project creation. "Dynamic Linking Library (DLL)" is selected.

In the following wizard chose an appropriate path for your solution file. Remember that we’re going to put it in the src/ folder.4

The visual studio wizard to create a solution.
The visual studio wizard to create a solution.

Gotcha!

You must be careful to follow our folder scheme here. You probably want to quit out of Visual Studio, and cut-and-paste the *.sln files to the src/ folder.

Next, we’re going to adjust some settings. Right-click on your Engine project and select “Properties”. On the top-right click “Configuration Manager”. For “Active Solution Platform” select “<Edit…>” and delete x86. In the year 2023 we’re really not gonna support 32 bit architectures anymore.

Back in the properties window, choose “All Configurations” in the top-left combox-box and change the following values (again, this assumes your solution is in src/):

  • General:
    • Output Directory: $(SolutionDir)..\bin\Game_$(Configuration)\

      This causes the DLL to build to /bin/Game_{Debug, Release} and be automatically found by our game executable.

    • Intermediate Directory: $(SolutionDir)..\tmp\$(ProjectName)_$(Configuration)\

      This causes all temporary files of the DLL to land in /tmp/Engine_{Debug, Release}.

    • C++ Language Standard: Preview - Features from the Leates C++ Working Draft (/std:c++latest)

      We’re going to use C++20 features and – rarely – C++23 features. Thus it is necessary to allow for all the features the compiler currently supports.

  • vcpkg:
    • Use Vcpkg Manifest: Yes

      Manifests are a way to specify the project dependencies. Instead of installing the globally, we’re going to manage them on a per-solution basis.

  • VC++ Directories:
    • All Modules are Public: Yes

      We’re going to use C++ modules in our DLL. To be able to use them in our Game executable, we have to export them. This setting will make sure they are found by the executable.

  • Precompiled Headers:
    • Precompiled Header: Not using Precompiled Headers

      We’re not going to use precompiled headers because we’re going to use C++20 modules.

Delete all files from the engine project and create a new file called prologue.hpp. This file will contain all macros that are vitally important for our entire project. Right now, all we’re interested in is to be able to export and import stuff from our library, so we’re going to create a macro for that:

// prologue.hpp
#pragma once

#ifdef _WINDLL
    //! \brief If the library is being compiled as a DLL, this macro is defined and 
    //! can be used to export symbols.
#   define ENGINE_API __declspec(dllexport)
#else
    //! \brief If the library is included, this macros is included and used to import
    //! symbols.
#   define ENGINE_API __declspec(dllimport)
#endif

Platform-specific code

If you want these examples to work on Linux, __declspec is not going to work. In this case, you want __attribute__((visibility("default"))) and extend the macros above to check for Windows (e.g. with #ifdef _WIN32). Here’s a StackOverflow answer that gives an example.

For now, we just want to have our library do something. So we’re creating a new file engine.ixx. You can do this by right-clicking on the project, then selecting “Add -> Module”. It looks like this:

// engine.ixx
module;

#include "prologue.hpp"

#include <iostream>

export module engine;

export namespace engine {
    ENGINE_API void say_hello() {
        std::cout << "Hello, World!\n";
    }
}

Avoiding "fatal error C1001: An internal error has occurred in the compiler"

C++20’s modules are kind of new and both compiler support and IntelliSense support is sometimes really janky. You might run into stupid compiler errors like the infamous C1001. One thing that (in January 2023) seems to provoke this error is mixing #include and import with STL headers. Thus, as a rule of thumb, in all of the code snippets we’re going to include STL headers in the global module fragment and not import them.

Next, create a new Console Application. Then, right click on your new Game project and set it as startup project. Next, click on the “References” node in the Solution Explorer, “Add Reference” and add a reference to the engine project. Now, you need to apply the same settings from above to this project as well.

These are the additional settings that should be changed:

  • Debugging:
    • Working Directory: $(SolutionDir)..\

      Sets the working directory to the root folder of our project.

Finally, change the main.cpp to use our DLL:

import engine;

int main()
{
	engine::say_hello();
	return 0;
}

When you build and run the project you should see “Hello, World!” printed in the console.

C++ modules in 3 minutes or less

The above code looks probably a bit unfamiliar to people who haven’t kept up with the newest language developments. Essentially, modules are a replacement for the traditional header/ source file pair. They have many nice properties such as reduced compile times, no transitive-by-default imports and selective exports. Read this excellent blogpost to get into the nitty gritty, here’s a very rudimentary crash course.

A module is defined in a module interface unit. In Visual Studio they have the ending *.ixx by default, but they can be regular *.cpp files as well. They look like this:

module; // Optional, "Global module fragment"

#include <unordered_map>

export module my_module;

import <vector>;
import other_module;
export import yet_another_module;

void do_bar() {

}

export template<typename T>
void do_foo(T) {
    do_bar();
}

Modules can start with a “global module fragment” module;. In it, we can include all files whose behavior depends on preprocessor state. In other words: If your header file changes depending on if certain #defines are present, they should probably go there. Note that no other code may go in the global module fragment.

We start our module with export module <module name>;. Now, we can import other modules. Under certain circumstances we can even import header files like <vector>. We can re-export imports (with export import) which makes them available to client code.

Similarly, we can export things from our module: Typically, we can export classes, structs, type aliases, templates, functions and variables5. Everything that is not exported from a module stays internal to it. This means, that clients of my_module are going to see do_foo but not do_bar. Preprocessor macros will never be exported from modules (that’s why prologue.hpp is a header and not a module).

Any code can consume modules by means of the import statement. In particular, in our game code main.cpp is not a module, but imports our engine’s module!

Note that this only scratches the surface and that you should really read this excellent blogpost.

This wraps the first part of our series, next time, we’re going to build a logging infrastructure.

Footnotes

  1. I have had to suffer and so will you.

  2. A quick note on Game Coding Complete: I feel this book is a nice read for beginners. It touches on a lot of subjects and gives you the words to google for better solutions. In no way however should you treat the author’s words as the “one, true way to do things”. Take it as a starting point and learn to do it better.

  3. This blog uses

    BasedOnStyle: WebKit
    
    ColumnLimit: 160
    
    IncludeCategories:
    - Regex: '^"(\.\.\/)*prologue.hpp"$' # Prologue comes first in headers
      Priority: -2
    # Now we have the file's main header
    - Regex: '^(<|"(gtest|gmock)/)' # GTest/ GMock headers should come next
      Priority: 1
    - Regex: '^<[[:alnum:]]+>$' # Now, we do STL headers
      Priority: 2
    - Regex: '^<.*>$' # Now, we do all external headers
      Priority: 3
    - Regex: '^.*$' # And finally, all other headers
      Priority: 4
    

    but it’s really a matter of taste.

  4. You can also put it in the root folder. The nice thing about this approach is that you see the assets/ in VS’s folder view. Again, this is a matter of taste.

  5. One needs to be careful with static in this context, but let’s not discuss this right now.

Previous article

This is the first article in the series.