Tutorial 20: Building and Running Your Game

Build System  ·  CMake  ·  Emscripten  ·  Android

ℹ

What you’ll learn

  • Producing a release build and sanity-checking it with ctest.
  • Packaging the executable on Windows and Linux.
  • Building for WebAssembly with Emscripten and an overview of the Android APK path.
  • Bundling assets with CPack.

Before you start — Tutorial 02: Setting Up Your Dev Environment (a configured toolchain) and Tutorial 03: Your First CNA Window (a project to build).

CNA uses CMake as its build system. This tutorial covers release builds for Linux and Windows, the ctest sanity check, WebAssembly via Emscripten, Android APK packaging, and a CMakeLists.txt install target that bundles your game with its assets.

CMake release build (Linux)

## Configure a release build with the OPENGLES3 renderer
cmake -S . -B build-release \
      -DCMAKE_BUILD_TYPE=Release \
      -DCNA_GRAPHICS_RENDERER=OPENGLES3

## Build only your game target (faster than building everything)
cmake --build build-release --target MyGame --parallel

## Run
./build-release/MyGame

CMake’s Release configuration enables optimization (-O3 with GCC/Clang, /O2 with MSVC) and defines NDEBUG; it also leaves out the debug information a Debug build carries. CNA sets no default build type, so always pass -DCMAKE_BUILD_TYPE=Release (or a preset) yourself. CPU-bound scenes, and the CPU-rasterizer renderers in particular, run dramatically faster than in Debug; measure your own game rather than trusting a fixed ratio.

Two things to know about a CNA build tree. On native Linux with CMake 3.27 or newer and GCC or Clang, CNA links itself once into libcna.so (CNA_SHARED_LIBRARY, on by default) and your executable links that library; pass -DCNA_SHARED_LIBRARY=OFF for a statically linked engine. And your project consumes CNA with add_subdirectory, so set CNA_BUILD_TESTS and CNA_BUILD_EXAMPLES to OFF before adding it (as in Tutorial 03) or every CNA test and demo is built with your game.

Test sanity check

Before shipping, run CNA’s test suite on the machine and configuration you ship from, to confirm your dependencies and build configuration are sane. Do this in a CNA checkout build (as in Tutorial 02) rather than through your game’s project, so you get exactly CNA’s own test targets:

cd ../cna
cmake -S . -B build-release \
      -DCMAKE_BUILD_TYPE=Release \
      -DCNA_GRAPHICS_RENDERER=OPENGLES3

# One of the 22 focused test executables (fast):
cmake --build build-release --target CnaMathTests
./build-release/CnaMathTests

# The aggregate binary, run directly; use xvfb-run on a machine without a display:
cmake --build build-release --target CnaTests
SDL_AUDIODRIVER=dummy xvfb-run -a ./build-release/CnaTests

The suite should come back clean. How many tests run depends on the exact configuration: general GoogleTest cases are joined by renderer/platform-specific registrations for the implementations compiled into that build. CNA’s own tests preset recommends running the CnaTests binary directly rather than a bare ctest: CTest starts one process per case, and the registered inventory also includes example and smoke executables that the CnaTests target does not build. Use ctest -R <pattern> for individual cases. If something fails, record the renderer, platform and audio selections with the result.

⚠

Do not try cmake --build build-release --target CNA. CNA is an INTERFACE library with no sources, so there is nothing to build under that name and CMake will refuse. Build your own game target, build CnaTests or one of the focused test targets, or omit --target entirely.

Windows executable

:: Configure (PowerShell or cmd.exe)
cmake -S . -B build-win-release ^
      -DCMAKE_BUILD_TYPE=Release ^
      -DCNA_GRAPHICS_RENDERER=SDL_RENDERER

:: Build
cmake --build build-win-release --target MyGame --config Release

:: Run
build-win-release\Release\MyGame.exe

The vendored SDL3, SDL3_image and SDL3_mixer are shared libraries on Windows (SDL3.dll and friends). CNA’s own demos copy them next to the executable with a helper CNA defines, cna_copy_sdl_runtime(<target>); that helper is not applied to your target automatically, so call it yourself after add_subdirectory (if(WIN32) cna_copy_sdl_runtime(MyGame) endif()) or copy the DLLs by hand. A MinGW build also needs its C++ runtime DLLs, for which CNA provides cna_copy_mingw_cxx_runtime(<target>). FFmpeg is never built for Windows, so Video is unavailable there.

