CMake for Complex Projects: C++ Game Engine for Desktop and WebAssembly

Practical guide to modern CMake for a cross-platform game engine (Windows, Linux, and WebAssembly). Learn to structure complex projects, manage dependencies (vendoring, FetchContent), optimize builds with PCH and generator expressions, and prepare packaging in the second installment.

sábado, 16 de agosto de 2025 • 8 min read • Q2BSTUDIO Team

Artificial-Intelligence-

A practical guide to modern CMake applied to a real Cpp game engine project and build management

If you have ever tried to compile a Cpp project with multiple dependencies and cross-platform support, you know what it entails. Makefiles become unmanageable, Visual Studio solutions don't work on Linux, and distributing a library to third parties can be a pain. CMake solves many of these problems, but many tutorials show simple examples with a single file. Here we will go deeper with a complex project: a complete engine called ColumbaEngine with more than 80 source files, support for native and web platforms via Emscripten, and an advanced build system.

In this article, the first part of a two-part series, we will focus on compilation configuration and build management: how to structure the CMake project, handle dependencies, support cross-platform builds, and set up a testing infrastructure. In the second part, we will cover deployment, packaging, and installation.

Q2BSTUDIO is a software development and custom applications company specialized in custom software solutions, artificial intelligence, cybersecurity, and aws and azure cloud services. We offer business intelligence services, AI for companies, AI agent development, and Power BI consulting to improve decision-making. Our experience in complex projects and custom software allows us to apply the best CMake and software lifecycle practices.

Project description and why it is interesting: an engine with more than 80 Cpp files organized into modules such as ECS, Renderer, Audio, and UI; cross-platform with Windows, Linux, and WebAssembly; multiple dependencies such as SDL2, OpenGL, and FreeType; an installable library so other projects can consume it via find_package; application examples and generation of installable packages like deb and rpm.

Recommended project structure: root folder with main CMakeLists txt, src directory with Engine and Editor submodules, examples folder for demo games, import for vendored dependencies, cmake for custom modules, and test for unit tests. This separation facilitates reproducible builds and custom software maintenance.

Step 1 Initial CMake configuration and options: define cmake minimum required to leverage modern features, declare configurable options to enable or disable examples, static builds or time traces, and adjust global variables like export compile commands and Cxx standard to 17. These options allow developers and CI systems to customize the build easily and are best practices recommended by Q2BSTUDIO when we deliver custom solutions.

Step 2 The dependency challenge: in real projects, dependencies abound. Main strategies: vendor dependencies inside import to control versions and allow offline builds; use managers like conan or vcpkg for lighter repos and precompiled binaries; use CMake FetchContent to declare git repos or tarballs and bring libraries at configuration time. Each approach has advantages and disadvantages in repo size, reproducibility, build times, and security. For game engines and custom software products, Q2BSTUDIO usually recommends a hybrid approach: vendor critical dependencies and use FetchContent or managers for less critical or development dependencies.

Summary comparison of strategies: vendoring provides absolute control and reproducibility but increases repository size and requires manual updates; package managers offer binary packages and easy updates but introduce an external dependency and potential conflicts; FetchContent is native to CMake and very flexible but compiles from source on each initial configuration, without binary caching by default.

Recommended hybrid pattern: vendor stable and critical libraries like SDL2, use FetchContent for development tools like googletest, and find package with fallback for optional utilities like Doxygen. This balances control, reproducibility, and ease of maintenance, ideal for enterprise deliveries and custom software projects.

Step 3 Cross-platform support: detect CMAKE SYSTEM NAME for platform conditionals. For Emscripten, prefer the libraries provided by the web build system and adjust link and optimization flags. For native builds, compile dependencies from source with add subdirectory. Keeping GLM as a header-only dependency makes it easy to use on all platforms. At Q2BSTUDIO, we apply these techniques in projects that must run on servers, desktops, and browsers via WebAssembly.

Step 4 Create the main target: group the engine's source files into a list and create a static or shared library depending on configuration. Using target precompile headers on stdafx h or a custom precompiled header significantly reduces compilation times, especially in large projects with many common includes. For enterprise solutions we deliver as custom software, optimizing build times is critical for team productivity.

Precompiled headers explanation and best practices: the problem is the parsing cost of standard and third-party headers in each compilation unit. Include only stable and massively used headers in the PCH, avoid headers that change frequently. Differentiate between PUBLIC and PRIVATE so library consumers benefit from the PCH when appropriate. In Q2BSTUDIO projects, this usually reduces clean builds from minutes to a fraction, accelerating development and testing cycles.

