This guide takes you from an empty directory to a running window with a lit cube, a skybox, and a camera you can fly around. Every snippet is taken from a project that builds against the current engine.
By the end you will have the four pieces every SleakEngine game needs:
- A CMake project that pulls the engine in as a subdirectory.
- An entry point that creates a Sleak::Application and runs it.
- A game class deriving from Sleak::GameBase that owns your scenes.
- A scene class deriving from Sleak::Scene that owns your objects.
1. Lay out the project
The engine expects to sit alongside your code, not inside it. A working layout looks like this:
MyGame/
├── CMakeLists.txt
├── Engine/ # SleakEngine, cloned or symlinked here
├── Game/
│ ├── CMakeLists.txt
│ ├── include/
│ │ ├── Game.hpp
│ │ └── Scenes/FirstScene.hpp
│ ├── src/
│ │ ├── Game.cpp
│ │ └── Scenes/FirstScene.cpp
│ └── assets/ # your textures, models, shaders, skybox
└── Client/
├── CMakeLists.txt
└── src/main.cpp
Game builds as a shared library and holds all your gameplay code. Client is the executable, and it exists mostly to hold main() and to copy assets next to the binary.
2. Wire up CMake
Root CMakeLists.txt. Send all build artifacts to one bin/ directory, then add the three subprojects in dependency order.
cmake_minimum_required(VERSION 3.31)
project(MyGame LANGUAGES CXX)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /utf-8")
endif()
add_subdirectory(Engine)
add_subdirectory(Game)
add_subdirectory(Client)
Game/CMakeLists.txt. The one thing to get right is the include path: your game must see Engine/include/public and nothing else from the engine tree.
cmake_minimum_required(VERSION 3.31)
project(MyGame_Game)
file(GLOB_RECURSE GAME_SOURCES "src/*.cpp")
add_library(SleakGame SHARED ${GAME_SOURCES})
target_compile_definitions(SleakGame PRIVATE SLEAK_EXPORTS)
target_include_directories(SleakGame PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/Engine/include/public)
target_link_libraries(SleakGame PRIVATE Engine)
SLEAK_EXPORTS switches the SLEAK_API macro from import to export, so mark your public game classes with it: class SLEAK_API Game : public
Sleak::GameBase. The engine's own exported symbols use ENGINE_API; both macros come from Core/OSDef.hpp.
Client/CMakeLists.txt. Link the game and the engine, then copy both asset trees next to the executable after every build.
cmake_minimum_required(VERSION 3.31)
project(MyGame_Client)
file(GLOB_RECURSE CLIENT_SOURCES "src/*.cpp")
add_executable(MyGame ${CLIENT_SOURCES})
target_link_libraries(MyGame PRIVATE SleakGame Engine)
target_include_directories(MyGame PRIVATE ${CMAKE_SOURCE_DIR}/Game/include)
add_custom_command(TARGET MyGame POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Engine/assets
$<TARGET_FILE_DIR:MyGame>/assets
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Game/assets
$<TARGET_FILE_DIR:MyGame>/assets
COMMENT "Copying assets to output directory")
if(WIN32)
add_custom_command(TARGET MyGame POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:MyGame>
$<TARGET_FILE_DIR:MyGame>
COMMAND_EXPAND_LISTS)
endif()
That copy step matters more than it looks. Every asset path you write in code is resolved relative to the executable's directory, so "assets/skybox/right.jpg" means bin/assets/skybox/right.jpg. Edit an asset and you have to rebuild for the copy to run again.
3. Write the entry point
main() does four things in order: point the working directory at the executable, parse the command line, initialize logging, and run the application.
#include <Game.hpp>
#include <filesystem>
#ifdef _WIN32
#include <windows.h>
#endif
#define PROJECT_NAME "MyGame"
static void SetWorkingDirToExePath(char* argv0) {
#ifdef _WIN32
char path[MAX_PATH];
if (GetModuleFileNameA(nullptr, path, MAX_PATH) != 0) {
std::filesystem::current_path(
std::filesystem::path(path).parent_path());
return;
}
#endif
auto exeDir = std::filesystem::path(argv0).parent_path();
if (!exeDir.empty()) std::filesystem::current_path(exeDir);
}
int main(int argc, char** argv) {
SetWorkingDirToExePath(argv[0]);
.Name = PROJECT_NAME,
Game* game = new Game();
return app.Run(game);
}
static void Parse(int argc, char **argv)
Parses argv into the value/flag tables. Call once from main(), before Application.
static void Init(const std::string &ProjectName)
Creates the console + file sinks and registers both loggers. Call once at startup.
Call Sleak::CommandLine::Parse before constructing the Application. The application reads its window size and backend choice out of the parsed table, so skipping this step silently disables every flag.
Once it is in place you get these for free:
| Flag | Effect |
| -r vulkan\|opengl\|d3d11\|d3d12 | Choose the graphics backend |
| -w <n> / -h <n> | Window width and height |
| -t My_Window_Title | Window title, with _ standing in for spaces |
| --fullscreen | Start fullscreen |
Add your own flags anywhere in your game with Sleak::CommandLine::GetValue("-seed") and Sleak::CommandLine::HasFlag("--fly"). To print usage for --help, register a callback with Sleak::CommandLine::SetHelpCallback before Parse.
Sleak::Logger::Init must run before any logging macro. After it, use SLEAK_LOG, SLEAK_INFO, SLEAK_WARN, and SLEAK_ERROR with fmt-style {} placeholders from anywhere.
4. Write the game class
Sleak::GameBase owns your scene registry. Three methods are pure virtual, so you implement all three even if two stay empty at first.
#ifndef _GAME_HPP_
#define _GAME_HPP_
public:
Game() = default;
~Game() override = default;
void Loop(
float deltaTime)
override;
private:
bool bIsGameRunning = true;
};
#endif
virtual void Loop(float DeltaTime)=0
Per-frame update, called after the active scene's own update.
virtual bool GetIsGameRunning()=0
virtual bool Initialize()=0
One-time setup before the loop starts. Return false to abort the run.
virtual void Begin()=0
Called once after Initialize(), before the first Loop().
#include "Game.hpp"
#include "Scenes/FirstScene.hpp"
bool Game::Initialize() {
auto* first = new FirstScene();
AddScene(first);
SetActiveScene(first);
return true;
}
void Game::Begin() {}
void Game::Loop(float deltaTime) {}
void SetVSync(bool enabled)
void SetMSAASampleCount(uint32_t samples)
static Application * GetInstance()
The one Application for this process, or null before construction.
AddScene hands ownership to GameBase, which unloads and deletes every registered scene in its destructor. SetActiveScene deactivates whatever was active and activates the new one, which triggers the scene's load and Begin. Returning false from Initialize aborts the run before the loop starts.
Loop runs once per frame, after the active scene has updated. Anything global to your game and independent of the current scene goes here.
5. Write the scene
Sleak::Scene gives you the full lifecycle. Override only what you need; each override should call its base implementation.
#ifndef _FIRST_SCENE_HPP_
#define _FIRST_SCENE_HPP_
public:
FirstScene() :
Sleak::Scene(
"FirstScene") {}
~FirstScene() override = default;
void Update(
float deltaTime)
override;
};
#endif
void Begin() override
Activates every owned object; runs once after Initialize().
void Update(float deltaTime) override
Advances all active, root-level objects by deltaTime, then steps lighting and physics.
Root namespace for everything the engine exposes.
The lifecycle hooks, in the order they fire:
| Hook | When it runs |
| OnLoad() | Once, when the scene is first loaded. Load assets here. |
| Initialize() | Once, after OnLoad. |
| Begin() | Once, after Initialize. Build your object graph here. |
| OnActivate() | Every time the scene becomes the active scene. |
| Update(dt) | Every frame while active. |
| FixedUpdate(dt) | On the fixed timestep, unless you call SetFixedUpdateEnabled(false). |
| LateUpdate(dt) | Every frame, after all Update calls. |
| OnDeactivate() | When another scene takes over. |
| OnUnload() | Once, on teardown. Release what OnLoad acquired. |
Now populate it. This is the whole scene: a material, a cube, a skybox, three lights, and a camera.
#include "Scenes/FirstScene.hpp"
#include <cmath>
void FirstScene::Begin() {
mat->SetShader("assets/shaders/default_shader.hlsl");
mat->SetDiffuseColor((uint8_t)230, (uint8_t)230, (uint8_t)230);
mat->SetMetallic(0.0f);
mat->SetRoughness(0.35f);
mat->SetAO(1.0f);
mat->SetOpacity(1.0f);
cube->SetTag("Cube");
AddObject(cube);
"assets/skybox/right.jpg", "assets/skybox/left.jpg",
"assets/skybox/top.jpg", "assets/skybox/bottom.jpg",
"assets/skybox/front.jpg", "assets/skybox/back.jpg"}));
sun->SetColor(1.0f, 0.95f, 0.85f);
sun->SetIntensity(4.0f);
sun->SetCastShadows(true);
AddObject(sun);
fill->SetColor(0.6f, 0.75f, 1.0f);
fill->SetIntensity(0.8f);
fill->SetCastShadows(false);
AddObject(fill);
if (auto* lm = GetLightManager()) {
lm->SetAmbientColor(0.08f, 0.10f, 0.15f);
lm->SetAmbientIntensity(0.6f);
lm->SetFogColor(0.55f, 0.62f, 0.78f);
lm->SetFogDistances(40.0f, 100.0f);
lm->SetFogEnabled(true);
}
auto* camera =
camera->SetLookTarget(target);
if (auto* ctrl =
ctrl->SetEnabled(true);
ctrl->SetYaw(std::atan2(forward.
GetX(), forward.
GetZ()));
ctrl->SetPitch(-std::asin(forward.
GetY()));
}
AddObject(camera);
SetActiveCamera(camera);
}
void FirstScene::Update(float deltaTime) {
}
bool Initialize() override
Derives initial yaw/pitch from the camera's current facing direction.
static GameObject * CreateCube(Math::Vector3D position)
A few things worth pulling out of that block.
Objects belong to the scene. AddObject transfers ownership. Never delete a GameObject yourself: call RemoveObject to destroy it now, or DestroyObject to queue it for the end of the frame. Sleak::Scene::Begin is called last on purpose, since it activates everything you just added.
Lights are GameObjects too. Sleak::DirectionalLight derives from Sleak::GameObject, so it goes in through AddObject like anything else, and the scene registers it with the Sleak::LightManager for you.
CreateCube and friends are prototyping helpers. Sleak::GameObject::CreatePlane, CreateCube, CreateSphere, CreateCapsule, CreateCylinder, and CreateTorus build a mesh and transform for you. Real content comes in through Sleak::ModelLoader::Load, covered in the next guide.
The camera controller needs Initialize() called explicitly before you query it, because it derives yaw and pitch from the camera's current facing. Syncing yaw and pitch afterward keeps the first mouse movement from snapping the view.
6. Build and run
cmake -S . -B build
cmake --build build
./bin/MyGame
Drive the free-look camera with W/A/S/D, hold LCTRL to move faster, and steer with the mouse. F11 toggles fullscreen and Esc closes the window.
Try a different backend to confirm the abstraction holds:
./bin/MyGame -r opengl -w 1600 -h 900
Where to go next