Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to the Max developer guide! You can read it in a linear fashion or use it as a quick reference. You can use the left sidebar menu to navigate the different pages or the search button if you're looking for anything specific.

This documentation is maintained by us, so if anything is out of date let us know! You can click on the suggest an edit button on the top right corner to open an issue on GitHub. Alternatively, you can tell us directly.

Note: If you're looking for the API reference, click here: TODO API DOCS LINK

Setting up

The first step into contributing to Max is to set up your development environment to work on Max. This section goes over installing the required packages, getting the repository and building it.

Getting started

Getting Max

In order to get a copy of Max on your computer, you need to have git configured.

Note: Your GitHub account needs to be added as a developer of Max in order for you to be able to pull the repository.

Setting up git

If you don't have git installed, use your package manager to install it.

  • Debian, Ubuntu, Pop!OS: sudo apt install git openssh-server
  • Fedora, Nobara: sudo dnf install git openssh-server
  • Arch, Manjaro, Endeavour: sudo pacman -S git openssh

Getting SSH keys

In order to pull the Max repository from GitHub, you need to have SSH keys configured.

If your user does not have any keys set up, you'll need to create them. To create a new SSH key, run ssh-keygen. The program will ask a bunch of questions, you can leave all of them blank by pressing enter.

This will generate an ssh key pair found at ~/.ssh/.

Configuring GitHub to recognize your SSH keys

In order for GitHub to recognize your ssh keys as yours, you need to add them to your account. To do so, log into github.com and open the following link: https://github.com/settings/keys

Alternatively, you can find the page through the GUI by navigating to Profile picture > Settings > SSH and GPG keys

Now you can copy your public key. On a terminal, run cd ~/.ssh; ls. You should see a file finishing in .pub like id_ed25519.pub or id_rsa.pub. Copy the contents of this file by running cat <FILE_NAME>.pub and copying the output.

With the public ssh key on your clipboard, click on New SSH key on the GitHub website and paste it on the Key text box. The title field will be automatically populated once you press on Add SSH Key, so you don't need to fill it.

Pulling the Max repository

On a directory of your choosing, you can run git clone git@github.com:GameDEV-UPC/max.git to download the Max repository on your computer.

Setting up the tool-chain

In order to ensure a consistent development environment across machines, we use Nix. You can opt out of using Nix, in which case you can skip straight to setting up the formatter for your editor. Note that you will not receive support if Max is not building correctly for you.

Installing and configuring Nix

You can find the Nix installation instructions here: https://nixos.org/download/.

Once Nix is installed, you need to configure it to enable flakes. You can do so by running the following command:

echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf

Installing and configuring direnv

In order to automatically load the development environment once you open the Max directory, you can install direnv on your system.

Follow the installation instructions at https://direnv.net/#getting-started.

Direnv doesn't load flakes by default, but you can enable it with an extension. To install the extension, follow these steps:

  • If it doesn't exist, create a configuration directory for direnv:

    mkdir ~/.config/direnv
    
  • Change directory into the direnv configuration directory:

    cd ~/.config/direnv
    
  • Clone the extension's repository:

    git clone https://github.com/nix-community/nix-direnv
    
  • Add the following line to the direnvrc file:

    source $HOME/.config/direnv/nix-direnv/direnvrc
    

    If it the direnvrc file doesn't exist, you can create it.

Trusting Max

Direnv doesn't load development environments unless you've told it you trust them before. cd into the directory where you cloned Max and you'll see a message on your terminal. You can run direnv allow to trust the repository.

Set up the formatter for your editor

Max strictly adheres to the style guide, so using a formatter is mandatory. The development environment provided by the Nix flake already includes the formatter, but you still have to integrate it with your text editor.

Neovim

Assuming you already have the nvim-lspconfig and ccls or clangd plugins installed and configured as well as your leader key set up, you just need to define the shortcut:

vim.keymap.set('n', '<leader>f', function()
    vim.lsp.buf.format { async = true }
end, opts)

Zed

The zed editor integrates the formatter out of the box, the shortcut to activate it can be found on it's docs.

Visual Studio Code

There exists a Visual Studio Code extension that integrates the formatter provided by the Nix flake into the editor.

Other editors

If you use any other text editors let us know! We'll include the instructions here.

Building Max

To keep the development environment clean, builds are done on the build/ directory. It doesn't come included on the repository, so you can create it with mkdir build. For the following build-related pages, it is assumed you're on the build/ directory.

Debug build

A debug build allows you to use the debug macros.

To set up the build directory for a development build, run:

cmake -DMAX_DEBUG=ON ..