Step 5 Usage requirements and generator expressions: modern CMake allows declaring target include directories and using BUILD INTERFACE for paths during the build and INSTALL INTERFACE for paths when the library is installed. Generator expressions allow conditionals per configuration and platform without polluting global variables. This is essential for maintaining reusable and well-packaged libraries, a common requirement for custom software projects and cloud solutions like aws and azure cloud services.

Examples of common patterns with generator expressions: apply flags per compiler, link to different libraries in Debug and Release, and platform-specific links. This approach avoids global ifs and supports multi-config generators like Visual Studio or Xcode, something Q2BSTUDIO considers essential when preparing integrations with corporate CI CD systems.

Step 6 Linking and dependency scope PUBLIC PRIVATE INTERFACE: correctly declaring dependencies avoids compilation errors in consumer projects. PUBLIC indicates a dependency necessary for the public interface, PRIVATE is only for implementation, and INTERFACE describes a dependency that the consumer must inherit even if the target does not use it in its implementation. Understanding transitivity avoids surprises in projects that integrate third-party libraries and is key in microservices architectures or modular components developed by Q2BSTUDIO.

Practical example of transitive dependencies: if ColumbaEngine exposes SDL2 types in its headers, SDL2 must be PUBLIC so applications using the engine get the necessary include paths and libraries. For header-only libraries, use INTERFACE and thus transmit dependencies like Eigen or glm to consumers.

Step 7 Multiple executables and examples: an engine includes tools, an editor, and game examples. Creating multiple executables that link against the same main library allows compiling the engine once and reusing the artifact in multiple applications. This pattern reduces times and facilitates delivering demos and examples to clients and end users in custom software projects.

Step 8 Testing infrastructure: enable testing, add googletest as a submodule or via FetchContent, and use gtest discover tests so ctest automatically discovers cases. Having integrated tests is a requirement in many Q2BSTUDIO contracts, especially when we offer business intelligence services, where automatic verification of calculations is critical.

Common pitfalls and solutions: avoid global variables like modifying CMAKE CXX FLAGS directly, prefer per-target properties; use generator expressions instead of configuration-time conditionals to ensure per-configuration and per-target behavior; correctly choose dependency scopes to avoid leaking implementation details to consumers.

Practical results achieved with these practices: improved developer experience with simple and reproducible build instructions, cross-platform compatibility with a single CMakeLists txt, automatic dependency resolution per platform, and build optimizations that reduce times through PCH and parallel compilation. This allows Q2BSTUDIO to deliver custom software solutions faster and with higher quality, integrating artificial intelligence and cybersecurity measures when the project requires it.

Example developer workflow: clone repository, create build directory, run cmake with custom options for builds and examples, run parallel make and ctest to run tests. For deployment and packaging in corporate environments, Q2BSTUDIO prepares the second part of this series covering installation, export import, package configuration, and CPack to generate professional installers.

Coming soon Part 2 will cover installation and distribution: how to make find package friendly, export and import mechanisms for modern libraries, create relocatable and dependent packages, integrate CPack, and generate installers for target platforms. These capabilities are key when we deliver cloud solutions or on-premise installations for clients requiring custom software and aws and azure cloud services.

Conclusion: building a robust build system with CMake for complex projects requires thinking in terms of targets, handling dependencies with the right strategy, abstracting cross-platform differences, and using generator expressions and target properties instead of global variables. Applying these practices achieves reproducible, maintainable, and optimized builds, essential for artificial intelligence solutions, AI for companies, AI agents, and projects requiring integration with power bi and business intelligence services.

Q2BSTUDIO accompanies companies throughout the entire cycle: from custom software architecture design to the implementation of artificial intelligence and cybersecurity, including deployments on aws and azure cloud services and business intelligence solutions with Power BI. If your project needs custom applications, custom software, AI agents, or cybersecurity services, contact Q2BSTUDIO for professional advice and scalable solutions.

Don't miss the second part where we will show how to properly package and distribute your library and cross-platform applications so other developers and clients can easily consume them.

What is your biggest challenge with CMake in real projects? Share your experience and questions, and at Q2BSTUDIO we will help you find the best strategy for your case.

OUR SERVICES

How we can help you

Do you have a project in mind?

Tell us your vision and we'll turn it into a software solution. Whatever the scope, we make your idea real.