The command above uses SDL_RENDERER, the Windows default: it is 2D only, and it needs no sibling checkouts beyond sharp-runtime. If you want a native Direct3D path instead, two renderers are gated to a Windows configure — DIRECTX9 and DIRECTX11. Swap the flag, for example -DCNA_GRAPHICS_RENDERER=DIRECTX11; naming one of them on a non-Windows configure is a hard FATAL_ERROR. The repository’s native-Windows workflows (MSVC builds of the Direct3D 11 renderer and of the SDL3 platform on Windows) are manual-dispatch only, and no automatic workflow builds for Windows. Test on the machines you intend to ship to.

Linux executable packaging

For distributing to end users on Linux, bundle your executable with its assets:

# In CMakeLists.txt — install target
install(TARGETS MyGame DESTINATION .)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/assets DESTINATION .)
install(FILES ${CMAKE_SOURCE_DIR}/README.txt DESTINATION .)

# Build the install tree
cmake --install build-release --prefix dist/linux-x64

# dist/linux-x64/ now contains:
#   MyGame
#   assets/
#   README.txt
⚠

Check what the installed binary needs. CNA has no install() rules for the C++ framework, so install(TARGETS MyGame) installs only your executable. At this snapshot the vendored SDL libraries are shared libraries built into .sdl-prebuilt-* inside the CNA checkout, and with the default CNA_SHARED_LIBRARY=ON the engine itself is libcna.so in your build tree. A binary that runs from the build directory can therefore fail on another machine (or after cmake --install strips the build RPATH). Run ldd on the installed executable, ship what it lists that is not a system library, or configure with -DCNA_SHARED_LIBRARY=OFF to fold the engine back into the executable.

For AppImage packaging, run linuxdeploy on the install prefix. For Flatpak, add a CNA module to your manifest and point to the install prefix.

Emscripten WebAssembly build

CNA’s CI builds for the web with Emscripten SDK 6.0.3 and no other version; older releases are unproven. Install and activate that release (./emsdk install 6.0.3 && ./emsdk activate 6.0.3, then source emsdk_env.sh), and see Tutorial 81 for the full walkthrough.

⚠

Web builds need a web renderer. OPENGLES3 and OPENGL33 are hard-gated off under Emscripten and will stop the configure with a FATAL_ERROR. The browser renderers are WEBGL2 (the Emscripten default) and the experimental WEBGPU.

## Configure with the Emscripten toolchain
emcmake cmake -S . -B build-web \
      -DCMAKE_BUILD_TYPE=Release \
      -DCNA_GRAPHICS_RENDERER=WEBGL2

## Build
cmake --build build-web --target MyGame

## Output files:
##   build-web/MyGame.js
##   build-web/MyGame.wasm
##   build-web/MyGame.html   (auto-generated shell page)
##   build-web/MyGame.data   (packaged assets)

Assets are packaged into the .data file by Emscripten’s --preload-file mechanism. A blocking Game::Run() on the web needs Asyncify, and CNA does not add it to executables outside its own source tree, so your final executable links CNA::EmscriptenAsyncify itself. In your CMakeLists.txt:

if(EMSCRIPTEN)
    set_target_properties(MyGame PROPERTIES SUFFIX ".html")
    # -sASYNCIFY=1 (a blocking Game::Run() suspends between browser frames)
    target_link_libraries(MyGame PRIVATE CNA::EmscriptenAsyncify)
    target_link_options(MyGame PRIVATE
        -sALLOW_MEMORY_GROWTH=1
        -sFORCE_FILESYSTEM=1
        "SHELL:--preload-file ${CMAKE_SOURCE_DIR}/assets@/assets")
    # -sMIN_WEBGL_VERSION / -sMAX_WEBGL_VERSION for a WEBGL2 build
    cna_apply_emscripten_renderer_link_contract(MyGame)
endif()

CNA builds SDL3 itself from its submodule (statically, under Emscripten), so no -sUSE_SDL port flags are needed. CNA’s CI links CNA’s own example programs, not an external consumer, so if your link fails compare with how cna_demo_2d is linked in modules/graphics/examples/CMakeLists.txt.

Serve the output directory with any HTTP server — browsers block file:// access to .wasm files:

cd build-web
python3 -m http.server 8080
# Open http://localhost:8080/MyGame.html

