G-Engine #2: Project Setup

The previous post introduced the idea and motivation behind the project. So here we are, ready to build a 3D game engine! This post walks through some early decisions, starting from absolutely nothing to having just an empty application window that can be moved around and closed.

This is not the most exciting end result, but there are plenty of important decisions to be made before we dig into actually writing game engine code.

Why G-Engine?

In Scott Bilas’ postmortem of GK3, he referred to the engine used to build the game as “the G-Engine.” I don’t know if this was an official name or what, but presumably it refers to “Gabriel Knight Engine.”

Anyway, you need to name your repository and folders something, so I figured “G-Engine” was as good a name as any. You can always change it later, right? (…right?)

Language

G-Engine will be developed in C++. Since I use Unity at my day job, most of my day-to-day coding is in C#. I like C# just fine, but it means that my C++ skills could use some periodic flexing.

C++ is also the “de facto” standard for game engine development, in my experience. One big reason for this is performance. Unity, for example, puts a lot of effort into “trans-piling” C# code into C++ code for the performance gains. Unreal Engine writes their engine and gameplay code directly in C++.

Version Control

The engine will be contained within a single repository. Perhaps someday it will make sense to create a separate repository to hold editor code or some other module, but for now, a single repository will do.

I decided to use Git for version control. Git is very popular these days, and is used for a lot of open-source development. One reason for this is that Git has a well-defined workflow for viewing source code, making changes, requesting a review of changes, and finally merging changes. Git also does a great job of versioning, branching, and merging text-based assets.

Handling Large Binary Files

One thing that Git is NOT great for is storing large binary files. This is actually the primary reason that we still use SVN for some of our more asset-heavy projects at Hidden Variable. Recently, Git added better support for large binary files with Git-LFS.

For this project, the majority of our assets will be text-based, since the engine will be mostly code.

If you recall from the first post in this series, my plan is to utilize the data files that were included with Gabriel Knight 3, interpreting the data within to reconstruct the game in a modern engine. Those data files are not text-based - they are large binary files, which is not a good fit for Git. What to do?

Fortunately, this problem solves itself: the data packaged with GK3 is not owned by me, and if I uploaded it to the internet, I’d probably be breaking a law or two. As a result, I will not be storing that data in Git. If other people download and run the engine, they will need to provide these copyrighted/licensed assets from a copy of GK3 they have obtained themselves (hopefully through legal means).

But hypothetically, if I did have to store those files, I’d probably use Git LFS.

Hosting

Though Git repositories can run locally and can be self-contained on a single machine, doing so would negate a major benefit of version control: having a backup. Imagine if I kept all my versioning history locally on my laptop, and then it was stolen or the hard drive crashed!

There are two big players in Git hosting: Github and Bitbucket. I decided to use Bitbucket because I’m already familiar with its interface and tools. It also allows you to store private repositories for free. For starters, I’m making the engine repo private - not because I don’t want to share it, but just because I don’t yet know whether this whole engine idea is going to pan out.

So, now I have a Git repository hosted on Bitbucket. As the folks at Atlassian say, time to fill that bucket with some bits!

IDE and Development Platform

For a long time, game development and game playing were both dominantly performed on Windows. When GK3 was released in 1999, it only supported Windows, and Windows was pretty much the only choice. Why support more platforms when everyone was using Windows?

These days, cross-platform game development (and game playing) is much more common. As Apple devices became more popular over the past decade, there was a bigger market for games that run on Macs. Also, mobile devices more and more worked as gaming platforms, so you often had a need for a game to run on iOS and Android, and maybe also Windows, macOS, and Linux in some cases.

Apple also complicates development a bit: if you want to develop a program for macOS, iOS, or tvOS, you WILL at some point need to use a computer running macOS to build and sign your applications. As a result, there is more pressure for game development tools to work on non-Windows platforms too!

For this game engine, my choice is to begin development on macOS using Xcode as my IDE. This choice was based off of a few things:

  1. I have a Mac laptop, so it would be most convenient. In contrast, my Windows desktop occasionally crashes at the moment - not so convenient.
  2. The original game was Windows-only, so getting things up and running on Mac is a decent proof-of-concept of the game working cross-platform.
  3. I prefer Mac’s Unix-based shell system to Windows’ command-line system, if I need to write shell scripts.
  4. I haven’t used Xcode as an IDE too much, and I’d like to get more comfortable with it.