You can compile as usual (with make), but to speed up the compilation process, you can split the compilation tasks across threads with the -j flag. If your computer has 8 threads, the command would be: make -j8.

Note: The command nproc outputs your processor's thread count.

Release build

Release builds don't contain any debug symbols and are more optimized, resulting in smaller, faster binaries. It is recommended that for distributing Max, it is built in release mode.

Linux

To set up the build directory for a release build, run:

cmake ..

To compile, run:

make

Windows

To be determined

Contributing

Max is an open source project built by many people, so there some considerations to be taken when contributing to it. Mainly about maintainability and legality.

In this following section we'll go over using git with the Max repository, keeping the code clean and maintaining the legality of the codebase.

Style guide

Formatter

Patterns

Smart pointers

Documentation

Git

At GameDev we use Git as our version control system, so in order to contribute code to the Max repository, basic Git knowledge is required. Even if you're well versed in Git or use another Git-compatible VCS, it's recommended you read the following section as it goes over the workflow etiquette that's expected from contributors.

Git basics

Note: This guide assumes you use the Git CLI, but if you use a graphical client or an IDE integration, it still applies.

It also assumes you've read the Getting started section and already have Git configured and know about the clone sub-command.

Main commands

These are the most important Git commands you need to know about:

git pull

Downloads the changes from the remote repository for your current branch

git checkout -b <branch_name>

Creates a new branch from the state of your local copy with the given name. Whenever you intend to work on a change or feature for Max, you must work on a branch.

Branch names must follow this standard: module-storyNumber-changeName, where storyNumber is the story number in the module's Taiga board. An example of a branch name would be: InputManager-21-controllerDetectionFix

git switch <branch_name>

Change which branch you're currently editing. This will update the contents of your local directory to reflect that of the latest change on the branch.

git add <files>

