Introducing Awen — A C++23 Game Engine
Written by, Funnan on March 23, 2026
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++23 — the baseline standard, with
std::expected, ranges, and more on the roadmap. The long-term plan is to migrate to C++26 to experiment with the static reflection proposal. - Raylib — a straightforward, dependency-light graphics library that gets a window on screen without a lot of ceremony.
- CMake 3.28+ with presets — every platform target is expressed as a named preset. One command configures, one builds, no per-platform scripts to maintain.
- vcpkg — all C++ dependencies (Raylib, GoogleTest) are declared in
vcpkg.jsonand resolved automatically during the CMake configure step. - GitHub Actions — five separate workflows, one per platform, run on every push and pull request to
main.
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:
-
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’sandroid.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. -
The vcpkg toolchain — Awen uses a custom wrapper toolchain (
cmake/android/toolchain.cmake) that setsVCPKG_CHAINLOAD_TOOLCHAIN_FILEto point at the NDK toolchain before includingvcpkg.cmake. This ensures vcpkg builds its own dependencies (Raylib included) forarm64-android, not for the host machine. -
Gradle configuration —
android/app/build.gradle.ktspasses the CMake toolchain, the target and host triplets, and a-DBUILD_SHARED_LIBS=OFFflag 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:
clang-format-check— runsclang-format --dry-run -Werroracross all.cpp,.h, and.ixxfiles underapp/andsrc/. Any formatting deviation is a hard error.clang-format— runs the same pass with-i, applying fixes in place.
# 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:
clang-tidy— runsrun-clang-tidyacross the full source tree in parallel, using the compile commands database generated during configure.clang-tidy-diff— runs analysis only on files changed relative tomainusingclang-tidy-diff. This is the fast path during development: you get actionable feedback on your diff without waiting for a full-tree analysis.- The
modernize-*checks in particular reinforce the C++23 direction of the project — flagging older idioms and pointing toward[[nodiscard]], trailing return types, range-based patterns, and similar modern conventions.
# 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.