Skip to content

Make a game

From an empty folder to a window with a moving sprite, on an engine installed once. It takes about fifteen minutes on a machine with a C++26 compiler, CMake 3.30 or newer, Ninja and git (see Getting started for the versions). The game is two files; the engine's tests build and run them exactly as written here (install_tutorial), so they stay true.

1. Install the engine

Build the engine once, then install its development files: modules, headers and the CMake package. ~/aegis2d is where they go; any folder works.

git clone git@github.com:Aegis2D-Dev/aegis2d.git
cd aegis2d
cmake --preset client-release          # on macOS: add -DAEGIS2D_MACOS_GL41=ON
cmake --build build/client-release
cmake --install build/client-release --component development --prefix ~/aegis2d

~/aegis2d/lib/cmake/Aegis2D/ now holds the package the game will find. The sources are no longer needed by the game.

2. Create the game

Make an empty folder, my_game, with two files: CMakeLists.txt and main.cpp.

CMakeLists.txt asks for the engine and the modules the game uses. render draws and brings the modules it needs: platform (the window, the keyboard) and core (the main loop):

cmake_minimum_required(VERSION 3.30)
project(my_game LANGUAGES CXX)

# The installed engine, found through CMAKE_PREFIX_PATH: render brings platform and core.
find_package(Aegis2D 0.1 REQUIRED COMPONENTS render)

add_executable(my_game main.cpp)
target_link_libraries(my_game PRIVATE aegis2d::render)
target_compile_features(my_game PRIVATE cxx_std_26)
if(WIN32)
    # Windows finds DLLs next to the executable: copy the engine's and SDL's there.
    add_custom_command(TARGET my_game POST_BUILD
        COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            $<TARGET_RUNTIME_DLLS:my_game> $<TARGET_FILE_DIR:my_game>
        COMMAND_EXPAND_LISTS)
endif()

3. Open a window

main.cpp starts with the engine's headers. Each module has its folder: aegis2d/core/, aegis2d/platform/, aegis2d/render/.

#include <aegis2d/core/clock.hpp>
#include <aegis2d/core/loop.hpp>
#include <aegis2d/platform/keyboard.hpp>
#include <aegis2d/platform/window.hpp>
#include <aegis2d/render/image.hpp>
#include <aegis2d/render/opengl/context.hpp>
#include <aegis2d/render/opengl/renderer.hpp>

#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <string_view>
#include <utility>
#include <vector>

namespace core = aegis2d::core;
namespace platform = aegis2d::platform;
namespace render = aegis2d::render;
using aegis2d::Vec2;

A sprite is drawn with a texture. This one is made in code, so the game has no image file to ship; render::Image::load("player.png") reads one instead.

// An 8x8 checkerboard, white and orange, made in code: no image file to ship.
render::Image checkerboard() {
    std::vector<std::uint8_t> pixels;
    for (int y = 0; y < 8; ++y) {
        for (int x = 0; x < 8; ++x) {
            const bool white = (x + y) % 2 == 0;
            pixels.insert(pixels.end(), {255, static_cast<std::uint8_t>(white ? 255 : 140),
                                         static_cast<std::uint8_t>(white ? 255 : 0), 255});
        }
    }
    return render::Image::from_pixels(8, 8, std::move(pixels));
}

In main(), the window comes first, then the renderer that draws in it, then the texture, uploaded to the graphics card once:

        platform::Window window{{.title = "My game", .width = 1280, .height = 720}};
        render::opengl::Renderer renderer{window};
        const render::TextureId texture = renderer.create_texture(checkerboard());

4. Move the sprite

The main loop calls update 60 times a second, whatever the speed of the machine, so the sprite moves at the same speed everywhere: positions change by velocity * seconds. It then calls render once per frame, which reads the window's events (the keyboard, the close button) and draws the sprite where it is:

        const Vec2 size{64.0F, 64.0F};
        Vec2 position{0.0F, 328.0F};
        Vec2 velocity{240.0F, 0.0F}; // pixels per second

        core::MainLoop loop{core::FixedTimestep{core::step_from_rate(60)}};
        loop.set_update([&](core::Duration step) {
            const float seconds = std::chrono::duration<float>{step}.count();
            const auto& keys = window.keyboard();
            velocity.y = keys.held(platform::Key::Up)     ? -240.0F
                         : keys.held(platform::Key::Down) ? 240.0F
                                                          : 0.0F;
            position += velocity * seconds;
            // Back the other way at the left and right edges.
            if (position.x < 0.0F || position.x + size.x > 1280.0F) {
                velocity.x = -velocity.x;
                position.x = position.x < 0.0F ? 0.0F : 1280.0F - size.x;
            }
        });

        std::uint64_t frames = 0;
        loop.set_render([&](double /*alpha*/) {
            window.poll_events();
            if (window.close_requested() || (max_frames > 0 && frames == max_frames)) {
                loop.stop();
                return;
            }
            renderer.begin_frame({.r = 0.1F, .g = 0.1F, .b = 0.15F});
            renderer.draw({.texture = texture, .position = position, .size = size});
            renderer.end_frame();
            ++frames;
        });
        loop.run();

The sprite slides from edge to edge; Up and Down move it while they are held. The whole file, with the error handling around it, is tests/install/tutorial/main.cpp.

5. Build and run

Point CMake at the installed engine, build, run:

cd my_game
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH=~/aegis2d
cmake --build build
./build/my_game                         # build\my_game.exe on Windows

A window opens with a checkered sprite sliding across it. Close it with its close button; ./build/my_game --frames 120 closes it after two seconds.

Next steps