Introducing Awen — A C++23 Game Engine

Written by, Funnan on March 23, 2026

engineeringopen-source

This past weekend I kicked off a new open source project live on Twitch: Awen, a 2D game engine written in C++23. The goal isn’t to ship a competitor to Unity — it’s to use game engine development as a vehicle for exploring what modern C++ has to offer. Design patterns, API design, architecture, the latest language features — Awen is where I get to think through all of that in public.

The full source is on GitHub at funnansoftwarellc/awen.


The Name

Awen is a Welsh and Celtic concept meaning divine inspiration — the living, flowing spirit of creative energy. In druidic tradition, awen represents the spark that moves through a craftsperson or poet: not just skill, but the drive to make something that didn’t exist before. Its symbol is three rays of light emanating outward, suggesting energy radiating in all directions.

That resonates with what I want this project — and Funnan Software — to be about.

C++ is a language I genuinely love working in. Awen is an opportunity to bring that enthusiasm into the open: to explore what’s possible with modern C++, to share hard-won experience as a professional, and to build something compelling and creative at the same time. A game engine touches almost every interesting problem in software — memory, concurrency, platform abstractions, API design, performance. It’s an ideal vehicle for the kind of deep, deliberate exploration the name implies.

The same spirit applies to the company. Funnan Software exists to build things we genuinely care about — games, tools, experiences that reflect real craft. Awen, with all its ambition and rough edges, is that in practice: inspiration made visible, worked on in public, refined over time.


The Stack

Before getting into platform setup, here’s what the project is built on:


C++ Modules

One of the first design decisions was to use C++23 named modules instead of headers for the engine’s own code. The core module lives in src/core/Awen.ixx:

module;

export module awen;

export namespace awn
{
    class Awen
    {
    public:
        auto set_value(int x) noexcept -> void
        {
            value_ = x;
        }

        [[nodiscard]] auto get_value() const noexcept -> int
        {
            return value_;
        }

    private:
        int value_;
    };
}

The export module awen; declaration turns this translation unit into a named module. Consumers import it with import awen; — no header, no include guards, no textual inclusion. The module boundary also acts as a hygiene layer: nothing in the implementation leaks into the consumer unless it’s explicitly exported.

On the CMake side, modules require the FILE_SET CXX_MODULES syntax introduced in CMake 3.28:

add_library(awen-core)

target_sources(awen-core PUBLIC
    FILE_SET cxx_modules TYPE CXX_MODULES FILES
        Awen.ixx
)

This is what tells CMake — and the underlying compiler — that Awen.ixx is a module interface unit, not a regular source file. Without it, the build system won’t generate the BMI (Binary Module Interface) file and dependency edges correctly.

The .ixx extension is a MSVC convention that most tooling now understands. Clang and GCC both handle it fine when CMake is managing the file set.


Getting It Building Everywhere

The CMake presets file (CMakePresets.json) at the project root simply includes five platform-specific preset files:

{
    "version": 7,
    "include": [
        "cmake/preset/platform/android.json",
        "cmake/preset/platform/linux.json",
        "cmake/preset/platform/osx.json",
        "cmake/preset/platform/wasm.json",
        "cmake/preset/platform/windows.json"
    ]
}

Each file defines the configure and build presets for that platform — compiler selection, toolchain, vcpkg integration, output directories. The result is that every platform is configured with the same shape of command: cmake --preset <name>.


macOS

Requirements: macOS 14 (Sonoma) or later, Apple Silicon, Homebrew, LLVM 21.

Awen targets Clang on macOS rather than Apple Clang because C++23 module support in Apple’s toolchain is still incomplete. LLVM 21 via Homebrew is the fix:

brew install llvm@21
echo 'export PATH="/opt/homebrew/opt/llvm@21/bin:$PATH"' >> ~/.zprofile
source ~/.zprofile

After that, clone the repo, bootstrap vcpkg, and build:

git clone https://github.com/funnansoftwarellc/awen.git
cd awen
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh

cmake --preset arm64-osx-clang-debug
cmake --build --preset arm64-osx-clang-debug
ctest --preset arm64-osx-clang-debug

Available presets: arm64-osx-clang-debug, arm64-osx-clang-release.


Linux

Requirements: Ubuntu 25.10 (or compatible), GCC or Clang.

The repo ships a Dev Container (.devcontainer/linux/) that includes everything — Ubuntu 25.10, GCC, Clang, CMake, Ninja — and is the recommended way to work on Linux. If you’d rather set up natively:

sudo apt-get update
sudo apt-get install -y cmake ninja-build git pkg-config python3 zip unzip tar

Then install GCC or Clang per the instructions in the Dockerfile. The build commands are the same shape:

cmake --preset x64-linux-clang-debug
cmake --build --preset x64-linux-clang-debug
ctest --preset x64-linux-clang-debug

Linux has four presets covering GCC and Clang in both Debug and Release configurations: x64-linux-gcc-debug, x64-linux-gcc-release, x64-linux-clang-debug, x64-linux-clang-release.


Windows

Requirements: Windows 10/11, Visual Studio 2022 with the Desktop development with C++ workload.

Run from a Developer Command Prompt for VS 2022 (or after invoking vcvarsall.bat amd64) so that MSVC and the Windows SDK are on the path:

git clone https://github.com/funnansoftwarellc/awen.git
cd awen
git clone https://github.com/microsoft/vcpkg.git
.\vcpkg\bootstrap-vcpkg.bat

cmake --preset x64-windows-msvc-release
cmake --build --preset x64-windows-msvc-release
ctest --preset x64-windows-msvc-release
cmake --build --preset x64-windows-msvc-release --target install

