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¶
- Gameplay with entities, components and systems instead of plain variables: ECS design and Add a component and a system.
- Images, text, sounds and gamepads: the module tables of Architecture.
- Ship the game: Install and package a game.