All that being said, the goal throughout development is to use cross-platform techniques wherever possible. When it comes to changes that need to occur between platforms, those changes are ideally rather small (an alternative function or class) and insulated with a platform-independence layer.

Directory Structure

When you initially create a project in either Xcode or Visual Studio, they both do something I don’t like: they put your source code either in the same directory or a subfolder of the solution/workspace/project files (e.g. the .xcodeproj or .sln file).

This default folder structure is not well suited for cross-platform development. If you create a project in Xcode, the source files are stored inside your Xcode project folder. Later, if you want to compile and run the code in Visual Studio on Windows, you have Visual Studio reaching into the Xcode folder. It feels cluttered and disorganized and not particular cross-platform.

To combat this, I have decided to structure the project in this way:

Root/
  Assets/
    SomeTexture.png
    Shaders/
      Skybox.frag
      Skybox.vert
  Libraries/
    minilzo/
    zlib/
  Source/
    Main.cpp
  Tests/
    VectorTests.cpp
  Xcode/
    GEngine.xcodeproj
    MacOS/
      Info.plist

This structure has a few properties that I like:

  • Anything that is not code, but is used by the application at runtime, can be stored in Assets. This includes textures, audio, shaders, meshes, binary files, etc. GK3 uses archive files called “Barns” (BRN extension), which would also be put here.
  • Libraries contains any code that the engine uses, but which is provided by a third-party. This can include loose source files, static libraries, and dynamic libraries.
  • Source code is isolated from any particular IDE, allowing it to be easily used from any IDE, or even compiled using the command line or makefiles.
  • Tests can be written and stored separately from application source code to avoid clutter.
  • Each IDE has its own folder. When I add Windows support, I’d create a “VS” or “VisualStudio” folder. There are then subfolders for supported build targets of that IDE. For example, Xcode can build to macOS, iOS, tvOS, etc.

One question is whether the Source/Tests folders should have subfolders. I think it does make sense to group some related code files together, but it also makes the #include statements more complicated. My plan is to keep most code files at the same level, and perhaps I’ll create subfolders as large systems come together. To organize within the IDE, it is actually possible to group files logically without actually changing their locations on the disk, which is maybe more ideal.

Xcode Project Creation

I created a new Xcode project by opening Xcode and selecting File > New > Project. When asked to choose a template, I chose Command Line Tool from the macOS section.

The naming of the project templates can be a bit misleading. Both Cocoa App and Game allow you to create a macOS app that can be released on the App Store - the only difference is the starter code provided. Command Line Tool can build more than just command line tools - you can build an entire, complex 3D game with that template. However, an application created with the Command Line Tool template can’t be easily released on the App Store (unless you significantly adjust the project config later on).

In hindsight, I made a mistake by selecting Command Line Tool here. It made it difficult to use some Apple features (like an App Icon) because my Info.plist file was not set up. Fortunately, this mistake was easily remedied: once the project was created, I could go to File > New > Target and add a new target to the project (using Game).

This highlights one nice thing about Xcode: you can create a Project that contains one or more Targets. Targets potentially share code or other data files, but they are compiled to separate executables, which perhaps support separate platforms. This would allow you, for example, to build to a macOS target and an iOS target with the same codebase.

SDL Library Integration

One goal I have for this project is for it to be a learning experience. As a result, I don’t want to use too many SDKs or libraries that do the hard work for me and obfuscate the things I want to be learning in the first place.

At the same time, I don’t want to re-write things that aren’t meant to be rewritten. For example, the LZO compression homepage provides compressor and decompressor code - I should definitely use their code, rather than rewrite it myself, if I need LZO compression! (forshadowing?)

There are also some systems that I may want to write myself eventually, but it makes sense right now to use some pre-made scaffolding to get things going. SDL fits into this category.

What is SDL?