The installed binary lands in build/<preset>/installed/bin/. Presets: x64-windows-msvc-debug, x64-windows-msvc-release.


Android

Android was by far the most involved target to get right, and the details are worth spelling out because the failure modes are subtle.

Requirements: Android NDK r29, SDK, JDK (handled automatically in the Dev Container). The project targets arm64-v8a and cross-compiles from either Linux or Windows.

The recommended path is the Android Dev Container (.devcontainer/android/), which runs Ubuntu 25.10 with Clang and NDK r29 pre-installed. If you’re setting up manually, the three things that have to align are:

  1. The NDK path — Gradle reads it from local.properties (written by the CMake toolchain), and the CMake toolchain reads it to chain-load the NDK’s android.toolchain.cmake. Getting this path consistent between Gradle and CMake — especially on Windows where backslash paths in CMake cache files cause parse errors — took the most debugging time.

  2. The vcpkg toolchain — Awen uses a custom wrapper toolchain (cmake/android/toolchain.cmake) that sets VCPKG_CHAINLOAD_TOOLCHAIN_FILE to point at the NDK toolchain before including vcpkg.cmake. This ensures vcpkg builds its own dependencies (Raylib included) for arm64-android, not for the host machine.

  3. Gradle configurationandroid/app/build.gradle.kts passes the CMake toolchain, the target and host triplets, and a -DBUILD_SHARED_LIBS=OFF flag so that all vcpkg dependencies are statically linked. Only the app target itself is built as a shared library (required by Android’s JNI loader).

Once those three pieces are aligned, the build reduces to the same CMake preset idiom:

cmake --preset x64-linux-clang-arm64-android-debug
cmake --build --preset x64-linux-clang-arm64-android-debug
cmake --build --preset x64-linux-clang-arm64-android-debug --target install

Windows host presets are also available: x64-windows-clang-arm64-android-debug / x64-windows-clang-arm64-android-release.


WebAssembly

Requirements: Emscripten. The .devcontainer/wasm/ container (based on emscripten/emsdk) is the recommended environment.

WebAssembly cross-compiles from Linux. Emscripten acts as both the compiler and the CMake toolchain — vcpkg detects this and builds Raylib for wasm32-emscripten automatically:

cmake --preset wasm32-emscripten-debug
cmake --build --preset wasm32-emscripten-debug
cmake --build --preset wasm32-emscripten-debug --target install

Output lands in build/<preset>/installed/. Presets: wasm32-emscripten-debug, wasm32-emscripten-release.


GitHub Actions CI

Every platform has its own workflow file in .github/workflows/. All five trigger on push and pull request to main, with concurrency groups configured to cancel in-progress runs when a newer commit arrives — keeping CI responsive during active development.

The Linux, Android, and WebAssembly workflows share a pattern: a docker job first builds (or restores from cache) the platform-specific container image, then a build job runs inside that container. This keeps the build environment fully reproducible without relying on the runner’s pre-installed tools.

For vcpkg, a reusable composite action (.github/actions/get-vcpkg) configures the environment variables and restores a binary cache keyed on vcpkg.json and vcpkg-configuration.json. On a cache hit, vcpkg skips recompiling dependencies entirely — a meaningful save when Raylib is in the graph.

The Linux workflow also runs clang-format-check and clang-tidy-diff as separate lint jobs, so formatting and static analysis are enforced in CI the same way as the build.


Code Quality as a First-Class Citizen

One principle that runs through the whole project: anything enforced in CI should be runnable at your desk with a single command, using nothing beyond what you already have to build the project. No separate scripts, no extra tooling to install, no special instructions. If it passes locally, it passes in CI.

Awen enforces this through two tools — clang-format and clang-tidy — both exposed as CMake targets defined in cmake/target/.

Formatting with clang-format

Two targets are registered when clang-format is found on the path:

# Check for formatting violations (what CI runs)
cmake --build --preset x64-linux-clang-debug --target clang-format-check

# Auto-fix formatting locally
cmake --build --preset x64-linux-clang-debug --target clang-format

The style is configured in .clang-format at the repo root, derived from the Google C++ style. If clang-format isn’t on the path, CMake emits a warning and skips registering the targets — no hard failure, no broken configure step.

Static Analysis with clang-tidy

The .clang-tidy configuration enables a broad set of checks: bugprone-*, cert-*, cppcoreguidelines-*, modernize-*, readability-*, and performance-*. All warnings are treated as errors (WarningsAsErrors: "*"), so anything that would cause CI to fail will cause a local build to fail too.

Three targets are available:

# Full analysis
cmake --build --preset x64-linux-clang-debug --target clang-tidy

# Analyze only your changes vs main (fast, ideal during development)
cmake --build --preset x64-linux-clang-debug --target clang-tidy-diff

Like clang-format, these targets are skipped gracefully if the executables aren’t found. And because they’re CMake targets, they work identically in the Dev Container, on a local machine, and inside GitHub Actions — the same commands, the same rules, the same results.

The Linux CI workflow runs clang-format-check and clang-tidy-diff as parallel lint jobs on every push. There’s no gap between what the pipeline enforces and what a developer can verify before pushing — that’s the point.


What’s Next

The project is in its early days — the current app is a Raylib triangle on screen — but the infrastructure is solid. From here the focus is on building out the C++23 feature set: std::expected for error handling without exceptions, ranges for data pipeline idioms, and the module graph expanding as the engine grows.

The longer-term goal is a migration to C++26 to experiment with static reflection once it lands in the major compilers. Using Awen as a testbed for that is exactly the kind of exploration the project is built for.

If you want to follow along, I stream development at twitch.tv/funnansoftware and post updates here. The source is open — contributions and questions welcome.