Tutorial 03: Your First CNA Window

CNA Tutorial Series  ·  Beginner

What you’ll learn

  • Laying out a minimal CNA project directory.
  • Writing the CMakeLists.txt that links against CNA.
  • Deriving from Game and running an empty window.

Before you startTutorial 02: Setting Up Your Dev Environment — you need a built CNA before you can link against it.

Project Structure

We will create a tiny standalone game project that lives outside the CNA source tree. This mirrors how a real game project would be structured. Create a new directory alongside your cloned repositories:

my-cna-workspace/
├── sharp-runtime/
├── cna/
└── my-first-game/        <-- create this
    ├── CMakeLists.txt
    ├── src/
    │   ├── MyGame.hpp
    │   └── MyGame.cpp
    └── main.cpp
mkdir my-first-game
cd my-first-game
mkdir src

CMakeLists.txt

Create my-first-game/CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(MyFirstGame LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# --- Locate CNA ---
# CNA must be cloned as a sibling of this project.
# Adjust the path if your layout differs.
set(CNA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cna")

# Add CNA as a subdirectory so we can use its targets.
# The C++ framework has no general install/export package; add_subdirectory is the way in.
# Choose the renderer before including CNA.
set(CNA_GRAPHICS_RENDERER "OPENGLES3" CACHE STRING "CNA graphics renderer")
add_subdirectory(${CNA_DIR} ${CMAKE_BINARY_DIR}/cna)

# --- Our game executable ---
add_executable(MyFirstGame
    main.cpp
    src/MyGame.cpp
)

target_link_libraries(MyFirstGame PRIVATE CNA)

# Copy assets next to the executable
add_custom_command(TARGET MyFirstGame POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_CURRENT_SOURCE_DIR}/assets
        $<TARGET_FILE_DIR:MyFirstGame>/assets
    COMMENT "Copying assets"
)

The CNA CMake target exposes CNA's include directories and, in this default SDL3 platform/audio configuration, links SDL3 transitively. You only need to call target_link_libraries(YourGame PRIVATE CNA); an SDL2/Headless/Terminal or non-SDL3-audio build resolves a different dependency closure.

The Minimal Game Class

Every CNA game starts with a class that extends Game. You override the lifecycle methods you need. Here is the minimal version that opens a window and clears it to the traditional XNA cornflower blue.

src/MyGame.hpp

#pragma once

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

class MyGame final : public Game {
public:
    MyGame();

protected:
    void Initialize() override;
    void LoadContent() override;
    void Update(GameTime& gameTime) override;
    void Draw(const GameTime& gameTime) override;
    void UnloadContent() override;

private:
    GraphicsDeviceManager graphics_;
};

src/MyGame.cpp

#include "MyGame.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"

MyGame::MyGame()
    : graphics_(this)   // GraphicsDeviceManager takes a pointer to the Game
{
    // Set window title and size before Initialize() is called
    setWindowTitle("My First CNA Game");
    graphics_.setPreferredBackBufferWidthProperty(800);
    graphics_.setPreferredBackBufferHeightProperty(600);
}

void MyGame::Initialize() {
    // Called once after the graphics device is created.
    // Call the base implementation first.
    Game::Initialize();
}

void MyGame::LoadContent() {
    // Load textures, sounds, fonts here.
    // Nothing to load yet.
}

void MyGame::Update(GameTime& gameTime) {
    (void)gameTime;  // unused for now
    // Game logic goes here.
}

void MyGame::Draw(const GameTime& gameTime) {
    (void)gameTime;

    // Get a reference to the graphics device
    auto& device = getGraphicsDeviceProperty();

    // Clear the back buffer to cornflower blue
    device.Clear(Color::CornflowerBlue);

    // Present the rendered frame to the screen
    device.Present();
}

void MyGame::UnloadContent() {
    // Free any resources loaded in LoadContent().
}

main.cpp

#include "src/MyGame.hpp"

int main() {
    MyGame game;
    game.Run();
    return 0;
}

Linking CNA

The CMakeLists.txt above already handles linking. Here is what happens under the hood when you call target_link_libraries(MyFirstGame PRIVATE CNA):

  • CNA's include/ directory is added to your include path, so #include "Microsoft/Xna/Framework/Game.hpp" resolves.
  • sharp-runtime headers (at ../sharp-runtime/include/) are transitively included.
  • SDL3, SDL3_image, and SDL3_mixer are linked as static libraries built from submodules.
  • The selected renderer (OPENGLES3) is linked. This tutorial uses the default single-renderer mode; an opt-in multi-renderer build can contain several, but its choice still latches before the first device is created.

CNA is an INTERFACE library, which is why you link against it but cannot build it: cmake --build build --target CNA fails. Build your own executable target instead, as shown below.

You should not need to set any CMAKE_PREFIX_PATH or find SDL3 manually.

Running Your Window

Build and run:

cd my-first-game
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3 -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc)
./build/MyFirstGame

You should see a window titled "My First CNA Game" with a solid cornflower blue background. Close it by clicking the window's close button — we have not added keyboard input yet.

What CornflowerBlue means

Color::CornflowerBlue is the traditional default clear color in XNA. Its RGBA value is (100, 149, 237, 255). Every XNA tutorial starts with this color — it is a nod to the original XNA "Getting Started" tutorial from 2006.

Troubleshooting

ErrorCauseFix
Could not find CNA targetCMake path to cna/ is wrongAdjust CNA_DIR in CMakeLists.txt
sharp-runtime not foundsharp-runtime not cloned as siblingClone sharp-runtime next to cna/
easy-gl / meta-gl not foundThe GL-profile renderers live in sibling checkoutsClone easy-gl and meta-gl next to cna/, or select SDL_RENDERER instead
Configure fails on libavcodecFFmpeg development packages missingOn Linux and macOS FFmpeg is required with no opt-out — install the dev packages listed in Tutorial 02
OpenGL context failedGPU driver missingInstall Mesa or proprietary drivers; try the SDL_RENDERER renderer
Black windowdevice.Present() missingEnsure Draw() calls device.Present() at the end

With a working window, you are ready to understand what CNA calls when it runs your game. Continue to Tutorial 04: The Game Class Lifecycle.