SDL is a library that I first became familiar with while teaching ITP 380 at USC. Put simply, SDL is a cross-platform development library that makes it easier to write a cross-platform game. One example of this is how you tell SDL to initialize a window for the game to run in: it’s just one function, but it works as expected on macOS, Windows, Linux, iOS, and Android, among others.

SDL is a platform-independence layer that provides interfaces for creating a window, receiving OS events, polling inputs, and graphics.

By using SDL early on, I can let it take care of creating a window, polling OS events, and polling inputs like keyboards and mice. Perhaps someday, I will want to remove SDL and write the code myself. On the other hand, it may prove so useful that I keep it indefinitely.

Integration Process

Integrating SDL is fairly easy, if you understand how to add plugins to your project! If you don’t (like me, when I was starting out) it can take awhile. Anyway, here’s how I did it:

First, download SDL for your platform. Both runtime and development libraries are available. The development library includes header files, which are needed to call SDL functions from your code. The distinction is actually only meaningful for Windows - on Mac, headers are included for both runtime and development libraries. If you are using Linux or some other platform, you may need to build from source or download from your package manager.

Put the library in your “Libraries” folder. Depending on your platform, this might be a .dll file (Windows), or a .framework file (Mac) or a .so file (Linux).

When compiling, you need to tell the compiler where the headers and libraries are located, and you also need to tell the linker to link against the library. In Xcode, you do this by adding appropriate entries to Framework Search Paths, Header Search Paths, Library Search Paths, and Other Linker Flags.

After doing this, it should be possible to use SDL in your project. We’ll find out if it worked in the next (and last) step of this post!

Window Creation

We now have our Xcode project and we have SDL integrated. We just need to write some code to generate a window using SDL and show it.

Let’s try this in Main.cpp:

#include <SDL2/SDL.h>

int main(int argc, const char * argv[])
{
    // Init video subsystem. Return 1 on error.
    if(SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { return 1; }

    // Create a 1024x768 window.
    SDL_CreateWindow("GK3", 0, 0, 1024, 768, 0);

    // Done!
    return 0;
}

This works, but it has two big problems. First, the window just closes immediately, since main returns, signaling the end of the program. Second, we are supposed to destroy a window after creating it, using the returned handle.

OK, so let’s change that code a little bit:

#include <SDL2/SDL.h>

int main(int argc, const char * argv[])
{
    // Init video subsystem. Return 1 on error.
    if(SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { return 1; }

    // Create a 1024x768 window. Return 1 on error.
    SDL_Window* window = SDL_CreateWindow("GK3", 100, 100, 1024, 768, 0);
    if(window == nullptr) { return 1; }

    // Loop forever to stop window from closing right away.
    while(true) { }

    // Destroy created window.
    SDL_DestroyWindow(window);

    // Done!
    return 0;
}

This also works, kind of. We never see the window because the game gets stuck in that infinite loop! Additionally, there’s no way to break out of that infinite loop, so our destroy code will never be called.

The reason the window doesn’t appear, and isn’t responsive, is because we aren’t polling the OS to respond to events, such as window appear, window move, close button pressed, etc.

Let’s change that:

#include <SDL2/SDL.h>

int main(int argc, const char * argv[])
{
    // Init video subsystem. Return 1 on error.
    if(SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { return 1; }

    // Create a 1024x768 window.
    SDL_Window* window = SDL_CreateWindow("GK3", 100, 100, 1024, 768, 0);
    if(window == nullptr) { return 1; }

    // Loop forever to stop window from closing right away.
    bool running = true;
    while(running)
    {
        // We'll poll for events here. Catch the quit event.
        SDL_Event event;
        while(SDL_PollEvent(&event))
        {
            switch(event.type)
            {
                case SDL_QUIT:
                    running = false;
                    break;
            }
        }
    }

    // Destroy created window.
    SDL_DestroyWindow(window);

    // Done!
    return 0;
}

That does the trick! We loop until the running bool becomes false. And every frame, we are polling the OS for events. When we get a “Quit” event from the OS, we set running to false, which breaks out of the loop, destroys the window, and exits the program.

This is a foundation for our game engine that we’ll continue to build upon. One problem is that we’ve just got everything in one Main.cpp file. We’ll break things up into more manageable chunks in the next post.

comments powered by Disqus