CNA also ships a web CMake preset that configures a WEBGL2 release build of CNA’s cna_house3d_demo into cmake-build-web. The preset carries no toolchain file, so run it through Emscripten’s wrapper: emcmake cmake --preset web, then cmake --build --preset web.

What the web build does differently

  • Stack-allocated Game objects are fine in this snapshot. alpha.1 unwound the caller of Game::Run() and required a heap-allocated Game. Since Game::Run() now suspends through Asyncify and returns normally, MyGame game; game.Run(); works on the web as it does natively — provided your executable links CNA::EmscriptenAsyncify as shown above. If you build against the alpha.1 tag instead, allocate with new.
  • Saves persist through IndexedDB, except in threaded builds. CNA mounts IDBFS for StorageDevice and restores it before main(), so saves survive a reload; a threaded build uses WasmFS by default and needs -DCNA_EMSCRIPTEN_USE_WASMFS=OFF for that. See Tutorial 19 and Tutorial 124.
  • No video playback. FFmpeg is never built for Emscripten. The Video and VideoPlayer types still link, but opening a Video or calling VideoPlayer::Play throws System::NotSupportedException. Guard video features with #ifdef CNA_VIDEO_AVAILABLE. The same is true on Windows, Android and iOS.

Android APK overview

Android support in CNA uses SDL3’s Android build infrastructure. The high-level steps are:

  1. Install an Android NDK and SDK. The one Android project in CNA’s tree (the Devices demo) pins NDK 30.0.14904198, minSdkVersion 24, targetSdkVersion 35 and the arm64-v8a ABI only; nothing in CMake checks these, and no other combination is verified.
  2. Copy the SDL3 Android project template from third_party/SDL/android-project/.
  3. Configure the Gradle build to reference your CNA library as a native dependency.
  4. Place assets in src/main/assets/.
  5. Run ./gradlew assembleRelease to produce an APK.
## CMake cross-compile for Android ARM64
cmake -S . -B build-android \
      -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
      -DANDROID_ABI=arm64-v8a \
      -DANDROID_PLATFORM=android-24 \
      -DCNA_BUILD_TESTS=OFF \
      -DCNA_BUILD_EXAMPLES=OFF \
      -DCNA_GRAPHICS_RENDERER=SDL_RENDERER \
      -DCMAKE_BUILD_TYPE=Release

cmake --build build-android --target MyGame

See Tutorial 82 for the Gradle/JNI shape. Be aware that Android code paths exist, but there is no CMake preset and no CI job that builds or runs them, so treat an Android build as something you verify yourself. SDL_RENDERER is the Android default; OPENGLES3 additionally needs the EasyGL and meta-gl siblings and a device with OpenGL ES 3.0, and has no Android build evidence here. FFmpeg is never built for Android, so video is unavailable for the same reason as on the web.

Packaging assets with CPack

CPack can produce a ZIP, DEB, or NSIS installer automatically:

## CMakeLists.txt additions for CPack
set(CPACK_PACKAGE_NAME        "MyGame")
set(CPACK_PACKAGE_VERSION     "1.0.0")
set(CPACK_PACKAGE_VENDOR      "My Studio")
set(CPACK_GENERATOR           "ZIP;DEB")   # or NSIS on Windows

include(CPack)
install(TARGETS MyGame DESTINATION bin)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/assets DESTINATION share/MyGame)
## Generate the package
cmake --build build-release --target package

## Output:
##   build-release/MyGame-1.0.0-Linux.zip
##   build-release/MyGame-1.0.0-Linux.deb

Full CMakeLists.txt example

cmake_minimum_required(VERSION 3.20)
project(MyGame VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Pull in CNA (assumes sibling directory layout), without its own tests and demos
set(CNA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CNA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(../cna cna-build)

add_executable(MyGame
    src/main.cpp
    src/MyGame.cpp
    src/states/MainMenuState.cpp
    src/states/GameplayState.cpp
)

target_link_libraries(MyGame PRIVATE CNA)

# Copy assets to build dir for development convenience
add_custom_command(TARGET MyGame POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_SOURCE_DIR}/assets $<TARGET_FILE_DIR:MyGame>/assets
)

# Install rules
install(TARGETS MyGame DESTINATION .)
install(DIRECTORY assets DESTINATION .)

# CPack
set(CPACK_PACKAGE_NAME    "MyGame")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_GENERATOR       "ZIP")
include(CPack)