Tells git that you want to add the changes you made on the provided files to your next commit. <files> is a list that can contain glob patters, for example **/*.h to add all the header files in all the directories.

git commit -m "<message>"

Creates a snapshot in time of the current state of your local copy of the repository. You should make commits often, with every small change being a commit, even if it does not compile at that point or if the feature or fix are not fully implemented.

Write commit messages in the imperative form, so they make sense if they're read as "This commit will <message>". For example: "This commit will Add Lambertian diffusion", "This commit will Fix overly long echo". Thus, messages like Removed unused code would not make sense in the phrase, so they're incorrect.

git push

This sends your change history for your current branch to the remote repository. You can push after every commit, but it's not strictly necessary, you can push whenever you need your changes to be on the remote.

You should only ever push to branches you created. In order for your changes to be part of Max, you'll first have to create a merge request and get it reviewed (explained in the following section).

Merge requests

Once you've made your changes and you think they're ready for Max, you can open a merge request.

First, go to the merge requests tab on the GitHub page for Max and click on new pull request. Select the base branch to be dev (or any other target branch other than main you may have). Alternatively, you can click on this link. Now, you select the branch with your changes as the compare branch and click on create pull request. The pull request title should follow the same format as your branch's name, but you can add spaces, for example: RenderManager - 42 - Add Blinn-Phong shader. You can create the pull request by clicking on Create pull request.

Passing review

Once a merge request has been created, two things will happen:

  • GitHub will automatically run some tests on your changes
  • Another contributor will review your changes

In order to get a merge request accepted, the following must be true:

  • The version of Max with your changes compiles
  • All the automated GitHub checks pass. If they don't, you can click on details for the failing test, which will tell you exactly why the test failed. If you push new commits to your branch, they will automatically be added to the merge request, and the tests will be re-run.
  • The changes follow the style guide
  • Another contributor has reviewed your code and has deemed it okay for it to be merged to the target branch. Contributors can leave comments and change suggestions on the merge request page, some of which can be applied automatically with the press of a button. Others may require you to make new changes and push it to your branch. Then, your merge request can be reviewed again.

Legal guidelines

The Max engine is a free and open source library dual-licensed under the MIT and Apache 2.0 licenses. The following section goes over basic things to have in mind to not breach those licenses.

Using libraries

Max can depend on other free and open source libraries, but they must be licensed under compatible licenses, for example: MIT, Apache2.0, Zlib, BSD, X11 and Public domain.

Max can however not depend on less permissive licenses such as any GNU license, the Mozilla Public License and any proprietary license. Before considering a library, check if it's compatible with these terms.

Copying code

Besides using libraries, other people's code can also be used in Max as long as the licenses are followed. If you re-use some code that was written outside of the Max repository, you must follow that code's license, which may mean to add a comment or similar indicating the origin and the author. This takes for granted that the licenses are compatible, just like with library usage.

If you aren't strictly copying code but otherwise gathering ideas on how to solve a problem, you shouldn't need to do this. Although it's recommended you don't do this with leaked proprietary codebases, as it could still land us in trouble.

LLMs

Adding LLM-generated code to Max is strictly forbidden. LLMs have been trained with lots of data without permission, which causes them to spit out code that's licensed without citing the sources.

You may use LLMs as a rubber ducky, for debugging, or help you understand how something works, but you can't paste code generated by an LLM into the Max codebase.

Architecture

This following section goes over Max's architecture. But in order to understand Max's layout, we must first understand some key concepts.

What is a game engine?

A game engine is the tool that allows game developers and artists to design and create video games. Think of a game engine as a black box where the inputs are assets and scripts and the output is a video game or application.

Game engine black box The image above shows the black box with its inputs and outputs. Notice how there's extra tooling like a user interface that's not necessarily attached to the main black box.

Components

Core

Ui

RenderManager

InputManager

PhysicsManager

SoundManager

ParticleManager

AssetManager

Debugging

Debug macros

As per the style guidelines, using <iostream> on the Max codebase is forbidden. It is for this reason that Max ships some debugging macros to print information to the terminal. They live at the Debug.hpp file.

Note: Debug macros only display the messages on debug builds.

There are six debugging macros included with Max:

  • show(): Prints the message unconditionally in bright purple. This macro is used when you want to display some information ONLY during the development stage and plan to remove it later.

    // my_sum_function.cpp
    int sum(int lhs, int rhs) {
        show("The first number is ", lhs, "!");
        return lhs + rhs;
    }
    int result = sum(2, 4);
    

    show() macro

  • trace(): Used for maximum verbosity messages. Information that you generally wouldn't want to see, but that is useful to have if you're trying to determine the exact state of the engine.

    // RenderManager/shaders/DefaultShaderInv.cpp
    void DefaultShaderInv::initObject(RenderObject *renderObject) {
        glBindVertexArray(renderObject->getVAO());
        trace("Bound VBO: ", renderObject->getVBO()[0]);
        glBindBuffer(GL_ARRAY_BUFFER, renderObject->getVBO()[0]);
        // ...
    }
    

    trace() macro

  • debug(): Used for medium verbosity messages. Information that lets you track the general state of Max, but not granular enough to tell exactly what it's currently doing.

    // InputManager/InputManager.cpp
    MXbool InputManager::isJoistickConnected() {
        MXbool connected = glfwJoystickPresent(GLFW_JOYSTICK_1) == GLFW_TRUE;
        if (connected) debug("Joystick connected.");
        return connected;
    }
    

    debug() macro

  • info(): Used for information you always want to have.

    // EngineLogic/MaxBuilder.cpp
    Max MaxBuilder::build() {
        // ...
        Input = new InputManager();
        Camera *camera = new Camera(_screen_width, _screen_height);
        Render = new RenderManager(_screen_width, _screen_height, camera);
        Max max(_camera, _scene, _window);
    
        info("New Max instance created.");
        return max
    }
    

    info() macro

  • warn(): Used to indicate something went wrong, but that it is not a critical issue and the engine can recover from it.

    // RenderManager/ModelLoader.cpp
    ModelObject *ModelLoader::create_model_object(std::string name, const MXchar *model_src) {
        Assimp::Importer importer;
        const aiScene *scene = importer.ReadFile(
            model_src, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_PreTransformVertices | aiProcess_GenNormals);
    
        if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
            warn("File ", model_src, " does not contain a model!");
            // ...
        }
        // ...
    }
    

    warn() macro

  • error(): Used for critical errors the engine cannot recover from, and most likely result in a crash.

    // EngineLogic/MaxBuilder.cpp
    Max MaxBuilder::build() {
        // ...
    
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            error("GLAD failed to initialize");
            throw BuildExeption(Cause::Faliure, Item::Internal);
        }
    
        // ...
    }
    

    error() macro

As you will have noticed, their structure is: DEBUG_LEVEL [file_name:function_name:line_number] user message.

Besides show(), debug macro messages will not be displayed by default, even on debug builds. They have to be enabled through the LOG_LEVEL environment variable.

The levels correspond to the macro names, and are ordered from least to most verbosity in the following list: ERROR < WARN < INFO < DEBUG < TRACE. Enabling a debug level automatically enables all the previous ones on the list.

LOG_LEVEL=INFO ./my_max_game Will show ERROR, WARN and DEBUG messages.

Profiling