<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Columba Engine Blog</title><description>Articles about automation, AI, and local tooling.</description><link>https://columbaengine.org/</link><language>en-us</language><item><title>How I Run Local AI with n8n on a Schedule (No Server, No API Costs)</title><link>https://columbaengine.org/blog/n8n_startup/</link><guid isPermaLink="true">https://columbaengine.org/blog/n8n_startup/</guid><description>A simple local setup lets n8n start a local AI server on a schedule, process batch jobs, and shut everything down without paying for always-on infrastructure or API calls.</description><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Most AI workflows run 24/7 and waste resources. Mine runs once a day, on my own machine, processes everything, and shuts down before I even wake up.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Why always-on AI is wasteful&lt;/h2&gt;
&lt;p&gt;A lot of AI workflows are set up to run continuously by default, but plenty of them don’t actually need real-time execution. If what you’re doing is batch-style monitoring, scraping, content processing, or scheduled analysis, keeping a server alive all day is mostly wasted uptime. You end up paying for idle compute, ongoing server costs, and infrastructure that just sits there between runs.&lt;/p&gt;
&lt;p&gt;That’s the problem this setup is meant to solve. Not every AI system needs to be on 24/7. For batch-style workflows, it often makes more sense to run on a schedule, finish the work, and shut down.&lt;/p&gt;
&lt;p&gt;I wanted to avoid paying for a server that would sit idle most of the day, so I started running everything on my own machine instead. Once I did that, the question changed. It stopped being “how do I keep this AI workflow running all the time?” and became “when does it actually need to run?” In my case, the answer was simple: wake up at 5AM, process everything in one batch, then shut down cleanly. That’s really the whole idea here. For a lot of AI automations, better timing beats more infrastructure.&lt;/p&gt;
&lt;h2&gt;A concrete example: my Reddit monitoring pipeline&lt;/h2&gt;
&lt;p&gt;In my case, the pipeline is for Reddit monitoring. Once a day, n8n wakes up, fetches posts from the sources I care about, and runs them through a local AI step that filters noise and keeps the signal.&lt;/p&gt;
&lt;p&gt;I don’t need to watch Reddit in real time. I don’t need a system running all day to track every new post or comment. I just need one scheduled pass that gathers fresh content, sorts through the volume, and leaves me with a short list of posts worth reading.&lt;/p&gt;
&lt;p&gt;That’s why this works well as a batch job. Fetch posts, filter noise, keep signal.&lt;/p&gt;
&lt;p&gt;The workflow is simple once you see the shape of it: Reddit -&gt; fetch, AI -&gt; analyze/filter, Store -&gt; results. First, the scheduled job starts the local model server. Then n8n fetches the Reddit posts, sends them through the model to filter out the noise, and keeps the useful items. After that, the workflow stores the results and stops the model cleanly.&lt;/p&gt;
&lt;p&gt;That order matters because the AI server only exists for the duration of the batch. Start the model, process everything in one pass, store the output, and shut it down.&lt;/p&gt;
&lt;h2&gt;Treat your machine like a scheduled worker&lt;/h2&gt;
&lt;p&gt;The basic mental model is simple: let the machine wake up on a schedule, do the job, and shut down. Instead of keeping AI infrastructure alive all day, you bring your own computer online only when there’s work to do. That’s a much better fit for batch jobs, monitoring, scraping, and other tasks that don’t need constant uptime.&lt;/p&gt;
&lt;p&gt;This works best when the job already has a natural batch shape. Monitoring, scraping, and batch processing are good examples because they can run on a schedule, finish, and get out of the way. If a workflow only needs a daily insight, then daily compute is enough.&lt;/p&gt;
&lt;p&gt;There’s an obvious boundary here. This is not a fit for chat apps, live agents, or anything else that depends on low-latency interaction. Those workloads need something that stays responsive all the time. This setup is for doing one focused pass on a local machine, processing the data, and shutting everything down cleanly.
Here’s your section rewritten with the &lt;strong&gt;actual working setup (Windows + n8n + schtasks + llama.cpp)&lt;/strong&gt; while keeping your tone and flow.&lt;/p&gt;
&lt;h2&gt;What n8n needs for this to work&lt;/h2&gt;
&lt;p&gt;One n8n capability makes this possible: &lt;strong&gt;Execute Command&lt;/strong&gt;. You need it for local automation because it lets the workflow trigger processes on your own machine. In practical terms, this is what allows n8n to start your local LLM server at the right moment, use it during the workflow, and shut it down afterward.&lt;/p&gt;
&lt;p&gt;It’s disabled by default, so you have to enable it first. If you’re running n8n locally on Windows, set this environment variable before starting n8n:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;$env:NODES_EXCLUDE = &quot;[]&quot;
n8n start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That removes &lt;code&gt;executeCommand&lt;/code&gt; and &lt;code&gt;readWriteFile&lt;/code&gt; from the excluded nodes list and gives you what matters here: the ability to run local scripts from inside your workflow.&lt;/p&gt;
&lt;p&gt;This setup only works in the right environment. n8n Cloud won’t allow it, and Docker setups are usually restricted for this kind of direct system access. The configuration that works is a &lt;strong&gt;local n8n instance running on the same Windows machine&lt;/strong&gt; that will launch the LLM server, run the workflow, and shut it down again.&lt;/p&gt;
&lt;h2&gt;Starting the local AI server from n8n&lt;/h2&gt;
&lt;p&gt;Once n8n can execute commands locally, the next step is launching your LLM server on demand. In my case, that’s &lt;code&gt;llama.cpp&lt;/code&gt;, but the exact model doesn’t matter. What matters is the pattern: n8n triggers something, the server starts, and the rest of the workflow uses it.&lt;/p&gt;
&lt;p&gt;The important detail is that you don’t call the script directly from n8n. Instead, you wrap it in a &lt;strong&gt;Windows scheduled task&lt;/strong&gt;, and n8n only triggers that task.&lt;/p&gt;
&lt;p&gt;Here’s the actual start script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# start-llama.ps1
$ErrorActionPreference = &quot;Stop&quot;

$Root = &quot;Z:\Ai\llama.cpp&quot;
$Exe  = Join-Path $Root &quot;build\bin\Release\llama-server.exe&quot;
$PidFile = Join-Path $Root &quot;llama-server.pid&quot;

Start-Process `
    -FilePath $Exe `
    -ArgumentList &quot;-m models\mistral-7b.gguf --port 8081 -ngl 999 -np 1 -cb -fa --mlock --no-mmap -t 4&quot; `
    -WorkingDirectory $Root `
    -WindowStyle Hidden

# wait until server is ready
$ready = $false
for ($i = 0; $i -lt 180; $i++) {
    Start-Sleep -Seconds 1
    try {
        $resp = Invoke-WebRequest -Uri &quot;http://127.0.0.1:8081/health&quot; -UseBasicParsing -TimeoutSec 2
        if ($resp.StatusCode -eq 200) {
            $ready = $true
            break
        }
    } catch {}
}

if (-not $ready) {
    throw &quot;llama-server did not become ready&quot;
}

# resolve PID after startup
$proc = Get-CimInstance Win32_Process |
    Where-Object {
        $_.Name -eq &quot;llama-server.exe&quot; -and
        $_.CommandLine -match &quot;--port 8081&quot;
    } |
    Select-Object -First 1

$proc.ProcessId | Set-Content $PidFile
exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script does three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;starts the server in the background,&lt;/li&gt;
&lt;li&gt;waits until it’s actually reachable via HTTP,&lt;/li&gt;
&lt;li&gt;saves the PID so it can be stopped later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That middle step is critical. Instead of guessing with a fixed delay, the script waits until the server is actually ready.&lt;/p&gt;
&lt;h2&gt;The key technical constraint&lt;/h2&gt;
&lt;p&gt;This was the part that broke my first version. n8n’s Execute Command node waits for the command to finish before continuing the workflow. That’s fine for short scripts, but a local LLM server is a long-running process by design.&lt;/p&gt;
&lt;p&gt;If you try to start the server directly from n8n, the workflow just sits there. From n8n’s point of view, the command never finishes, so nothing else runs.&lt;/p&gt;
&lt;p&gt;The problem isn’t the model. It’s that a server is not a one-shot command.&lt;/p&gt;
&lt;p&gt;That’s the core constraint: &lt;strong&gt;Execute Command expects something that exits, but your LLM server is meant to stay alive&lt;/strong&gt;. Until you handle that mismatch, the workflow stalls.&lt;/p&gt;
&lt;h2&gt;The fix: delegate to Windows&lt;/h2&gt;
&lt;p&gt;The fix was to stop letting n8n manage the process directly and let Windows handle it instead.&lt;/p&gt;
&lt;p&gt;Instead of calling the PowerShell script directly, I created a scheduled task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;schtasks /Create /TN &quot;LlamaServerStart&quot; /TR &quot;\&quot;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe\&quot; -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \&quot;Z:\Ai\llama.cpp\start-llama.ps1\&quot;&quot; /SC ONCE /ST 00:00 /F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then from n8n, I only run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;schtasks /Run /TN &quot;LlamaServerStart&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the key difference.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;schtasks /Run&lt;/code&gt; returns immediately&lt;/li&gt;
&lt;li&gt;Windows runs the script independently&lt;/li&gt;
&lt;li&gt;n8n doesn’t get stuck waiting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So instead of trying to force a long-running server into a short-lived command model, I hand off execution to the OS and let n8n just trigger it.&lt;/p&gt;
&lt;p&gt;That’s what makes the workflow actually usable.&lt;/p&gt;
&lt;h2&gt;Shutting the server down cleanly&lt;/h2&gt;
&lt;p&gt;Stopping the server uses the same pattern.&lt;/p&gt;
&lt;p&gt;First, the stop script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# stop-llama.ps1
$Root = &quot;Z:\Ai\llama.cpp&quot;
$PidFile = Join-Path $Root &quot;llama-server.pid&quot;

$pidValue = Get-Content $PidFile -ErrorAction SilentlyContinue

if ($pidValue -and (Get-Process -Id $pidValue -ErrorAction SilentlyContinue)) {
    Stop-Process -Id $pidValue -Force
}

Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create the task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;schtasks /Create /TN &quot;LlamaServerStop&quot; /TR &quot;\&quot;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe\&quot; -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \&quot;Z:\Ai\llama.cpp\stop-llama.ps1\&quot;&quot; /SC ONCE /ST 00:00 /F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And call it from n8n:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;schtasks /Run /TN &quot;LlamaServerStop&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why this setup works&lt;/h2&gt;
&lt;p&gt;The key idea is simple once you see it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;n8n orchestrates&lt;/li&gt;
&lt;li&gt;Windows executes&lt;/li&gt;
&lt;li&gt;the LLM runs independently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The PID file connects start and stop, the HTTP check guarantees readiness, and &lt;code&gt;schtasks&lt;/code&gt; prevents n8n from blocking on a long-running process.&lt;/p&gt;
&lt;p&gt;That combination is what makes the start–use–stop cycle reliable.&lt;/p&gt;
&lt;p&gt;Without it, you’re fighting the execution model of n8n. With it, everything behaves like a normal service lifecycle: bring it up, use it, shut it down cleanly, and start fresh on the next run.&lt;/p&gt;
&lt;h2&gt;Why 5AM is the real trick&lt;/h2&gt;
&lt;p&gt;The timing is what makes this setup practical instead of just clever on paper. I scheduled mine for 5AM, when my machine is idle and nothing else I’m doing is competing with it. That gives the workflow a quiet compute window to start the local LLM server, run the batch job, and shut everything back down before the day begins.&lt;/p&gt;
&lt;p&gt;And 5AM is just my choice, not a rule. The broader idea is to schedule around your own idle time. For batch work, scraping, monitoring, or any once-a-day automation, that shift makes local compute a lot more practical.&lt;/p&gt;
&lt;h2&gt;Results and trade-offs&lt;/h2&gt;
&lt;p&gt;What this setup buys me is pretty simple: zero infrastructure cost beyond the machine I already own, automated daily insights, and time saved. In practice, the machine wakes up, checks Reddit once a day, filters out the noise, and leaves me with a small set of posts worth reading. I don’t have to pay for an extra always-on server or babysit the process. It’s not magic, and it won’t fit every use case, but for a scheduled batch job like this, it’s genuinely useful.&lt;/p&gt;
&lt;p&gt;The trade-offs are real too. This only works if the machine is on at the scheduled time, so it’s best for a local-only setup where you control the hardware. There’s also some scripting complexity, since you need to start the LLM server in the background, track its process, and stop it cleanly afterward. That’s a good fit for batch jobs, monitoring, and other workflows that don’t need 24/7 uptime, but it’s not the right fit for everything.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;That’s the main lesson I took from this: most AI automations don’t need a bigger stack, a cloud bill, or a server running all day. They need better timing. If the job is batchable, run it in the quiet window, spin up the local model just long enough to do the work, then shut it down.&lt;/p&gt;
&lt;p&gt;Once you stop treating every workflow like a 24/7 service, the whole problem gets simpler. You don’t need more infrastructure. You need better timing.&lt;/p&gt;</content:encoded></item><item><title>Persistent Storage in C++ Web Apps: WASMFS + OPFS with Emscripten</title><link>https://columbaengine.org/blog/wasmfs-opfs/</link><guid isPermaLink="true">https://columbaengine.org/blog/wasmfs-opfs/</guid><description>How to replace IDBFS with the newer WASMFS + OPFS backend in an Emscripten C++ project, get rid of JS callbacks, and handle saves correctly across browser lifecycle events</description><pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to use WASMFS to get persistent storage for a web app in C++&lt;/h1&gt;
&lt;p&gt;Ever wanted to create an app in C++ and serve it on the web, but you need to store some data of the user and you can&apos;t find any guide on the web?
Here I will show you how to use the OPFS backend of the new WASMFS storage layer for any app built with emscripten.&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;I am making a game engine fully written in C++ and I want to use it to create some small scope games for game jams and such, so a web export is
pretty much mandatory to have players test and play those games on itch. So for my web builds I naturally use emscripten for this.&lt;/p&gt;
&lt;p&gt;What is emscripten? Emscripten is a cross compiling tool that enables devs to port a C++ app to webassembly so that it can run in browsers.
It works really well with my usecase as I use SDL as the backend for all of my render calls and input handling and it is backed into
emscripten so it pretty much builds out of the box without doing too much handling on my part.&lt;/p&gt;
&lt;p&gt;But one of the major issues I faced using emscripten was the handling of files and more specifically the persistent storage of my saves.
There are multiple ways given by this tool to access files. First, for all the static files (such as resources or shaders), you can precompile them
directly into the binary with the linker flag &lt;code&gt;--preload-file&lt;/code&gt;. This directly compiles the files into the wasm and you can access them with any basic file
accessor such as &lt;code&gt;fopen&lt;/code&gt; or &lt;code&gt;fstream&lt;/code&gt;.
This works really well for all the files known at compile time and is quite easy to use for the tradeoff of having a bigger &lt;code&gt;.wasm&lt;/code&gt; size.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;set_target_properties(${TARGET} PROPERTIES
    LINK_FLAGS &quot;--preload-file res \
                --preload-file shader \
                --preload-file scripts&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those files get baked in and are read-only, which is fine for shaders, scripts or any asset that doesn&apos;t change at runtime.&lt;/p&gt;
&lt;p&gt;The main issue comes from files that are not present during compile time or need to evolve during runtime such as &lt;strong&gt;save&lt;/strong&gt; files.
For those we need to use a different kind of file handling.&lt;/p&gt;
&lt;p&gt;Thats where WASMFS comes into play.&lt;/p&gt;
&lt;h2&gt;WASMFS&lt;/h2&gt;
&lt;p&gt;So if you skim through the emscripten docs, you will see that there are a lot of different ways to access, manage and store files in emscripten.
For a quick runthrough you have access to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IDBFS&lt;/strong&gt; — the original persistent storage backend, backed by the browser&apos;s IndexedDB. It works but requires you to synchronize explicitly using &lt;code&gt;EM_ASM&lt;/code&gt; and JavaScript callbacks every time you want to flush data to disk. It&apos;s now being deprecated in favor of WASMFS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PROXYFS&lt;/strong&gt; — lets you proxy filesystem calls to another Emscripten worker or shared memory. Useful for sharing a filesystem between workers but not really relevant if you just want to persist save data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WASMFS&lt;/strong&gt; — the new filesystem layer. It&apos;s a fully C++ API, supports multiple backends (including OPFS), and handles threading out of the box. This is the one we want.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here the two mains filesystem solutions that interest us are the IDBFS backend and the newer WASMFS + OPFS backend.&lt;/p&gt;
&lt;h2&gt;Why WASMFS over IDBFS&lt;/h2&gt;
&lt;p&gt;First, IDBFS is being deprecated for the newer WASMFS so if you want to future-proof your app and you don&apos;t know which to choose,
it is better to onboard with the more user friendly WASMFS.
Next is a simpler and fully C++ API for WASMFS so the integration is faster and easier with existing C++ code, whereas before the IDBFS functions like mounting or flushing a directory
required you to drop into &lt;code&gt;EM_ASM&lt;/code&gt; and JavaScript callbacks to manage the filesystem.
Finally you get better threading support as WASMFS supports it out of the box, something that was not supported by the previous implementation.&lt;/p&gt;
&lt;p&gt;That said, be careful: WASMFS still doesn&apos;t have support for all backends, so if you are targeting browsers that don&apos;t support OPFS you will still need to work with the old
API if you want to have access to persistent storage.&lt;/p&gt;
&lt;h2&gt;Let&apos;s get into it&lt;/h2&gt;
&lt;p&gt;Now that we know why we want WASMFS, let&apos;s go through the actual setup step by step.&lt;/p&gt;
&lt;h3&gt;CMake configuration&lt;/h3&gt;
&lt;p&gt;So first things first, before you can even think about mounting OPFS you need to have the right linker flags in your CMake. The three that matter here are &lt;code&gt;-sWASMFS&lt;/code&gt; to enable the new filesystem layer, &lt;code&gt;-s FORCE_FILESYSTEM=1&lt;/code&gt; so the filesystem actually gets linked even if emscripten doesn&apos;t detect any explicit FS calls in your code, and then &lt;code&gt;-pthread&lt;/code&gt; with &lt;code&gt;-sPTHREAD_POOL_SIZE&lt;/code&gt; for threading:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;set_target_properties(${TARGET} PROPERTIES
    LINK_FLAGS &quot;-sWASMFS \
                -s FORCE_FILESYSTEM=1 \
                -sPTHREAD_POOL_SIZE=4 \
                -pthread \
                -s ALLOW_MEMORY_GROWTH=1&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to be careful about with the thread pool size: WASMFS internally defers its async operations to a background thread, so you always need to account for that extra thread on top of whatever your app already uses. If you don&apos;t allocate enough threads your app can just stall waiting on IO with no obvious error.&lt;/p&gt;
&lt;h3&gt;Mounting the OPFS backend&lt;/h3&gt;
&lt;p&gt;Now for the actual mounting, first you need to include the wasmfs header, under an &lt;code&gt;__EMSCRIPTEN__&lt;/code&gt; guard of course:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#ifdef __EMSCRIPTEN__
#include &amp;#x3C;emscripten/wasmfs.h&gt;
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then before you start your main loop you create the backend and mount it to a directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#ifdef __EMSCRIPTEN__
    std::string savePath = &quot;/&quot; + config.saveFolder;   // e.g. &quot;/save&quot;
    backend_t backend = wasmfs_create_opfs_backend();
    int err = wasmfs_create_directory(savePath.c_str(), 0777, backend);
    if (err != 0 &amp;#x26;&amp;#x26; errno != EEXIST)
        printf(&quot;Warning: OPFS mount returned %d (errno=%d)\n&quot;, err, errno);
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things to watch out for here. First the path needs to start with &lt;code&gt;/&lt;/code&gt;, that&apos;s just how emscripten&apos;s virtual filesystem works, the root is always &lt;code&gt;/&lt;/code&gt;. On desktop you use relative paths but on web everything has to be absolute. Second don&apos;t worry about &lt;code&gt;EEXIST&lt;/code&gt; in your error check, that just means the directory was already there from a previous session which is actually the happy path for save data.&lt;/p&gt;
&lt;p&gt;Since the path format differs between builds you need a small conditional when you construct it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string constructSavePath() const
{
#ifdef __EMSCRIPTEN__
    return &quot;/&quot; + saveFolder + &quot;/&quot; + saveSystemFile;
#else
    return saveFolder + &quot;/&quot; + saveSystemFile;
#endif
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On desktop you get &lt;code&gt;save/system.sz&lt;/code&gt;, on web &lt;code&gt;/save/system.sz&lt;/code&gt;. Same logical location, just the web one maps into the OPFS-backed virtual directory you just mounted.&lt;/p&gt;
&lt;h3&gt;Reading and writing files&lt;/h3&gt;
&lt;p&gt;This is actually the nicest part of WASMFS compared to IDBFS: once the backend is mounted you don&apos;t need any special API to read or write to it. Standard C++ file I/O just works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Write
std::ofstream out{&quot;/save/system.sz&quot;};
out &amp;#x3C;&amp;#x3C; data;

// Read
std::ifstream in{&quot;/save/system.sz&quot;};
std::string content((std::istreambuf_iterator&amp;#x3C;char&gt;(in)),
                     std::istreambuf_iterator&amp;#x3C;char&gt;());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WASMFS transparently routes anything under &lt;code&gt;/save/&lt;/code&gt; to the OPFS backend and persists it across sessions. No manual flush, no sync call, no JS callbacks. That&apos;s pretty much the whole point of switching to it.&lt;/p&gt;
&lt;h3&gt;Saving on browser lifecycle events&lt;/h3&gt;
&lt;p&gt;One thing that can catch you off guard: the browser doesn&apos;t guarantee your writes make it through if the user just slams the tab closed. To cover that you need to hook into two browser lifecycle events.&lt;/p&gt;
&lt;p&gt;The first is &lt;code&gt;visibilitychange&lt;/code&gt; which fires when the tab becomes hidden, so things like switching tabs or minimizing the window. The second is &lt;code&gt;beforeunload&lt;/code&gt; which fires synchronously right before the page unloads on refresh or close. Together they cover pretty much all the cases:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#ifdef __EMSCRIPTEN__
    // Tab hidden: user switched tabs, minimized, etc.
    emscripten_set_visibilitychange_callback(nullptr, false,
        [](int, const EmscriptenVisibilityChangeEvent* e, void*) -&gt; EM_BOOL {
            if (e-&gt;hidden)
                saveNow();
            return EM_TRUE;
        });

    // Page close / refresh
    emscripten_set_beforeunload_callback(nullptr,
        [](int, const void*, void*) -&gt; const char* {
            saveNow();
            return nullptr;  // nullptr = no &quot;Are you sure?&quot; dialog
        });
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;beforeunload&lt;/code&gt; fires synchronously so a blocking write is fine there. &lt;code&gt;visibilitychange&lt;/code&gt; is the one that covers mobile browsers where &lt;code&gt;beforeunload&lt;/code&gt; is unreliable, so you really want both.&lt;/p&gt;
&lt;h3&gt;Threading and initialization order&lt;/h3&gt;
&lt;p&gt;The last piece of the puzzle is the initialization order, and this one matters more than it looks. The full sequence needs to go like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exec()
  ├─ wasmfs_create_opfs_backend()     ← must happen before the main loop
  ├─ wasmfs_create_directory(...)
  ├─ SDL_Init(...)
  ├─ SDL_CreateWindow(...)
  ├─ std::thread initThread { initializeWindow() }   ← window/ECS init in background
  └─ emscripten_set_main_loop_arg(mainLoopCallback)  ← hands control to the browser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The OPFS mount has to happen before &lt;code&gt;emscripten_set_main_loop_arg&lt;/code&gt; because WASMFS internally creates promises that need a running browser event loop to resolve. If you mount after handing control to the browser you can end up in a situation where the promises never resolve.&lt;/p&gt;
&lt;p&gt;The window initialization goes into a background thread so the main loop can start right away and give those promises room to process. Then in the main loop callback you just check an atomic flag before doing anything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static void mainLoopCallback(void* arg)
{
    Engine* engine = ...;

    if (not engine-&gt;windowReady.load())
        return;  // still waiting for initThread

    if (not engine-&gt;initialized)
    {
        engine-&gt;initializeECS();
        engine-&gt;initialized = true;
    }

    // normal render / event loop
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what avoids the deadlock where all your pthread pool threads are sitting blocked on OPFS operations with nobody left to run the event loop and resolve the pending promises. If you ever find your app just hanging on startup with no error, this is probably why.&lt;/p&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;That&apos;s all there is to it. WASMFS with the OPFS backend is honestly a much cleaner solution than what came before — no JS callbacks, no manual syncing, just standard C++ file I/O that persists across sessions. The only real gotchas are the thread pool size and the initialization order, and now you know about both.&lt;/p&gt;
&lt;p&gt;If this was useful to you or you ran into a different issue with the setup feel free to leave a comment, I&apos;d be happy to extend the article with more edge cases.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you are curious about the engine this was built for, PgEngine is a C++ game engine targeting both desktop and web. You can check it out and leave a star on the repo here: &lt;a href=&quot;https://github.com/gallasko/ColumbaEngine&quot;&gt;ColumbaEngine repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Live demos at: &lt;a href=&quot;https://columbaengine.org/demos&quot;&gt;columbaengine.org/demos&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discord:&lt;/strong&gt; &lt;a href=&quot;https://discord.gg/un4VtehX3W&quot;&gt;Join our community&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you played one of the games made with it and want to leave some feedback or just say hi, you can find them on my itch page here: &lt;a href=&quot;https://pigeoncodeur.itch.io/&quot;&gt;Itch&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>The Code Generator Journey: From Manual Hell to Declarative Heaven</title><link>https://columbaengine.org/blog/component-generator/</link><guid isPermaLink="true">https://columbaengine.org/blog/component-generator/</guid><description>How we built a YAML-to-C++ code generator in PgScript, eliminated 559+ lines of boilerplate, and made ECS components 6x faster to create</description><pubDate>Fri, 30 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;The Problem: Writing the Same Code for the 50th Time&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Picture this: You&apos;re building a game engine with a pure Entity Component System (ECS) architecture. You need to add a new component—let&apos;s call it &lt;code&gt;HealthComponent&lt;/code&gt;. Simple enough, right? You need a few fields: &lt;code&gt;currentHealth&lt;/code&gt;, &lt;code&gt;maxHealth&lt;/code&gt;, and &lt;code&gt;isDead&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But wait. In practice, here&apos;s what you actually need to write:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Component struct definition&lt;/strong&gt; - Fields, getters, setters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event-firing setters&lt;/strong&gt; - Notify systems when values change&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VM proxy schema&lt;/strong&gt; - Expose component to scripting system&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serialization code&lt;/strong&gt; - Save/load support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attach handlers&lt;/strong&gt; - Let scripts create and attach components&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For our &quot;simple&quot; HealthComponent, that&apos;s easily 100+ lines of boilerplate code. And you&apos;ll write it all by hand. Again. For the 50th time.&lt;/p&gt;
&lt;h3&gt;The Real Cost: PositionComponent&lt;/h3&gt;
&lt;p&gt;Let me show you what &quot;boilerplate&quot; really means with a real example from ColumbaEngine. Our &lt;code&gt;PositionComponent&lt;/code&gt; handles 2D position, size, rotation, and visibility. It has 8 fields. Here&apos;s what we had to maintain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;position.h&lt;/strong&gt;: 359 lines - Struct definition, enums, helper structs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;position.cpp&lt;/strong&gt;: 150 lines - Setter implementations, utility functions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ecsserialization.cpp&lt;/strong&gt;: 98 lines - VM proxy and serialization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Total: ~607 lines of code for ONE component with 8 fields.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every time we wanted to add a field:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update the struct definition&lt;/li&gt;
&lt;li&gt;Write getter/setter with event firing&lt;/li&gt;
&lt;li&gt;Add to VM proxy schema&lt;/li&gt;
&lt;li&gt;Update serialization code&lt;/li&gt;
&lt;li&gt;Register with attach handler&lt;/li&gt;
&lt;li&gt;Hope we didn&apos;t forget anything&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Maintenance Nightmare&lt;/h3&gt;
&lt;p&gt;Copy-paste errors were common. Event firing was inconsistent. Adding &lt;code&gt;viewport&lt;/code&gt; to TTFText? I forgot to update the serialization. Position&apos;s &lt;code&gt;rotation&lt;/code&gt; setter wasn&apos;t firing events properly. The serialization for &lt;code&gt;colors&lt;/code&gt; used a different pattern than other fields.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every component felt like writing the same code with slightly different names.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And that&apos;s when I asked: What if we didn&apos;t have to?&lt;/p&gt;
&lt;h2&gt;The Vision: Declarative Components&lt;/h2&gt;
&lt;p&gt;What if defining a component looked like this instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: PositionComponent
namespace: pg
base_class: Component

fields:
  - name: x
    type: float
    default: 0.0f
    setter: event

  - name: y
    type: float
    default: 0.0f
    setter: event

  - name: rotation
    type: float
    default: 0.0f
    setter: event
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it. Just declare what you want. The code writes itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single source of truth&lt;/strong&gt; - One &lt;code&gt;.pgcomp&lt;/code&gt; file defines everything&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency guaranteed&lt;/strong&gt; - Generated code follows the same patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to read and modify&lt;/strong&gt; - YAML is human-friendly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regenerate anytime&lt;/strong&gt; - Change something? Regenerate. No fear.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Build Our Own Generator?&lt;/h2&gt;
&lt;p&gt;Before diving in, we considered alternatives:&lt;/p&gt;
&lt;h3&gt;Option 1: External Templating (Jinja2, Mustache, etc.)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Mature, well-tested
&lt;strong&gt;Cons:&lt;/strong&gt; Python dependency, another language to learn, separate tooling&lt;/p&gt;
&lt;h3&gt;Option 2: Python Generator Scripts&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Flexible, easy to write
&lt;strong&gt;Cons:&lt;/strong&gt; External dependency, not integrated with engine&lt;/p&gt;
&lt;h3&gt;Option 3: C++ Template Metaprogramming&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; No runtime overhead
&lt;strong&gt;Cons:&lt;/strong&gt; Compile-time cost, hard to read, limited flexibility&lt;/p&gt;
&lt;h3&gt;Our Choice: PgScript&lt;/h3&gt;
&lt;p&gt;We chose to write the generator in &lt;strong&gt;PgScript&lt;/strong&gt;, ColumbaEngine&apos;s own scripting language.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dogfooding&lt;/strong&gt; - Use the engine&apos;s own language to build engine tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero external dependencies&lt;/strong&gt; - Ships with the engine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Demonstrates capability&lt;/strong&gt; - Shows PgScript is production-ready&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster iteration&lt;/strong&gt; - No recompilation needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Educational&lt;/strong&gt; - Others can read and learn from it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The decision to use PgScript forced us to make the language better. And that benefited everyone.&lt;/p&gt;
&lt;h2&gt;Building the Foundation: Native Modules&lt;/h2&gt;
&lt;p&gt;Before we could write a component generator in PgScript, we needed to enhance the language itself.&lt;/p&gt;
&lt;h3&gt;String Module: 349 Lines of Utilities&lt;/h3&gt;
&lt;p&gt;A YAML parser needs string manipulation. Lots of it. So we added:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// String inspection
startsWith(str, prefix)    // &quot;PositionComponent&quot; starts with &quot;Position&quot;
endsWith(str, suffix)      // &quot;component.h&quot; ends with &quot;.h&quot;
contains(str, substring)   // &quot;type: float&quot; contains &quot;float&quot;

// String manipulation
substring(str, start, end) // Extract portions
trim(str)                  // Remove whitespace
replace(str, from, to)     // Replace all occurrences

// String transformation
capitalize(str)            // &quot;position&quot; -&gt; &quot;Position&quot;
indexOf(str, substring)    // Find position of substring
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Commit: &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/191babed&quot;&gt;191babed&lt;/a&gt;&lt;/strong&gt; - Added 349 lines to &lt;code&gt;stringmodule.h&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;File Module: Reading and Writing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &quot;file&quot;

var content = readFile(&quot;PositionComponent.pgcomp&quot;)
var success = writeFile(&quot;PositionComponent.generated.h&quot;, generatedCode)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple but essential. The generator reads &lt;code&gt;.pgcomp&lt;/code&gt; files and writes generated &lt;code&gt;.h&lt;/code&gt; and &lt;code&gt;.cpp&lt;/code&gt; files.&lt;/p&gt;
&lt;h3&gt;Algorithm Module: Vector Operations&lt;/h3&gt;
&lt;p&gt;YAML has arrays. Arrays everywhere. Field lists, include lists, method lists. We needed robust array manipulation to build the generator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we added to the algorithm module:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &quot;algorithm&quot;

// Array operations
var fields = []
push(fields, fieldDef)           // Add to end
insert(fields, 0, fieldDef)      // Insert at index
var field = pop(fields)          // Remove and return last
removeAt(fields, 2)              // Remove at specific index

// Length and access
var count = len(fields)          // Get array length
var field = fields[0]            // Access by index

// Type checking and introspection
var type = typeOf(fields)        // Returns &quot;array&quot;
if (typeOf(value) == &quot;string&quot;)   // Dynamic type checking

// Table/object operations
if (contain(currentItem, &quot;name&quot;)) // Check if key exists
{
    var name = currentItem[&quot;name&quot;]
}

// Type conversion
var number = toInt(&quot;42&quot;)         // String to integer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why these functions matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;push()&lt;/code&gt; and &lt;code&gt;pop()&lt;/code&gt;&lt;/strong&gt; - Essential for building arrays during parsing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;len()&lt;/code&gt;&lt;/strong&gt; - Needed for loops and bounds checking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;contain()&lt;/code&gt;&lt;/strong&gt; - Critical for checking if YAML fields exist before accessing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;typeOf()&lt;/code&gt;&lt;/strong&gt; - Dynamic type checking for flexible parsing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;toInt()&lt;/code&gt;&lt;/strong&gt; - Converting string numbers in YAML to integers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Without these utilities, we&apos;d have no way to work with the structured data coming from YAML. Arrays are first-class citizens in the component generator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Commit: &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/4db26392&quot;&gt;4db26392&lt;/a&gt;&lt;/strong&gt; - Added 158 lines of vector manipulation&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Commit: &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/ebc08c06&quot;&gt;ebc08c06&lt;/a&gt;&lt;/strong&gt; - Added &lt;code&gt;typeOf()&lt;/code&gt; utility&lt;/p&gt;
&lt;p&gt;With these modules in place, PgScript had everything needed for the generator: string manipulation, file I/O, and data structure operations.&lt;/p&gt;
&lt;h2&gt;The YAML Parser Challenge&lt;/h2&gt;
&lt;h3&gt;Why YAML?&lt;/h3&gt;
&lt;p&gt;We needed a format for &lt;code&gt;.pgcomp&lt;/code&gt; files that was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Human-readable&lt;/strong&gt; - Developers edit these by hand&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured&lt;/strong&gt; - Support nested objects and arrays&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard&lt;/strong&gt; - Don&apos;t invent a new format&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comment-friendly&lt;/strong&gt; - Documentation inline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;YAML checked all the boxes. But... we had to write a YAML parser. In PgScript.&lt;/p&gt;
&lt;h3&gt;Parser Architecture&lt;/h3&gt;
&lt;p&gt;Here&apos;s the core structure from &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/blob/main/tools/component_generator.pg&quot;&gt;&lt;code&gt;component_generator.pg&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class YamlParser
{
    init()
    {
        this.indentStack = []
    }

    parse(content)
    {
        var lines = splitLines(content)
        var result = {}
        var currentSection = &quot;&quot;
        var currentArray = []
        var currentItem = {}

        for (var i = 0; i &amp;#x3C; len(lines); i++)
        {
            var line = lines[i]
            var trimmed = trim(line)

            // Skip empty lines and comments
            if (len(trimmed) &gt; 0 and not startsWith(trimmed, &quot;#&quot;))
            {
                var indent = this.getIndent(line)

                if (startsWith(trimmed, &quot;-&quot;))
                {
                    // Array item
                    this.parseArrayItem(trimmed, currentItem, currentArray)
                }
                else if (contains(trimmed, &quot;:&quot;))
                {
                    // Key-value pair
                    this.parseKeyValue(trimmed, indent, result, currentSection)
                }
            }
        }

        return result
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Tricky Parts&lt;/h3&gt;
&lt;p&gt;Writing a YAML parser from scratch revealed complexity we didn&apos;t anticipate. Here are the challenges we had to solve:&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;&lt;strong&gt;1. Indentation Tracking&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;YAML uses indentation for nesting. We needed to count spaces to determine structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;getIndent(line)
{
    var indent = 0
    for (var i = 0; i &amp;#x3C; len(line); i++)
    {
        var char = substring(line, i, i + 1)
        if (char == &quot; &quot;)
            indent = indent + 1
        else
            break
    }
    return indent
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The challenge:&lt;/strong&gt; Different indent levels mean different things:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;fields:              # indent = 0 (top-level)
  - name: x          # indent = 2 (array item)
    type: float      # indent = 4 (nested property)
    default: 0.0f    # indent = 4 (same level)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We maintain a state machine tracking current section, current array, and current object. When indent decreases, we know we&apos;re back to a parent level.&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;&lt;strong&gt;2. Array Items with Key-Value Pairs&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;This YAML pattern was particularly tricky:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;fields:
  - name: x          # Array item with nested key-value
    type: float
    default: 0.0f
  - name: y          # Next array item
    type: float
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; The &lt;code&gt;-&lt;/code&gt; starts a new array item, but the following indented lines are properties of that item.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our solution:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var currentItem = {}

if (startsWith(trimmed, &quot;-&quot;))
{
    // Save previous item if it exists
    if (contain(currentItem, &quot;name&quot;) or contain(currentItem, &quot;params&quot;))
    {
        push(currentArray, currentItem)
    }

    // Start new item
    currentItem = {}

    // Parse the line after the dash
    var afterDash = trim(substring(trimmed, 1, len(trimmed)))

    if (contains(afterDash, &quot;:&quot;))
    {
        // &quot;- name: x&quot; format
        var parts = this.splitFirst(afterDash, &quot;:&quot;)
        var key = trim(parts[0])
        var value = trim(parts[1])
        currentItem[key] = this.parseValue(value)
    }
}
else if (indent &gt; 0 and contains(trimmed, &quot;:&quot;))
{
    // Nested property of current item
    var parts = this.splitFirst(trimmed, &quot;:&quot;)
    var key = trim(parts[0])
    var value = trim(parts[1])
    currentItem[key] = this.parseValue(value)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We accumulate properties into &lt;code&gt;currentItem&lt;/code&gt;, then push it to the array when we see the next &lt;code&gt;-&lt;/code&gt; or reach a different section.&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;&lt;strong&gt;3. Type Inference for Values&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;YAML values can be strings, numbers, booleans, arrays, or objects. We needed to parse them correctly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;parseValue(value)
{
    // Parse inline arrays: [item1, item2, item3]
    if (startsWith(value, &quot;[&quot;) and endsWith(value, &quot;]&quot;))
    {
        var innerContent = substring(value, 1, len(value) - 1)
        var items = split(innerContent, &quot;,&quot;)
        var result = []

        for (var i = 0; i &amp;#x3C; len(items); i = i + 1)
        {
            push(result, trim(items[i]))
        }

        return result
    }

    // Parse inline objects: {key1: value1, key2: value2}
    if (startsWith(value, &quot;{&quot;) and endsWith(value, &quot;}&quot;))
    {
        // ... parse object
        return obj
    }

    // Parse booleans
    if (value == &quot;true&quot;)
        return true
    if (value == &quot;false&quot;)
        return false

    // Parse numbers (kept as strings for simplicity)
    // Everything else is a string
    return value
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For simplicity, we keep most values as strings and let the code generator handle type conversion. This avoided complex number parsing.&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;&lt;strong&gt;4. Comment Handling&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Comments (&lt;code&gt;#&lt;/code&gt;) should be ignored:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;fields:
  - name: x           # X coordinate
    type: float       # Using float for precision
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Simple solution:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Skip empty lines and comments
if (len(trimmed) &gt; 0 and not startsWith(trimmed, &quot;#&quot;))
{
    // Process line
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We check this early and skip commented lines entirely.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Parser Statistics&lt;/h3&gt;
&lt;p&gt;The complete YAML parser is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~400 lines&lt;/strong&gt; of PgScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supports:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Top-level key-value pairs&lt;/li&gt;
&lt;li&gt;Nested sections&lt;/li&gt;
&lt;li&gt;Arrays with &lt;code&gt;-&lt;/code&gt; notation&lt;/li&gt;
&lt;li&gt;Objects within arrays&lt;/li&gt;
&lt;li&gt;Inline arrays &lt;code&gt;[a, b, c]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Inline objects &lt;code&gt;{key: value}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Comments&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does NOT support:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Multiline strings&lt;/li&gt;
&lt;li&gt;Anchors and aliases (&lt;code&gt;&amp;#x26;&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Complex multiline syntax (&lt;code&gt;|&lt;/code&gt;, &lt;code&gt;&gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Explicit typing (&lt;code&gt;!!str&lt;/code&gt;, &lt;code&gt;!!int&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Merge keys (&lt;code&gt;&amp;#x3C;&amp;#x3C;:&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this is enough:&lt;/strong&gt; We control the &lt;code&gt;.pgcomp&lt;/code&gt; format. We don&apos;t need full YAML spec compliance—just enough to make component definitions readable and writable by humans.&lt;/p&gt;
&lt;p&gt;The parser works perfectly for our needs, and we can extend it when new features require new YAML constructs.&lt;/p&gt;
&lt;h2&gt;Code Generation: The Heart of the System&lt;/h2&gt;
&lt;p&gt;Now for the interesting part: generating C++ code from YAML definitions.&lt;/p&gt;
&lt;h3&gt;The .pgcomp Format&lt;/h3&gt;
&lt;p&gt;Here&apos;s a complete example - our Health component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: HealthComponent
namespace: pg

includes:
  - &amp;#x3C;cstdint&gt;

fields:
  - name: currentHealth
    type: int32_t
    default: 100
    setter: event

  - name: maxHealth
    type: int32_t
    default: 100
    setter: basic

  - name: isDead
    type: bool
    default: false
    setter: none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, readable, and complete. Let&apos;s see what gets generated.&lt;/p&gt;
&lt;h3&gt;Header Generation&lt;/h3&gt;
&lt;p&gt;From the YAML above, we generate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// PositionComponent.generated.h
// AUTO-GENERATED - DO NOT EDIT

#pragma once

#include &amp;#x3C;cstdint&gt;
#include &quot;ECS/system.h&quot;

namespace pg
{
    struct HealthComponent : public Component
    {
        // Fields
        int32_t currentHealth = 100;
        int32_t maxHealth = 100;
        bool isDead = false;

        // Setters
        void setCurrentHealth(int32_t value);  // event setter
        void setMaxHealth(int32_t value);      // basic setter
        // No setter for isDead (setter: none)

        // ECS integration
        _unique_id id = 0;
        EntitySystem* ecsRef = nullptr;
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clean, consistent, and complete.&lt;/p&gt;
&lt;h3&gt;Setter Levels&lt;/h3&gt;
&lt;p&gt;This is where the system shines. We support &lt;strong&gt;4 levels of setters&lt;/strong&gt;, each for different needs:&lt;/p&gt;
&lt;h4&gt;Level 1: No Setter (Direct Access)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: isDead
  type: bool
  default: false
  setter: none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generated:&lt;/strong&gt; Just the field. Scripts access it directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Derived state, internal flags&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Level 2: Basic Setter&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: maxHealth
  type: int32_t
  default: 100
  setter: basic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generated:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void setMaxHealth(int32_t value) {
    maxHealth = value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Simple assignment with validation opportunity&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Level 3: Event Setter (The Key Innovation)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: currentHealth
  type: int32_t
  default: 100
  setter: event
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generated:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void setCurrentHealth(int32_t value)
{
    if (this-&gt;currentHealth != value) // Only fire if changed!
    {
        this-&gt;currentHealth = value;

        if (ecsRef)
        {
            ecsRef-&gt;sendEvent(HealthComponentChangedEvent{id});
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The optimization:&lt;/strong&gt; &lt;code&gt;if (this-&gt;currentHealth != value)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This one line prevents cascading updates. Before code generation, we wrote this check manually (sometimes). Now it&apos;s guaranteed for every event setter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before the generator,&lt;/strong&gt; we had code like this in PositionComponent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void PositionComponent::setX(float x)
{
    if (areNotAlmostEqual(this-&gt;x, x))  // Sometimes we remembered the check
    {
        LOG_THIS(DOM);
        this-&gt;x = x;

        if (ecsRef)
            ecsRef-&gt;sendEvent(PositionComponentChangedEvent{id});
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But other setters forgot the check. Inconsistent. Now it&apos;s automatic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Components that trigger system updates, fields that affect rendering or game state&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Future: Level 4 - Custom Setters&lt;/h4&gt;
&lt;p&gt;We&apos;re planning a 4th level for complex custom logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: wrap
  type: bool
  default: false
  setter: custom
  custom_code: |
    this-&gt;wrap = value;
    this-&gt;changed = true;  // Custom logic
    recalculateLayout();   // More custom logic
    fireEvent();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will allow embedding arbitrary C++ code for setters that need more than just event firing. &lt;strong&gt;Not yet implemented, but on the roadmap!&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Constructor Generation&lt;/h3&gt;
&lt;p&gt;Components often need constructors with default parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;constructors:
  - params: [text, fontPath, scale, colors]
    defaults: {colors: &quot;{255.0f, 255.0f, 255.0f, 255.0f}&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generated:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;TTFText(const std::string&amp;#x26; text,
        const std::string&amp;#x26; fontPath,
        float scale,
        constant::Vector4D colors = {255.0f, 255.0f, 255.0f, 255.0f})
    : text(text)
    , fontPath(fontPath)
    , scale(scale)
    , colors(colors)
{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Member initialization, default parameters, proper formatting—all automatic.&lt;/p&gt;
&lt;h2&gt;The Bootstrap Problem&lt;/h2&gt;
&lt;p&gt;Now we hit an interesting challenge: &lt;strong&gt;How do you build an engine that needs components when the components are generated by the engine?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is a classic chicken-and-egg problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We need the engine to run PgScript&lt;/li&gt;
&lt;li&gt;PgScript runs the component generator&lt;/li&gt;
&lt;li&gt;The component generator creates components&lt;/li&gt;
&lt;li&gt;The engine needs those components to build&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Solution: The Bootstrap Compiler&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Commit: &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/b7508293&quot;&gt;b7508293&lt;/a&gt;&lt;/strong&gt; - &quot;Can now build a minimal pgcompiler without any graphical deps&quot;&lt;/p&gt;
&lt;p&gt;We created a &lt;strong&gt;minimal PgCompiler&lt;/strong&gt; that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Has &lt;strong&gt;zero graphical dependencies&lt;/strong&gt; (no SDL, no OpenGL)&lt;/li&gt;
&lt;li&gt;Can run PgScript programs&lt;/li&gt;
&lt;li&gt;Compiles separately from the main engine&lt;/li&gt;
&lt;li&gt;Runs during CMake configuration phase&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;CMake Integration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Build minimal bootstrap compiler first
add_executable(pgcompiler_bootstrap
    src/Engine/Compiler/*.cpp
    # Exclude graphical systems
)

# Generate components before main build
file(GLOB PGCOMP_FILES &quot;src/Engine/Components/*.pgcomp&quot;)

foreach(PGCOMP_FILE ${PGCOMP_FILES})
    get_filename_component(COMP_NAME ${PGCOMP_FILE} NAME_WE)

    add_custom_command(
        OUTPUT &quot;${CMAKE_BINARY_DIR}/Generated/${COMP_NAME}.generated.h&quot;
        COMMAND pgcompiler_bootstrap
                tools/component_generator.pg
                ${PGCOMP_FILE}
        DEPENDS ${PGCOMP_FILE} tools/component_generator.pg
        COMMENT &quot;Generating ${COMP_NAME} component...&quot;
    )
endforeach()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Build flow:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CMake configures&lt;/li&gt;
&lt;li&gt;Bootstrap compiler builds (fast, no graphics deps)&lt;/li&gt;
&lt;li&gt;Component generator runs for each &lt;code&gt;.pgcomp&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Generated files created in &lt;code&gt;build/Generated/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Main engine build includes generated files&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Beautiful.&lt;/p&gt;
&lt;h3&gt;Preventing Rebuild Loops&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; The generator ran every time, triggering full rebuilds even when nothing changed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Commit: &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/3f771c7d&quot;&gt;3f771c7d&lt;/a&gt;&lt;/strong&gt; - &quot;Only regenerate .pgcomp file on change&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_custom_command(
    OUTPUT &quot;${GENERATED_FILE}&quot;
    COMMAND pgcompiler_bootstrap ... &gt; &quot;${GENERATED_FILE}.tmp&quot;
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
            &quot;${GENERATED_FILE}.tmp&quot;
            &quot;${GENERATED_FILE}&quot;
    DEPENDS ${PGCOMP_FILE}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;copy_if_different&lt;/code&gt; only updates the file if content actually changed. This preserves timestamps and prevents unnecessary recompilation.&lt;/p&gt;
&lt;h2&gt;Migration: Real Impact with Real Numbers&lt;/h2&gt;
&lt;p&gt;Time to migrate our existing components and measure the impact.&lt;/p&gt;
&lt;h3&gt;PositionComponent - The Big One&lt;/h3&gt;
&lt;p&gt;PositionComponent is complex. It handles 2D transforms, anchoring, visibility, events, and more.&lt;/p&gt;
&lt;h4&gt;Before: The Manual Implementation&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;position.h&lt;/strong&gt; (359 lines):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#pragma once

#include &quot;ECS/system.h&quot;
#include &quot;pgconstant.h&quot;

namespace pg
{
    enum class AnchorType : uint8_t { ... };
    enum class ResizeHandle : uint8_t { ... };

    struct PositionComponentChangedEvent
    {
        _unique_id id = 0;
    };

    struct PositionComponent : public Component
    {
        float x = 0.0f;
        float y = 0.0f;
        float z = 0.0f;
        float width = 0.0f;
        float height = 0.0f;
        float rotation = 0.0f;
        bool visible = true;
        bool observable = true;

        // Getters
        float getX() const { return x; }
        float getY() const { return y; }
        // ... 6 more getters

        // Setters with event firing
        void setX(float x);
        void setY(float y);
        // ... 6 more setters

        // Utility methods
        bool isVisible() const { return visible; }
        bool isObservable() const { return observable; }
        bool isRenderable() const { return visible &amp;#x26;&amp;#x26; observable; }

        // ... more methods

        _unique_id id = 0;
        EntitySystem* ecsRef = nullptr;
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;position.cpp&lt;/strong&gt; (150 lines):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void PositionComponent::setX(float x)
{
    if (areNotAlmostEqual(this-&gt;x, x))
    {
        LOG_THIS(DOM);
        this-&gt;x = x;

        if (ecsRef)
            ecsRef-&gt;sendEvent(PositionComponentChangedEvent{id});
    }
}

// Repeat for setY, setZ, setWidth, setHeight, setRotation...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ecsserialization.cpp&lt;/strong&gt; (98 lines for this component):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// VM proxy schema
auto positionProxySchema = [](VM* vm, EntityRef entity) -&gt; Value {
    auto component = entity.getComponent&amp;#x3C;PositionComponent&gt;();
    // ... 100 lines of proxy setup
};

// Serialization
archive(&quot;x&quot;, component-&gt;x);
archive(&quot;y&quot;, component-&gt;y);
// ... serialization for each field
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Total: ~607 lines&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;After: The Declarative Implementation&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;PositionComponent.pgcomp&lt;/strong&gt; (62 lines):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: PositionComponent
namespace: pg
base_class: Component

includes:
  - &quot;ECS/system.h&quot;
  - &quot;pgconstant.h&quot;

forwards:
  - &quot;struct UiAnchor;&quot;

fields:
  - name: x
    type: float
    default: 0.0f
    setter: event

  - name: y
    type: float
    default: 0.0f
    setter: event

  - name: z
    type: float
    default: 0.0f
    setter: event

  - name: width
    type: float
    default: 0.0f
    setter: event

  - name: height
    type: float
    default: 0.0f
    setter: event

  - name: rotation
    type: float
    default: 0.0f
    setter: event

  - name: visible
    type: bool
    default: true
    setter: event

  - name: observable
    type: bool
    default: true
    setter: event

methods:
  - &quot;bool isVisible() const { return visible; }&quot;
  - &quot;bool isObservable() const { return observable; }&quot;
  - &quot;bool isRenderable() const { return visible and observable; }&quot;
  - &quot;bool updatefromAnchor(const UiAnchor&amp;#x26; anchor);&quot;
  - &quot;void setVisibility(const bool&amp;#x26; value);&quot;

events:
  - PositionComponentChangedEvent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Total: 62 lines&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;The Numbers&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Reduction: 607 lines → 62 lines = 90% smaller&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Impact: Beyond Line Counts&lt;/h2&gt;
&lt;h3&gt;Development Velocity&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Adding a new component:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write struct definition&lt;/li&gt;
&lt;li&gt;Implement setters with event firing&lt;/li&gt;
&lt;li&gt;Add VM proxy schema&lt;/li&gt;
&lt;li&gt;Write serialization code&lt;/li&gt;
&lt;li&gt;Register attach handler&lt;/li&gt;
&lt;li&gt;Debug inconsistencies&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write &lt;code&gt;.pgcomp&lt;/code&gt; definition&lt;/li&gt;
&lt;li&gt;Run generator&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Adding a field to existing component:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update struct&lt;/li&gt;
&lt;li&gt;Add getter/setter&lt;/li&gt;
&lt;li&gt;Update proxy&lt;/li&gt;
&lt;li&gt;Update serialization&lt;/li&gt;
&lt;li&gt;Pray you didn&apos;t forget anything&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add 4 lines to &lt;code&gt;.pgcomp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Regenerate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Faster iteration, and less bug prone&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Bug Reduction: The &quot;Only Fire When Changed&quot; Story&lt;/h3&gt;
&lt;p&gt;During code generation implementation, we discovered an important optimization:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void setX(float value)
{
    if (this-&gt;x != value) // This check!
    {  
        this-&gt;x = value;

        if (ecsRef)
        {
            ecsRef-&gt;sendEvent(PositionComponentChangedEvent{id});
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Without the &lt;code&gt;if (this-&gt;x != value)&lt;/code&gt; check, setting a field to its current value fires an event. Systems respond to that event. Those systems might set more fields. More events fire. Cascading updates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before the generator:&lt;/strong&gt; This check was manual. Sometimes we remembered. Sometimes we forgot. Inconsistent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After the generator:&lt;/strong&gt; This check is in every event setter. Guaranteed. Consistent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Commit: &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/25754476&quot;&gt;25754476&lt;/a&gt;&lt;/strong&gt; - &quot;Fixed event generation in component to only fire when the value changes&quot;&lt;/p&gt;
&lt;p&gt;One fix in the generator → all components benefit forever.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Documentation Benefits&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;.pgcomp&lt;/code&gt; files double as documentation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; &quot;What fields does PositionComponent have?&quot;
→ Open position.h, scroll through 359 lines of code&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; &quot;What fields does PositionComponent have?&quot;
→ Open PositionComponent.pgcomp, see 8 clear field definitions&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;fields:
  - name: x
    type: float
    default: 0.0f
    setter: event
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Type, default value, behavior—all visible at a glance.&lt;/p&gt;
&lt;p&gt;New team members understand components in minutes, not hours.&lt;/p&gt;
&lt;h2&gt;Lessons Learned: What Worked and What Didn&apos;t&lt;/h2&gt;
&lt;h3&gt;What Went Exceptionally Well&lt;/h3&gt;
&lt;h4&gt;Dogfooding Accelerated Language Development&lt;/h4&gt;
&lt;p&gt;Using PgScript to build engine tools revealed gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;String manipulation&lt;/strong&gt; was basic → Added 46+ utility functions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File I/O&lt;/strong&gt; needed write support → Added &lt;code&gt;writeFile()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Array operations&lt;/strong&gt; needed helpers → Added vector module&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type introspection&lt;/strong&gt; was missing → Added &lt;code&gt;typeof()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every gap we filled made PgScript more capable. Not just for the generator, but for &lt;strong&gt;all&lt;/strong&gt; PgScript users.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Use your own tools. The pain you feel is real user pain.&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Incremental Approach Reduced Risk&lt;/h4&gt;
&lt;p&gt;We didn&apos;t migrate everything at once:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Phase 1:&lt;/strong&gt; Write the generator (simple Health component example)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 2:&lt;/strong&gt; Migrate PositionComponent (medium complexity)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 3:&lt;/strong&gt; Migrate TTFText (complex, with custom constructors)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 4:&lt;/strong&gt; Generate serialization and VM proxies&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each phase validated the approach. Each success built confidence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Small iterations makes it easier for refactoring old or complex systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;What Was Challenging&lt;/h3&gt;
&lt;h4&gt;YAML Parsing Complexity&lt;/h4&gt;
&lt;p&gt;Writing a YAML parser is harder than it looks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Indentation tracking&lt;/strong&gt; - Spaces vs tabs, mixed indentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nested structures&lt;/strong&gt; - Objects within arrays within objects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge cases&lt;/strong&gt; - Empty values, comments, multiline strings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error handling&lt;/strong&gt; - Where exactly did parsing fail?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The parser is now 400+ lines and still doesn&apos;t handle all YAML edge cases. We support the subset we need, but adding new &lt;code&gt;.pgcomp&lt;/code&gt; features sometimes means parser changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Consider an existing parser library if your language supports it. We couldn&apos;t, but you might be able to.&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Build System Integration Took Iteration&lt;/h4&gt;
&lt;p&gt;The bootstrap compiler solution works, but we went through multiple iterations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Attempt 1:&lt;/strong&gt; Generate during build -&gt; rebuild loops&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attempt 2:&lt;/strong&gt; Generate in separate step -&gt; dependency issues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attempt 3:&lt;/strong&gt; Bootstrap compiler -&gt; chicken-and-egg problem&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Final solution:&lt;/strong&gt; Minimal bootstrap + &lt;code&gt;copy_if_different&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;CMake&apos;s dependency tracking is subtle. Getting it right required patience and testing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Build system integration is never as simple as you think. Plan extra time.&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Balancing Flexibility vs Simplicity&lt;/h4&gt;
&lt;p&gt;We have 4 setter levels. Is that too many? Not enough?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some components need custom logic&lt;/li&gt;
&lt;li&gt;Most components need simple patterns&lt;/li&gt;
&lt;li&gt;Adding flexibility adds complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&apos;re still finding the right balance. Too many options overwhelm users. Too few options make the system inflexible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Start simple. Add flexibility only when needed. Resist feature creep.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Future Improvements&lt;/h3&gt;
&lt;p&gt;Ideas for next iteration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Better error messages&lt;/strong&gt; - Parser errors should show line numbers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt; - Catch typos in setter levels, invalid types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component relationships&lt;/strong&gt; - Parent/child components, dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE integration&lt;/strong&gt; - Syntax highlighting for &lt;code&gt;.pgcomp&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hot-reload&lt;/strong&gt; - Change &lt;code&gt;.pgcomp&lt;/code&gt;, see changes in editor without rebuild&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type checking&lt;/strong&gt; - Validate field types against C++ types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic Docs Creation&lt;/strong&gt; - Reuse the YAML parser to automatically create the docs of the components in our &lt;a href=&quot;https://columbaengine.readthedocs.io/en/latest/&quot;&gt;official docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion: The Power of Meta-Programming&lt;/h2&gt;
&lt;p&gt;Let&apos;s return to where we started: the 50th component.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before the generator:&lt;/strong&gt;
&lt;em&gt;Sigh. Another component. Open position.h, copy-paste struct definition, update field names, write 8 setters, add proxy schema, write serialization, register attach handler, test, debug, commit. 45 minutes later...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After the generator:&lt;/strong&gt;
&lt;em&gt;Create &lt;code&gt;WeaponComponent.pgcomp&lt;/code&gt;, define 5 fields, run generator. Done. 5 minutes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That&apos;s the power of code generation. But the benefits run deeper:&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Key Takeaways&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Boilerplate is a Signal&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Repetitive code isn&apos;t just annoying—it&apos;s a signal. If you&apos;re writing the same pattern over and over, automate it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Dogfooding Accelerates Development&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using PgScript to build PgScript tools made the language better. Use your own creations. Feel the pain. Fix it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Declarative &gt; Imperative&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Compare these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;607 lines of imperative C++&lt;/li&gt;
&lt;li&gt;62 lines of declarative YAML&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which would you rather maintain?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Code Generation Compounds&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The first component saved 545 lines. The second saved 200 more. The third will save even more. Benefits multiply with scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Meta-Programming is Accessible&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You don&apos;t need complex C++ template magic. A simple script can generate code. YAML + string concatenation is often enough.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;What&apos;s Your Boilerplate?&lt;/h3&gt;
&lt;p&gt;Think about your codebase:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What do you copy-paste frequently?&lt;/li&gt;
&lt;li&gt;What patterns do you repeat?&lt;/li&gt;
&lt;li&gt;Where do inconsistencies creep in?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s your opportunity for code generation.&lt;/p&gt;
&lt;p&gt;Maybe it&apos;s:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database models&lt;/li&gt;
&lt;li&gt;API endpoints&lt;/li&gt;
&lt;li&gt;Serialization code&lt;/li&gt;
&lt;li&gt;Unit test boilerplate&lt;/li&gt;
&lt;li&gt;UI component definitions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whatever it is, consider: Could a generator help?&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Final Thought&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The best code is the code you don&apos;t have to write.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The component generator doesn&apos;t just save time—it eliminates entire classes of bugs, enforces consistency, and makes our ECS architecture more accessible to everyone on the team.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;559 lines eliminated. Countless bugs prevented. Infinite time saved.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That&apos;s the journey from manual hell to declarative heaven.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ColumbaEngine Repository:&lt;/strong&gt; &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine&quot;&gt;github.com/Gallasko/ColumbaEngine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component Generator Source:&lt;/strong&gt; &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/blob/main/tools/component_generator.pg&quot;&gt;tools/component_generator.pg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/blob/main/docs/COMPONENT_GENERATOR.md&quot;&gt;COMPONENT_GENERATOR.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/blob/main/docs/COMPONENT_SCHEMA_REFERENCE.md&quot;&gt;COMPONENT_SCHEMA_REFERENCE.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key Commits Referenced&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/9f061bb6&quot;&gt;9f061bb6&lt;/a&gt; - Start working on generator tool&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/191babed&quot;&gt;191babed&lt;/a&gt; - Added native functions for string/file manipulation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/3883d054&quot;&gt;3883d054&lt;/a&gt; - Working basic component generator&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/4c6f4b5a&quot;&gt;4c6f4b5a&lt;/a&gt; - Added bootstrap compiler&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/b7508293&quot;&gt;b7508293&lt;/a&gt; - Minimal pgcompiler without graphical deps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/3f2ecc26&quot;&gt;3f2ecc26&lt;/a&gt; - Position component migration (-208 lines)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/a9f154a4&quot;&gt;a9f154a4&lt;/a&gt; - TTFText migration with constructor generation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/25754476&quot;&gt;25754476&lt;/a&gt; - Fixed event generation optimization&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine/commit/3f771c7d&quot;&gt;3f771c7d&lt;/a&gt; - Only regenerate on change&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Want to discuss code generation, ECS architecture, or game engine development? Find me on &lt;a href=&quot;https://discord.gg/un4VtehX3W&quot;&gt;Discord&lt;/a&gt; or check out &lt;a href=&quot;https://columbaengine.org/&quot;&gt;columbaengine.org&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title>Building Resize Handles That Actually Work: Lessons from a Game Engine Editor</title><link>https://columbaengine.org/blog/resize/</link><guid isPermaLink="true">https://columbaengine.org/blog/resize/</guid><description>Adding resize capabilities to the ColumbaEngine.</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building Resize Handles That Actually Work: Lessons from a Game Engine Editor&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You know that moment when you&apos;re using a design tool and you grab the corner of an element, expecting to resize it, but instead you accidentally move the whole thing? Or worse—nothing happens at all.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I just spent the afternoon implementing proper resize handles for a game engine editor, and it reminded me why good editor UX is so much harder than it looks. Here&apos;s what I learned building a system that prioritizes handle clicks over dragging, integrates with undo/redo, and doesn&apos;t break when you have complex UI hierarchies.&lt;/p&gt;
&lt;h2&gt;The Problem with &quot;Simple&quot; Resize Systems&lt;/h2&gt;
&lt;p&gt;Most resize implementations I&apos;ve seen follow this pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Draw some handles on a selection outline&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check if mouse clicks hit the handles&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resize on drag&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Sounds straightforward. But the devil lives in the interaction priority. When you click near a resize handle, what should happen? In many editors, there&apos;s an annoying ambiguity—sometimes you resize, sometimes you select a different element, sometimes you start dragging.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Handle clicks need absolute priority in the hit detection hierarchy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Building Priority-Based Click Detection&lt;/h2&gt;
&lt;p&gt;Here&apos;s how I structured the click detection in the EntityFinder system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;virtual void onEvent(const OnMouseClick&amp;#x26; event) override
{
    bool hit = false;

    // Priority 1: Check resize handles first
    for (const auto&amp;#x26; elem : viewGroup&amp;#x3C;PositionComponent, ResizeHandleComponent&gt;())
    {
        if (inClipBound(elem-&gt;entity, event.pos.x, event.pos.y))
        {
            hit = true;
            auto selectedId = selectionOutline.get&amp;#x3C;SelectedEntity&gt;()-&gt;id;
            if (selectedId != 0)
            {
                ecsRef-&gt;sendEvent(StartResize{ selectedId, 
                    elem-&gt;get&amp;#x3C;ResizeHandleComponent&gt;()-&gt;handle, 
                    event.pos.x, event.pos.y });
            }
            break;
        }
    }

    // Priority 2: Only check entities if no handle was clicked
    if (not hit)
    {
        // ... existing entity selection logic
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This priority system eliminates the frustrating &lt;em&gt;&quot;did I click the handle or the element?&quot;&lt;/em&gt; problem. &lt;strong&gt;Handles always win.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Eight-Handle Layout&lt;/h2&gt;
&lt;p&gt;I went with the classic eight-handle approach: four corners plus four edge midpoints. Each handle type maps to specific resize behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;enum class ResizeHandle : uint8_t
{
    None = 0,
    TopLeft, Top, TopRight,
    Left, Right,
    BottomLeft, Bottom, BottomRight
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tricky part is positioning these handles correctly. They need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Anchor to the selection outline&apos;s boundaries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stay visible at different zoom levels&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Have a consistent hit target size (8px)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I solved this with a helper function in the outline creation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto createResizeHandle = [&amp;#x26;](ResizeHandle handleType, AnchorType anchorX, AnchorType anchorY) {
    auto handle = makeUiSimple2DShape(ecs, Shape2D::Square, handleSize, handleSize, handleColor);
    
    auto handleAnchor = handle.get&amp;#x3C;UiAnchor&gt;();
    auto handleResizeComp = ecs-&gt;attach&amp;#x3C;ResizeHandleComponent&gt;(handle.entity);
    handleResizeComp-&gt;handle = handleType;
    
    // Position based on anchor types
    if (anchorX == AnchorType::Left)
        handleAnchor-&gt;setLeftAnchor({outline.entity.id, AnchorType::Left, -handleSize/2});
    else if (anchorX == AnchorType::Right)
        handleAnchor-&gt;setRightAnchor({outline.entity.id, AnchorType::Right, -handleSize/2});
    // ... etc for all positions
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The offset of &lt;code&gt;-handleSize/2&lt;/code&gt; centers the handle on the outline edge—a small detail that makes the interaction feel precise.&lt;/p&gt;
&lt;h2&gt;Resize Logic That Doesn&apos;t Fight You&lt;/h2&gt;
&lt;p&gt;The actual resize math varies by handle type. Corner handles modify both position and size, while edge handles only change one dimension:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;switch (activeHandle)
{
    case ResizeHandle::TopLeft:
        newX = resizeStartX + deltaX;
        newY = resizeStartY + deltaY;
        newWidth = resizeStartWidth - deltaX;
        newHeight = resizeStartHeight - deltaY;
        break;
    case ResizeHandle::Right:
        newWidth = resizeStartWidth + deltaX;
        break;
    // ... other cases
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that when resizing from the &lt;strong&gt;top-left&lt;/strong&gt;, the position changes &lt;em&gt;inverse&lt;/em&gt; to the delta. This keeps the bottom-right corner fixed while the top-left moves—exactly what users expect.&lt;/p&gt;
&lt;p&gt;I added a minimum size constraint to prevent elements from disappearing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;constexpr float minSize = 10.0f;
if (newWidth &amp;#x3C; minSize || newHeight &amp;#x3C; minSize)
    return;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Undo/Redo Integration&lt;/h2&gt;
&lt;p&gt;The resize system needed to play nicely with the existing command pattern. I created a &lt;code&gt;ResizeCommand&lt;/code&gt; that captures the complete before/after state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct ResizeCommand : public InspectorCommands
{
    _unique_id entityId;
    ResizeHandle handle;
    float startWidth, startHeight, startX, startY;
    float endWidth, endHeight, endX, endY;
    
    virtual void execute() override {
        auto pos = ecsRef-&gt;getComponent&amp;#x3C;PositionComponent&gt;(entityId);
        pos-&gt;setX(endX);
        pos-&gt;setY(endY);
        pos-&gt;setWidth(endWidth);
        pos-&gt;setHeight(endHeight);
    }
    
    virtual void undo() override {
        auto pos = ecsRef-&gt;getComponent&amp;#x3C;PositionComponent&gt;(entityId);
        pos-&gt;setX(startX);
        pos-&gt;setY(startY);
        pos-&gt;setWidth(startWidth);
        pos-&gt;setHeight(startHeight);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command gets created when the resize operation ends, capturing the full transformation in one &lt;strong&gt;atomic operation&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Real-Time Visual Feedback&lt;/h2&gt;
&lt;p&gt;During resize, both the element and its selection outline update in real-time. This required careful coordination between the resize logic and the outline positioning:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Update the element
pos-&gt;setX(newX);
pos-&gt;setY(newY);
pos-&gt;setWidth(newWidth);
pos-&gt;setHeight(newHeight);

// Update selection outline to match
auto ent = ecsRef-&gt;getEntity(&quot;SelectionOutline&quot;);
if (ent &amp;#x26;&amp;#x26; ent-&gt;get&amp;#x3C;SelectedEntity&gt;()-&gt;id == resizingEntity)
{
    auto outlinePos = ent-&gt;get&amp;#x3C;PositionComponent&gt;();
    outlinePos-&gt;setX(newX - 2.f);
    outlinePos-&gt;setY(newY - 2.f);
    outlinePos-&gt;setWidth(newWidth + 4.f);
    outlinePos-&gt;setHeight(newHeight + 4.f);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The outline stays &lt;strong&gt;2 pixels larger&lt;/strong&gt; than the element on all sides, maintaining the visual relationship throughout the resize operation.&lt;/p&gt;
&lt;h2&gt;What I&apos;d Do Differently&lt;/h2&gt;
&lt;p&gt;If I were starting fresh, I&apos;d consider:&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Constraint system integration&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The current implementation checks for anchor constraints but could be smarter about respecting layout relationships during resize.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Aspect ratio locking&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Adding &lt;code&gt;Shift+drag&lt;/code&gt; to maintain proportions would make the tool more flexible.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Visual cursor feedback&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Changing the cursor to resize arrows when hovering over handles would improve discoverability.&lt;/p&gt;
&lt;h2&gt;The Bigger Picture&lt;/h2&gt;
&lt;p&gt;Building resize handles taught me that good editor UX comes down to &lt;strong&gt;eliminating ambiguity&lt;/strong&gt;. Users should never wonder &lt;em&gt;&quot;what will happen if I click here?&quot;&lt;/em&gt; The system should handle the complexity so they can focus on their creative work.&lt;/p&gt;
&lt;p&gt;The technical implementation—events, commands, hit detection—is just scaffolding. The real challenge is making interactions feel &lt;strong&gt;natural and predictable&lt;/strong&gt;. When someone grabs a corner handle, they expect to resize from that corner. When they press &lt;code&gt;Ctrl+Z&lt;/code&gt;, they expect the resize to undo completely.&lt;/p&gt;
&lt;p&gt;Meeting those expectations requires thinking beyond the happy path to all the edge cases and failure modes. But when it works, the editor just disappears and lets users create.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Want to see the full implementation? The complete code is available in the &lt;a href=&quot;https://github.com/gallasko/ColumbaEngine&quot;&gt;ColumbaEngine repository&lt;/a&gt; with all the resize handle logic, priority-based detection, and undo/redo integration.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>CMake for Complex Projects (Part 2): Building a C++ Game Engine from Scratch for Desktop and WebAssembly</title><link>https://columbaengine.org/blog/cmake-part2/</link><guid isPermaLink="true">https://columbaengine.org/blog/cmake-part2/</guid><description>A practical guide to modern CMake through a real-world C++ game engine project - Deployment &amp; Distribution</description><pubDate>Mon, 18 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A practical guide to modern CMake through a real-world C++ game engine project - Deployment &amp;#x26; Distribution&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Welcome back! In &lt;a href=&quot;https://columbaengine.org/blog/cmake-part1/&quot;&gt;Part 1&lt;/a&gt;, we built a robust compilation and build system for our ColumbaEngine game engine. We covered project setup, dependency management, cross-platform builds, and testing. The response has been incredible - thank you to everyone who shared feedback, corrections, and suggestions!&lt;/p&gt;
&lt;p&gt;Now it&apos;s time for the &lt;strong&gt;deployment side&lt;/strong&gt; - the part where most CMake tutorials stop, but arguably the most important for real projects. How do you make your carefully crafted library actually usable by other developers? How do you create professional installers? How do you handle the complex world of package distribution?&lt;/p&gt;
&lt;p&gt;This is where CMake really shows its power (and complexity). We&apos;ll dive deep into installation systems, export mechanisms, package configuration, and distribution strategies that turn your project from &quot;works on my machine&quot; to &quot;works for everyone.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The feedback from Part 1 has been fantastic, and several CMake experts have pointed out areas where my approach could be improved (modern FILE_SET usage, superbuild patterns, better dependency defaults, etc.). I&apos;ll be publishing an appendix soon addressing these insights - it&apos;s amazing how much you can learn from the community!&lt;/p&gt;
&lt;h2&gt;Recap: What We Built in Part 1&lt;/h2&gt;
&lt;p&gt;Our ColumbaEngine now has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;80+ source files compiled into a robust static library&lt;/li&gt;
&lt;li&gt;Cross-platform support (Windows, Linux, Web)&lt;/li&gt;
&lt;li&gt;Complex dependency management with vendored libraries&lt;/li&gt;
&lt;li&gt;Precompiled headers for fast builds&lt;/li&gt;
&lt;li&gt;Multiple executables and comprehensive testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Missing Piece&lt;/strong&gt;: How do other developers use our engine?&lt;/p&gt;
&lt;p&gt;Right now, they&apos;d have to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clone our entire repository (including 500MB of dependencies)&lt;/li&gt;
&lt;li&gt;Build everything from source&lt;/li&gt;
&lt;li&gt;Manually figure out include paths and linking&lt;/li&gt;
&lt;li&gt;Hope it works on their system&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&apos;s not professional. Let&apos;s fix it.&lt;/p&gt;
&lt;h2&gt;Step 9: Installation - Making Your Library Usable&lt;/h2&gt;
&lt;p&gt;This is where most tutorials stop, but it&apos;s crucial for real projects. How do other developers use your library?&lt;/p&gt;
&lt;p&gt;The installation code goes at the end of your main &lt;code&gt;CMakeLists.txt&lt;/code&gt;, after all targets have been defined:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Install the library and its dependencies
install(TARGETS ColumbaEngine SDL2-static glm libglew_static freetype
    EXPORT ColumbaEngineTargets
    ARCHIVE DESTINATION lib        # .a files
    LIBRARY DESTINATION lib        # .so files
    RUNTIME DESTINATION bin        # .exe/.dll files
    INCLUDES DESTINATION include   # Header search path
)

# Install headers
install(DIRECTORY src/Engine/
    DESTINATION include/ColumbaEngine
    FILES_MATCHING PATTERN &quot;*.h&quot; PATTERN &quot;*.hpp&quot;
)

# Create the package config files
install(EXPORT ColumbaEngineTargets
    FILE ColumbaEngineTargets.cmake
    NAMESPACE ColumbaEngine::
    DESTINATION lib/cmake/ColumbaEngine
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running &lt;code&gt;make install&lt;/code&gt; (or &lt;code&gt;sudo make install&lt;/code&gt; for system-wide installation), other projects can use your library like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;find_package(ColumbaEngine REQUIRED)
target_link_libraries(MyGame PRIVATE ColumbaEngine::ColumbaEngine)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No manual include paths, no hunting for library files - just clean, simple usage.&lt;/p&gt;
&lt;h3&gt;The Export/Import Mechanism&lt;/h3&gt;
&lt;p&gt;The installation system is probably the most complex part of CMake. Let&apos;s break down what happens:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. During Build (Export)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;install(TARGETS ColumbaEngine SDL2-static glm
    EXPORT ColumbaEngineTargets  # ← This creates a target &quot;export set&quot;
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells CMake: &quot;When installing, remember these targets and their properties.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Target Properties Get Captured&lt;/strong&gt;
CMake captures each target&apos;s:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include directories (&lt;code&gt;target_include_directories&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Compile definitions (&lt;code&gt;target_compile_definitions&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Link libraries (&lt;code&gt;target_link_libraries&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Compile features (&lt;code&gt;target_compile_features&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Export File Generation&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;install(EXPORT ColumbaEngineTargets
    FILE ColumbaEngineTargets.cmake
    NAMESPACE ColumbaEngine::        # ← Creates ColumbaEngine::ColumbaEngine
    DESTINATION lib/cmake/ColumbaEngine
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates &lt;code&gt;ColumbaEngineTargets.cmake&lt;/code&gt; containing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Generated file - don&apos;t edit!
add_library(ColumbaEngine::ColumbaEngine STATIC IMPORTED)
set_target_properties(ColumbaEngine::ColumbaEngine PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES &quot;/usr/local/include/ColumbaEngine;/usr/local/include&quot;
    INTERFACE_LINK_LIBRARIES &quot;ColumbaEngine::SDL2-static;ColumbaEngine::glm;OpenGL::GL&quot;
    # ... more properties
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Package Configuration&lt;/strong&gt;
The &lt;code&gt;ColumbaEngineConfig.cmake.in&lt;/code&gt; template becomes &lt;code&gt;ColumbaEngineConfig.cmake&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# This runs when someone does find_package(ColumbaEngine)
find_dependency(OpenGL REQUIRED)  # Ensure dependencies are found first
include(&quot;${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake&quot;)  # Import our targets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. User Experience&lt;/strong&gt;
When another project does &lt;code&gt;find_package(ColumbaEngine)&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CMake finds &lt;code&gt;ColumbaEngineConfig.cmake&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Runs the config script (finds OpenGL, loads targets)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ColumbaEngine::ColumbaEngine&lt;/code&gt; target becomes available&lt;/li&gt;
&lt;li&gt;Users link to it and get all the transitive dependencies automatically&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why the Namespace?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;NAMESPACE ColumbaEngine::&lt;/code&gt; is crucial:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prevents conflicts&lt;/strong&gt;: Multiple libraries might have a &quot;Engine&quot; target&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear API&lt;/strong&gt;: &lt;code&gt;ColumbaEngine::ColumbaEngine&lt;/code&gt; is unambiguous&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Import detection&lt;/strong&gt;: Can check &lt;code&gt;if(TARGET ColumbaEngine::ColumbaEngine)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 10: Package Configuration&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;cmake/ColumbaEngineConfig.cmake.in&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;@PACKAGE_INIT@

include(CMakeFindDependencyMacro)

# Find required system dependencies that consumers need
if(NOT ${CMAKE_SYSTEM_NAME} MATCHES &quot;Emscripten&quot;)
    find_dependency(OpenGL REQUIRED)
    find_dependency(Threads REQUIRED)

    # Find X11 libraries for SDL2
    find_package(X11 QUIET)
    if(X11_FOUND)
        # Create imported targets for X11 if they don&apos;t exist
        if(NOT TARGET X11::X11)
            add_library(X11::X11 UNKNOWN IMPORTED)
            set_target_properties(X11::X11 PROPERTIES
                IMPORTED_LOCATION &quot;${X11_X11_LIB}&quot;
                INTERFACE_INCLUDE_DIRECTORIES &quot;${X11_X11_INCLUDE_PATH}&quot;
            )
        endif()

        if(NOT TARGET X11::Xext AND X11_Xext_FOUND)
            add_library(X11::Xext UNKNOWN IMPORTED)
            set_target_properties(X11::Xext PROPERTIES
                IMPORTED_LOCATION &quot;${X11_Xext_LIB}&quot;
                INTERFACE_INCLUDE_DIRECTORIES &quot;${X11_X11_INCLUDE_PATH}&quot;
            )
        endif()
    endif()

    # Find other system dependencies that SDL2 might need
    find_package(PkgConfig QUIET)
    if(PKG_CONFIG_FOUND)
        pkg_check_modules(ALSA QUIET alsa)
        pkg_check_modules(PULSE QUIET libpulse-simple)
    endif()
endif()

# Include our targets
include(&quot;${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake&quot;)

# Provide variables for backward compatibility
set(ColumbaEngine_LIBRARIES ColumbaEngine::ColumbaEngine)

# Set include directories based on the imported target
get_target_property(ColumbaEngine_INCLUDE_DIRS ColumbaEngine::ColumbaEngine INTERFACE_INCLUDE_DIRECTORIES)

# Create an alias for easier consumption if it doesn&apos;t exist
if(NOT TARGET ColumbaEngine)
    add_library(ColumbaEngine ALIAS ColumbaEngine::ColumbaEngine)
endif()

check_required_components(ColumbaEngine)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gets processed into &lt;code&gt;ColumbaEngineConfig.cmake&lt;/code&gt; and installed. It ensures that when someone does &lt;code&gt;find_package(ColumbaEngine)&lt;/code&gt;, all dependencies are found automatically.&lt;/p&gt;
&lt;h3&gt;The Package Configuration Template System&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;.cmake.in&lt;/code&gt; template system is CMake&apos;s way of generating configuration files with proper path handling. Let&apos;s see what each part does:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;@PACKAGE_INIT@
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This special macro expands to several helper functions and variables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set_and_check()&lt;/code&gt;: Sets a variable and verifies the path exists&lt;/li&gt;
&lt;li&gt;&lt;code&gt;check_required_components()&lt;/code&gt;: Validates that requested components are available&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PACKAGE_PREFIX_DIR&lt;/code&gt;: The installation prefix, relocated if needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why relocatable?&lt;/strong&gt; If you install to &lt;code&gt;/usr/local&lt;/code&gt; but someone copies the installation to &lt;code&gt;/opt/columbaengine&lt;/code&gt;, the paths need to update automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;include(CMakeFindDependencyMacro)
find_dependency(OpenGL REQUIRED)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;find_dependency&lt;/code&gt; is like &lt;code&gt;find_package&lt;/code&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Forwards the &lt;code&gt;QUIET&lt;/code&gt; and &lt;code&gt;REQUIRED&lt;/code&gt; flags from the parent &lt;code&gt;find_package&lt;/code&gt; call&lt;/li&gt;
&lt;li&gt;Properly handles the dependency chain for error reporting&lt;/li&gt;
&lt;li&gt;Works correctly with package components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: If someone calls &lt;code&gt;find_package(ColumbaEngine REQUIRED QUIET)&lt;/code&gt;, the OpenGL search will also be QUIET and REQUIRED.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;include(&quot;${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This loads the exported targets. The &lt;code&gt;${CMAKE_CURRENT_LIST_DIR}&lt;/code&gt; ensures we load from the same directory as the config file, making the installation relocatable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;check_required_components(ColumbaEngine)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This validates that all requested components are available. Our engine doesn&apos;t have components yet, but you might add them later:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;find_package(ColumbaEngine REQUIRED COMPONENTS Renderer Audio Networking)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advanced: Component-Based Installation&lt;/h3&gt;
&lt;p&gt;Here&apos;s how you could extend the engine with components:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Install different parts as components
install(TARGETS ColumbaEngine_Core
    EXPORT ColumbaEngineTargets
    COMPONENT ColumbaEngine_Core
    ARCHIVE DESTINATION lib
)

install(TARGETS ColumbaEngine_Renderer
    EXPORT ColumbaEngineTargets
    COMPONENT ColumbaEngine_Renderer
    ARCHIVE DESTINATION lib
)

# In ColumbaEngineConfig.cmake.in
set(ColumbaEngine_Core_FOUND TRUE)

find_dependency(OpenGL)
if(OpenGL_FOUND)
    set(ColumbaEngine_Renderer_FOUND TRUE)
endif()

include(&quot;${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake&quot;)
check_required_components(ColumbaEngine)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Users could then request only the parts they need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;find_package(ColumbaEngine REQUIRED COMPONENTS Core)  # No renderer
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 11: Package Distribution with CPack&lt;/h2&gt;
&lt;p&gt;Want to create installers? CPack has you covered. Currently, ColumbaEngine has basic CPack configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# CPack configuration for creating installers
set(CPACK_PACKAGE_NAME &quot;ColumbaEngine&quot;)
set(CPACK_PACKAGE_VENDOR &quot;PigeonCodeur&quot;)
set(CPACK_PACKAGE_VERSION_MAJOR &quot;1&quot;)
set(CPACK_PACKAGE_VERSION_MINOR &quot;0&quot;)
set(CPACK_PACKAGE_VERSION_PATCH &quot;0&quot;)
set(CPACK_PACKAGE_VERSION &quot;${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}&quot;)
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY &quot;ColumbaEngine - A modern C++ game engine&quot;)
set(CPACK_PACKAGE_CONTACT &quot;pigeoncodeur@gmail.com&quot;)
set(CPACK_PACKAGE_HOMEPAGE_URL &quot;https://github.com/Gallasko/ColumbaEngine&quot;)

# Detect available generators
set(CPACK_GENERATOR &quot;TGZ&quot;)

# Check for DEB tools
find_program(DPKG_CMD dpkg)
if(DPKG_CMD)
    list(APPEND CPACK_GENERATOR &quot;DEB&quot;)
endif()

# Check for RPM tools
find_program(RPMBUILD_CMD rpmbuild)
if(RPMBUILD_CMD)
    list(APPEND CPACK_GENERATOR &quot;RPM&quot;)
endif()

include(CPack)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;cpack&lt;/code&gt; and get basic installers for multiple package managers. The CPack configuration is still evolving and will be enhanced with more sophisticated packaging options in future updates.&lt;/p&gt;
&lt;h3&gt;CPack - From Source to Distribution&lt;/h3&gt;
&lt;p&gt;CPack is CMake&apos;s packaging system that transforms your built project into distributable packages. Let&apos;s understand how it works:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Package Discovery and Generator Selection&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Detect available packaging tools on the system
set(CPACK_GENERATOR &quot;TGZ&quot;)  # Always available

find_program(DPKG_CMD dpkg)
if(DPKG_CMD)
    list(APPEND CPACK_GENERATOR &quot;DEB&quot;)
endif()

find_program(RPMBUILD_CMD rpmbuild)
if(RPMBUILD_CMD)
    list(APPEND CPACK_GENERATOR &quot;RPM&quot;)
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This auto-detection means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux systems with dpkg&lt;/strong&gt;: Get &lt;code&gt;.deb&lt;/code&gt; packages for Ubuntu/Debian&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux systems with rpmbuild&lt;/strong&gt;: Get &lt;code&gt;.rpm&lt;/code&gt; packages for RHEL/Fedora&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All systems&lt;/strong&gt;: Get &lt;code&gt;.tar.gz&lt;/code&gt; archives as fallback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Dependency Declaration&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;set(CPACK_DEBIAN_PACKAGE_DEPENDS &quot;libgl1-mesa-dev, libfreetype6-dev&quot;)
set(CPACK_RPM_PACKAGE_REQUIRES &quot;mesa-libGL-devel, freetype-devel&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells the package manager what system libraries your package needs. When users install your &lt;code&gt;.deb&lt;/code&gt;, it automatically installs OpenGL and FreeType development packages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Package Structure Control&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# What gets packaged?
install(TARGETS ColumbaEngine COMPONENT Runtime)
install(DIRECTORY include/ DESTINATION include COMPONENT Development)
install(DIRECTORY examples/ DESTINATION share/ColumbaEngine/examples COMPONENT Examples)

# Create separate packages for different use cases
set(CPACK_COMPONENTS_ALL Runtime Development Examples)
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION &quot;ColumbaEngine runtime libraries&quot;)
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION &quot;Headers and development files&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. The Package Build Process&lt;/strong&gt;
When you run &lt;code&gt;make package&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;File Collection&lt;/strong&gt;: CPack gathers all &lt;code&gt;install()&lt;/code&gt; commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency Resolution&lt;/strong&gt;: Checks what system libraries are needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata Generation&lt;/strong&gt;: Creates package info (version, description, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Archive Creation&lt;/strong&gt;: Builds the actual package file&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt;: Runs package-specific tests&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Real-World Package Examples&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Debian Package Contents&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;columbaengine_1.0.0_amd64.deb
├── DEBIAN/
│   ├── control          # Package metadata
│   ├── postinst        # Post-installation script
│   └── prerm           # Pre-removal script
├── usr/
│   ├── lib/
│   │   ├── libColumbaEngine.a
│   │   └── cmake/ColumbaEngine/
│   └── include/ColumbaEngine/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;RPM Package Structure&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;columbaengine-1.0.0-1.x86_64.rpm
├── usr/lib64/libColumbaEngine.a
├── usr/include/ColumbaEngine/
└── usr/lib64/cmake/ColumbaEngine/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advanced CPack: Custom Package Scripts&lt;/h3&gt;
&lt;p&gt;You can add custom installation behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Custom post-installation script
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
    &quot;${CMAKE_CURRENT_SOURCE_DIR}/cmake/postinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/prerm&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example &lt;code&gt;postinst&lt;/code&gt; script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# Update library cache after installation
ldconfig
# Create symlinks for backward compatibility
ln -sf /usr/lib/libColumbaEngine.a /usr/lib/libGameEngine.a
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Advanced Distribution Strategies&lt;/h2&gt;
&lt;h3&gt;1. Multi-Package Strategy&lt;/h3&gt;
&lt;p&gt;For complex libraries, consider splitting into multiple packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Runtime package - just the libraries
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION &quot;ColumbaEngine runtime libraries&quot;)
set(CPACK_COMPONENT_RUNTIME_REQUIRED TRUE)

# Development package - headers and CMake files
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION &quot;Development files for ColumbaEngine&quot;)
set(CPACK_COMPONENT_DEVELOPMENT_DEPENDS Runtime)

# Documentation package - optional docs and examples
set(CPACK_COMPONENT_DOCUMENTATION_DESCRIPTION &quot;Documentation and examples&quot;)
set(CPACK_COMPONENT_DOCUMENTATION_DEPENDS Development)

# Configure Debian packages
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME &quot;libcolumbaengine1&quot;)
set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_NAME &quot;libcolumbaengine-dev&quot;)
set(CPACK_DEBIAN_DOCUMENTATION_PACKAGE_NAME &quot;libcolumbaengine-doc&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates three separate &lt;code&gt;.deb&lt;/code&gt; files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;libcolumbaengine1.deb&lt;/code&gt; - Runtime libraries&lt;/li&gt;
&lt;li&gt;&lt;code&gt;libcolumbaengine-dev.deb&lt;/code&gt; - Development headers (depends on runtime)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;libcolumbaengine-doc.deb&lt;/code&gt; - Documentation (optional)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Cross-Platform Packaging&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Platform-specific package settings
if(WIN32)
    set(CPACK_GENERATOR &quot;ZIP;NSIS&quot;)
    set(CPACK_NSIS_DISPLAY_NAME &quot;ColumbaEngine Game Engine&quot;)
    set(CPACK_NSIS_HELP_LINK &quot;https://github.com/Gallasko/ColumbaEngine&quot;)
    set(CPACK_NSIS_URL_INFO_ABOUT &quot;https://github.com/Gallasko/ColumbaEngine&quot;)
    set(CPACK_NSIS_MODIFY_PATH ON)
elseif(APPLE)
    set(CPACK_GENERATOR &quot;TGZ;DragNDrop&quot;)
    set(CPACK_DMG_FORMAT &quot;UDBZ&quot;)
    set(CPACK_DMG_VOLUME_NAME &quot;ColumbaEngine&quot;)
elseif(UNIX)
    set(CPACK_GENERATOR &quot;TGZ;DEB;RPM&quot;)
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Version Management and Compatibility&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Semantic versioning
set(COLUMBAENGINE_VERSION_MAJOR 1)
set(COLUMBAENGINE_VERSION_MINOR 2)
set(COLUMBAENGINE_VERSION_PATCH 3)
set(COLUMBAENGINE_VERSION &quot;${COLUMBAENGINE_VERSION_MAJOR}.${COLUMBAENGINE_VERSION_MINOR}.${COLUMBAENGINE_VERSION_PATCH}&quot;)

# ABI compatibility
set(COLUMBAENGINE_SOVERSION ${COLUMBAENGINE_VERSION_MAJOR})

# Set target properties for shared libraries
if(BUILD_SHARED_LIBS)
    set_target_properties(ColumbaEngine PROPERTIES
        VERSION ${COLUMBAENGINE_VERSION}
        SOVERSION ${COLUMBAENGINE_SOVERSION}
    )
endif()

# Package version file
write_basic_package_version_file(
    &quot;${CMAKE_CURRENT_BINARY_DIR}/ColumbaEngineConfigVersion.cmake&quot;
    VERSION ${COLUMBAENGINE_VERSION}
    COMPATIBILITY SameMajorVersion  # 1.x.x is compatible with 1.y.z
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Distribution Workflows&lt;/h2&gt;
&lt;h3&gt;1. Automated Package Building with CI&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/workflows/release.yml
name: Release
on:
  release:
    types: [published]

jobs:
  build-packages:
    strategy:
      matrix:
        os: [ubuntu-20.04, ubuntu-22.04, windows-latest]

    runs-on: ${{ matrix.os }}

    steps:
    - uses: actions/checkout@v3

    - name: Install dependencies (Linux)
      if: runner.os == &apos;Linux&apos;
      run: |
        sudo apt-get update
        sudo apt-get install -y libgl1-mesa-dev libfreetype6-dev

    - name: Configure CMake
      run: |
        mkdir build
        cd build
        cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=OFF

    - name: Build
      run: cmake --build build --parallel

    - name: Create packages
      run: |
        cd build
        cpack

    - name: Upload packages
      uses: actions/upload-release-asset@v1
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: build/*.deb
        asset_name: columbaengine-${{ github.event.release.tag_name }}-${{ matrix.os }}.deb
        asset_content_type: application/vnd.debian.binary-package
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Package Repository Integration&lt;/h3&gt;
&lt;p&gt;For professional distribution, integrate with package repositories:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debian/Ubuntu PPA&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Configure for PPA upload
set(CPACK_DEBIAN_PACKAGE_MAINTAINER &quot;Pigeon Codeur pigeoncodeur@gmail.com&gt;&quot;)
set(CPACK_DEBIAN_PACKAGE_SECTION &quot;libdevel&quot;)
set(CPACK_DEBIAN_PACKAGE_PRIORITY &quot;optional&quot;)
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE &quot;https://github.com/Gallasko/ColumbaEngine&quot;)

# Source package for PPA
set(CPACK_SOURCE_GENERATOR &quot;TGZ&quot;)
set(CPACK_SOURCE_IGNORE_FILES &quot;/build/;/.git/;/.github/;/import/&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. (Future Work:) Documentation Integration&lt;/h3&gt;
&lt;p&gt;Include documentation in your packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Find Doxygen
find_package(Doxygen)
if(DOXYGEN_FOUND)
    set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/docs)
    set(DOXYGEN_PROJECT_NAME &quot;ColumbaEngine&quot;)
    set(DOXYGEN_PROJECT_VERSION ${PROJECT_VERSION})

    doxygen_add_docs(docs
        ${CMAKE_SOURCE_DIR}/src/Engine
        COMMENT &quot;Generate API documentation&quot;
    )

    # Install documentation
    install(DIRECTORY ${CMAKE_BINARY_DIR}/docs/html/
        DESTINATION share/doc/columbaengine
        COMPONENT Documentation
        OPTIONAL
    )
endif()

# Man pages for command-line tools
install(FILES
    ${CMAKE_SOURCE_DIR}/docs/columbaengine.1
    DESTINATION share/man/man1
    COMPONENT Documentation
    OPTIONAL
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing Your Distribution&lt;/h2&gt;
&lt;h3&gt;1. Installation Testing&lt;/h3&gt;
&lt;p&gt;Create a separate test project to verify your package works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# test-installation/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(TestColumbaEngine)

find_package(ColumbaEngine REQUIRED)

add_executable(test_app main.cpp)
target_link_libraries(test_app PRIVATE ColumbaEngine::ColumbaEngine)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// test-installation/main.cpp
#include &amp;#x3C;ColumbaEngine/engine.h&gt;

int main() {
    ColumbaEngine::Engine engine;
    if (engine.initialize()) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;ColumbaEngine loaded successfully!&quot; &amp;#x3C;&amp;#x3C; std::endl;
        return 0;
    }
    return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Testing script&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# test-installation.sh

# Install the package
sudo dpkg -i columbaengine-dev_1.0.0_amd64.deb

# Test that find_package works
mkdir test-build &amp;#x26;&amp;#x26; cd test-build
cmake ../test-installation
make

# Run the test
./test_app

# Clean up
cd ..
rm -rf test-build
sudo dpkg -r columbaengine-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Cross-Platform Validation&lt;/h3&gt;
&lt;p&gt;For consistent testing across different platforms and distributions, you can use Docker containers or virtual machines to validate that your packages install and work correctly on clean systems. This helps catch missing dependencies or platform-specific issues before your users encounter them.&lt;/p&gt;
&lt;h2&gt;The Complete User Experience&lt;/h2&gt;
&lt;p&gt;After implementing all these deployment concepts, here&apos;s what we achieved:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For Package Maintainers:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Build and create all packages
mkdir build &amp;#x26;&amp;#x26; cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j8
cpack

# Results in:
# columbaengine-1.0.0-Linux.tar.gz
# (and .deb/.rpm if tools are available)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;For System Installation:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install to system directories
mkdir build &amp;#x26;&amp;#x26; cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j8
sudo make install

# Now available system-wide
find_package(ColumbaEngine REQUIRED)  # Works in any CMake project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;For End Users (Ubuntu/Debian):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install from package
sudo dpkg -i columbaengine-dev_1.0.0_amd64.deb

# Or from PPA
sudo add-apt-repository ppa:yourusername/columbaengine
sudo apt update
sudo apt install libcolumbaengine-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;For Developers:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# In their CMakeLists.txt
find_package(ColumbaEngine 1.0 REQUIRED)
add_executable(MyGame src/main.cpp)
target_link_libraries(MyGame PRIVATE ColumbaEngine::ColumbaEngine)

# That&apos;s it! No manual configuration needed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(Future Work): For Package Managers&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# These integrations are planned for future releases

# Conan
conan install columbaengine/1.0.0@

# vcpkg
vcpkg install columbaengine

# CPM
CPMAddPackage(&quot;gh:Gallasko/ColumbaEngine@1.0.0&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lessons Learned and Future Improvements&lt;/h2&gt;
&lt;h3&gt;What Worked Well&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Target-based approach&lt;/strong&gt;: Made the export/import system much cleaner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component separation&lt;/strong&gt;: Allows users to install only what they need&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform packaging&lt;/strong&gt;: Same CMake code generates packages for multiple platforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated testing&lt;/strong&gt;: Catches installation issues before users see them&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Areas for Improvement&lt;/h3&gt;
&lt;p&gt;Based on the excellent feedback from Part 1, there are several areas I want to improve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modern CMake patterns&lt;/strong&gt;: Exploring FILE_SET instead of target_include_directories&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Superbuild approaches&lt;/strong&gt;: For better dependency management and packaging&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency defaults&lt;/strong&gt;: Making find_package the default with vendoring as fallback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generator expression usage&lt;/strong&gt;: Using them more judiciously&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;m working on an appendix that will address these improvements in detail - the CMake community feedback has been invaluable!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Building a professional distribution system with CMake involves several sophisticated concepts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Export/Import Systems&lt;/strong&gt; - Making your targets available to other projects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package Configuration&lt;/strong&gt; - Handling dependencies and relocatable installations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-format Packaging&lt;/strong&gt; - Supporting different package managers and platforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component Architecture&lt;/strong&gt; - Allowing users to install only what they need&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version Management&lt;/strong&gt; - Ensuring compatibility across different versions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing Infrastructure&lt;/strong&gt; - Validating that your packages actually work&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The deployment system we&apos;ve built transforms ColumbaEngine from a complex, hard-to-use source project into a professional library that integrates seamlessly into any CMake-based project.&lt;/p&gt;
&lt;p&gt;Remember: &lt;strong&gt;Distribution is not an afterthought&lt;/strong&gt; - it&apos;s a core part of your project&apos;s success. A library that&apos;s hard to install and use won&apos;t get adopted, no matter how good the code is.&lt;/p&gt;
&lt;p&gt;Your deployment system is your project&apos;s handshake with the world. Make it count.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The complete source code for ColumbaEngine with all the CMake patterns from this series is available on &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine&quot;&gt;GitHub&lt;/a&gt;. These patterns scale from small libraries to large, complex applications.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coming Next&lt;/strong&gt;: An appendix addressing the excellent feedback from the CMake community, covering modern patterns like FILE_SET, superbuild approaches, and improved dependency management strategies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What distribution challenges have you faced?&lt;/strong&gt; Share your experiences in the comments below!&lt;/p&gt;</content:encoded></item><item><title>Self-hosting WebAssembly game with Astro</title><link>https://columbaengine.org/blog/embed-em-in-js/</link><guid isPermaLink="true">https://columbaengine.org/blog/embed-em-in-js/</guid><description>How to embed multiple WASM demos in Astro with modal launchers, flexible file paths and build-time demo discovery.</description><pubDate>Tue, 12 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Embedding WASM games in Astro like itch.io does&lt;/h2&gt;
&lt;p&gt;I wanted to embed multiple playable WASM demos directly on my Astro site - click play, game loads in a modal, no page navigation. Basically what itch.io does, but self-hosted. Sounds simple, but WASM has some specific requirements that made this more complex than expected.&lt;/p&gt;
&lt;h3&gt;The problem&lt;/h3&gt;
&lt;p&gt;I had compiled my game engine (ColumbaEngine) to WebAssembly and wanted to showcase multiple demos on my site. Just dropping the files into Astro and calling it a day? Not happening. WASM needs specific headers, correct MIME types, and SharedArrayBuffer support for threading. Plus, I wanted a smooth experience where users could switch between demos without page reloads.&lt;/p&gt;
&lt;h3&gt;First attempt - just drop files in src/&lt;/h3&gt;
&lt;p&gt;Started by putting WASM files directly in the src directory. Astro&apos;s build process immediately broke this - files got hashed, moved around, and the WASM loader couldn&apos;t find anything.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// This doesn&apos;t work - Astro will process these files
Module.locateFile = (filename) =&gt; {
  return &apos;/src/demos/&apos; + filename; // Files won&apos;t be there after build
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Moving to public/ - better but not enough&lt;/h3&gt;
&lt;p&gt;Moved everything to &lt;code&gt;public/demos/&lt;/code&gt;. Files stay untouched during build, paths are predictable. Each demo gets its own directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public/demos/
├── demo1/
│   ├── demo1.wasm
│   ├── demo1.js
│   ├── demo1.data
│   └── demo1.worker.js
└── demo2/
    ├── game.wasm    # Some demos use different naming
    ├── game.js
    └── game.data
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Created a flexible path resolver to handle naming variations&lt;/h3&gt;
&lt;p&gt;Different build tools and Emscripten versions produce different file naming patterns. Some demos had &lt;code&gt;demo1.wasm&lt;/code&gt;, others had generic &lt;code&gt;game.wasm&lt;/code&gt;. Instead of renaming everything manually (and breaking future builds), I built a fallback system that checks multiple naming patterns. First it looks for files matching the demo slug name, then falls back to generic names. This way I can drop in any WASM build and it just works.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getGameConfig(slug) {
  return {
    wasmFile: `/demos/${slug}/${slug}.wasm`,
    jsFile: `/demos/${slug}/${slug}.js`,
    dataFile: `/demos/${slug}/${slug}.data`,
    workerFile: `/demos/${slug}/${slug}.worker.js`,
    fallbacks: {
      wasmFile: `/demos/${slug}/game.wasm`,
      jsFile: `/demos/${slug}/game.js`,
      dataFile: `/demos/${slug}/game.data`,
      workerFile: `/demos/${slug}/game.worker.js`
    }
  };
}

async function getWorkingPath(primary, fallback) {
  if (await checkFileExists(primary)) return primary;
  if (fallback &amp;#x26;&amp;#x26; await checkFileExists(fallback)) return fallback;
  return primary;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the files in place and the paths sorted, I thought I was done. Then I tried enabling threading and hit the next wall.&lt;/p&gt;
&lt;h3&gt;The CORS nightmare - SharedArrayBuffer requirements&lt;/h3&gt;
&lt;p&gt;SharedArrayBuffer enables true multi-threading in WASM, but browsers disabled it after Spectre/Meltdown unless you promise you know what you&apos;re doing via specific headers. Without &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt; and &lt;code&gt;Cross-Origin-Embedder-Policy&lt;/code&gt;, your multi-threaded WASM silently falls back to single-threaded mode or just crashes with &quot;SharedArrayBuffer is not defined&quot;. The worst part? These headers must be set everywhere - on the HTML page, on the WASM files, on the worker scripts. Miss one place and everything breaks.&lt;/p&gt;
&lt;p&gt;Development setup via Vite plugin in &lt;code&gt;astro.config.mjs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function addCrossOriginHeaders() {
  return {
    name: &apos;add-cross-origin-headers&apos;,
    configureServer(server) {
      server.middlewares.use((req, res, next) =&gt; {
        res.setHeader(&apos;Cross-Origin-Opener-Policy&apos;, &apos;same-origin&apos;);
        res.setHeader(&apos;Cross-Origin-Embedder-Policy&apos;, &apos;require-corp&apos;);
        res.setHeader(&apos;Cross-Origin-Resource-Policy&apos;, &apos;cross-origin&apos;);

        // Correct MIME type for WASM
        if (req.url?.endsWith(&apos;.wasm&apos;)) {
          res.setHeader(&apos;Content-Type&apos;, &apos;application/wasm&apos;);
        }
        next();
      });
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Production headers via &lt;code&gt;public/_headers&lt;/code&gt; file (this is Cloudflare Pages&apos; way of setting headers - other hosts use different methods like &lt;code&gt;.htaccess&lt;/code&gt; or &lt;code&gt;vercel.json&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/demos/*
  Cross-Origin-Embedder-Policy: require-corp
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Resource-Policy: cross-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;_headers&lt;/code&gt; file tells Cloudflare to apply these headers to anything under &lt;code&gt;/demos/&lt;/code&gt;. But that&apos;s not enough - the HTML page loading the game also needs these headers, otherwise the browser blocks SharedArrayBuffer at the page level. So in the Astro page component itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// In src/pages/demos/index.astro
if (Astro.response) {
  Astro.response.headers.set(&apos;Cross-Origin-Embedder-Policy&apos;, &apos;require-corp&apos;);
  Astro.response.headers.set(&apos;Cross-Origin-Opener-Policy&apos;, &apos;same-origin&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Headers fixed, threading working. Now I needed a way to manage all these demos without editing code every time I added a new one.&lt;/p&gt;
&lt;h3&gt;Automating demo discovery&lt;/h3&gt;
&lt;p&gt;Instead of manually maintaining a list of demos, built a system that scans the demos directory at build time. Every time I add a new demo folder, it automatically appears on the site after rebuild. No config files to update, no hardcoded lists to maintain. Just drop a new WASM build in a folder and it&apos;s live. If a demo has a &lt;code&gt;metadata.json&lt;/code&gt; file, it uses that for the title and description. Otherwise, it generates them from the folder name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function getDemos() {
  const fs = await import(&apos;fs/promises&apos;);
  const path = await import(&apos;path&apos;);
  const demosDir = &apos;public/demos&apos;;
  const demosList = [];

  const entries = await fs.readdir(demosDir);

  for (const entry of entries) {
    const demoPath = path.join(demosDir, entry);
    const stats = await fs.stat(demoPath);

    if (stats.isDirectory()) {
      const files = await fs.readdir(demoPath);

      // Optional metadata.json for custom info
      let metadata = {};
      try {
        const metadataPath = path.join(demoPath, &apos;metadata.json&apos;);
        const metadataContent = await fs.readFile(metadataPath, &apos;utf-8&apos;);
        metadata = JSON.parse(metadataContent);
      } catch {
        // Use defaults if no metadata
      }

      demosList.push({
        slug: entry,
        title: metadata.title || entry.charAt(0).toUpperCase() + entry.slice(1).replace(/-/g, &apos; &apos;),
        description: metadata.description || `WebAssembly demo: ${entry}`,
        coverImage: files.find(f =&gt; f.match(/^cover\.(png|jpg|webp)$/)) ?
                    `/demos/${entry}/${coverImage}` : null,
        tags: metadata.tags || [&apos;Demo&apos;]
      });
    }
  }
  return demosList;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now I had a nice grid of demos that auto-populated from the file system. Clicking on them though? That&apos;s where things got interesting with Emscripten&apos;s expectations about DOM structure.&lt;/p&gt;
&lt;h3&gt;The modal loader&lt;/h3&gt;
&lt;p&gt;Built a modal system that creates a fresh canvas for each game. The key insight was that Emscripten expects a specific canvas element to exist before initialization, and it gets confused if you try to reuse the same canvas for different games. So instead of complex canvas recycling logic, I just create a new canvas element each time a user clicks play. The modal handles the overlay, escape key binding, and click-outside-to-close behavior, while the canvas container gets completely replaced for each game:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function openGameModal(slug, title) {
  currentDemoSlug = slug;
  modal.classList.add(&apos;active&apos;);
  document.body.style.overflow = &apos;hidden&apos;;

  // Fresh canvas every time
  const canvas = document.createElement(&apos;canvas&apos;);
  canvas.id = &apos;canvas&apos;;
  canvas.width = 820;
  canvas.height = 640;

  canvasContainer.innerHTML = &apos;&apos;;
  canvasContainer.appendChild(canvas);

  await loadGame(slug);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual WASM loading with Emscripten module setup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function loadGame(slug) {
  const canvas = document.getElementById(&apos;canvas&apos;);
  const config = getGameConfig(slug);

  // Get actual file paths with fallbacks
  const GAME_CONFIG = {
    wasmFile: await getWorkingPath(config.wasmFile, config.fallbacks.wasmFile),
    jsFile: await getWorkingPath(config.jsFile, config.fallbacks.jsFile),
    dataFile: await getWorkingPath(config.dataFile, config.fallbacks.dataFile),
    workerFile: await getWorkingPath(config.workerFile, config.fallbacks.workerFile)
  };

  // Module configuration
  window.Module = {
    canvas,

    locateFile(filename) {
      if (filename.endsWith(&apos;.wasm&apos;)) return GAME_CONFIG.wasmFile;
      if (filename.endsWith(&apos;.data&apos;)) return GAME_CONFIG.dataFile;
      if (filename.endsWith(&apos;.worker.js&apos;)) return GAME_CONFIG.workerFile;
      return `/demos/${slug}/` + filename;
    },

    monitorRunDependencies(left) {
      updateLoadingText(left ? `Loading... (${left} files)` : &apos;Starting...&apos;);
    },

    onRuntimeInitialized() {
      console.log(&apos;[WASM] Runtime initialized&apos;);
      updateLoadingText(&apos;Ready!&apos;);
      hideLoadingSoon();
      canvas.focus();
    },

    print: (t) =&gt; console.log(&apos;[GAME]&apos;, t),
    printErr: (t) =&gt; console.error(&apos;[GAME ERROR]&apos;, t)
  };

  // Enable threading if available
  const hasWorker = await checkFileExists(GAME_CONFIG.workerFile);
  const hasSAB = typeof SharedArrayBuffer !== &apos;undefined&apos;;

  if (hasWorker &amp;#x26;&amp;#x26; hasSAB) {
    gameModule.PTHREAD_POOL_SIZE = 2;
    // Worker wrapper setup for SharedArrayBuffer support...
  }

  // Load WASM binary first
  const wasmResponse = await fetch(GAME_CONFIG.wasmFile);
  const wasmBuffer = await wasmResponse.arrayBuffer();
  gameModule.wasmBinary = wasmBuffer;

  // Then load the JS glue code
  const script = document.createElement(&apos;script&apos;);
  script.src = GAME_CONFIG.jsFile + &apos;?t=&apos; + Date.now();
  document.head.appendChild(script);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything was working great - games loaded, ran smoothly, modal closed properly. But then I noticed the browser getting slower after loading multiple demos. Time to deal with cleanup.&lt;/p&gt;
&lt;h3&gt;Memory management - the nuclear option&lt;/h3&gt;
&lt;p&gt;Tried implementing proper cleanup for switching between demos. Module cleanup, GL context disposal, worker termination - worked sometimes, crashed randomly. The games would leak memory, and after 4-5 demos the browser tab would die.&lt;/p&gt;
&lt;p&gt;The solution? Page refresh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function exitGame() {
  console.log(&apos;[WASM] Refreshing page to clean up game...&apos;);
  window.location.reload();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sounds crude, but it&apos;s actually brilliant. The browser&apos;s built-in cleanup on page navigation is bulletproof. Since everything is static and cached (thanks to Astro&apos;s build process), the refresh is nearly instant. Users get a clean slate every time, no memory leaks, no accumulated state.&lt;/p&gt;
&lt;h3&gt;Production build considerations&lt;/h3&gt;
&lt;p&gt;Astro build configuration that keeps everything working:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export default defineConfig({
  output: &apos;static&apos;,

  adapter: cloudflare({
    mode: &apos;directory&apos;
  }),

  vite: {
    plugins: [addCrossOriginHeaders()],
    assetsInclude: [&apos;**/*.wasm&apos;, &apos;**/*.data&apos;],
    optimizeDeps: {
      exclude: [&apos;*.wasm&apos;, &apos;*.data&apos;, &apos;*.worker.js&apos;]
    },
    build: {
      format: &apos;file&apos; // Generates .html files for all routes
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The result&lt;/h3&gt;
&lt;p&gt;A seamless WASM game embedding system that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Loads games in modals without page navigation&lt;/li&gt;
&lt;li&gt;Handles multiple file naming conventions&lt;/li&gt;
&lt;li&gt;Supports SharedArrayBuffer and threading&lt;/li&gt;
&lt;li&gt;Cleans up perfectly via page refresh&lt;/li&gt;
&lt;li&gt;Works in development and production (Cloudflare Pages)&lt;/li&gt;
&lt;li&gt;Auto-discovers demos from the file system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Live demos at: &lt;a href=&quot;https://columbaengine.org/demos&quot;&gt;columbaengine.org/demos&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The key lesson: stop fighting the platform. Browsers are really good at certain things - use those strengths instead of trying to reinvent them. WASM in the browser is complex enough without adding unnecessary complexity on top.&lt;/p&gt;</content:encoded></item><item><title>Understanding Modern Game Engine Architecture with ECS</title><link>https://columbaengine.org/blog/ecs-architecture-with-ecs/</link><guid isPermaLink="true">https://columbaengine.org/blog/ecs-architecture-with-ecs/</guid><description>How ColumbaEngine&apos;s Entity Component System Makes Game Development Simple</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Reading time: ~5 minutes | Level: Intermediate | Next: Building Complete Games with ColumbaEngine&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Your Game Architecture Probably Sucks (And That&apos;s Not Your Fault)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s be honest. If you&apos;ve built games the &quot;traditional&quot; way—with inheritance hierarchies, GameObject base classes, and virtual functions everywhere—you&apos;ve probably hit that moment. You know the one. Where adding a simple feature means refactoring half your codebase. Where your &quot;Player&quot; class is 2,000 lines long. Where making a treasure chest that can be destroyed requires multiple inheritance gymnastics that would make a C++ compiler cry.&lt;/p&gt;
&lt;p&gt;You&apos;re not alone. I&apos;ve been there. The entire industry has been there. That&apos;s why Unity rewrote their entire engine with DOTS. Why Unreal built Mass Entity. Why id Software moved to ECS for DOOM Eternal. The old way of building games is fundamentally broken.&lt;/p&gt;
&lt;p&gt;But here&apos;s the thing: there&apos;s a better way. It&apos;s called Entity Component System (ECS), and it&apos;s not just another programming pattern—it&apos;s a completely different way of thinking about game architecture. Today, I&apos;ll show you exactly how it works using &lt;strong&gt;ColumbaEngine&lt;/strong&gt;, our production-ready ECS engine, with real code you can compile and run right now.&lt;/p&gt;
&lt;p&gt;No theory. No abstract concepts. Just practical, working examples that will change how you build games forever.&lt;/p&gt;
&lt;h2&gt;Why Traditional Game Programming Is Broken&lt;/h2&gt;
&lt;p&gt;Let&apos;s say you&apos;re building an action RPG. You start with a reasonable class hierarchy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class GameObject {
public:
    virtual void update(float deltaTime) = 0;
    virtual void render() = 0;
};

class Character : public GameObject {
protected:
    float health;
    float x, y;
    Sprite sprite;
public:
    void takeDamage(float amount) { health -= amount; }
    void move(float dx, float dy) { x += dx; y += dy; }
};

class Player : public Character { /* ... */ };
class Enemy : public Character { /* ... */ };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks good, right? Then reality hits:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;We need treasure chests&quot;&lt;/strong&gt; - They render and have position, but don&apos;t move or have health. Where do they fit?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;We need moving platforms&quot;&lt;/strong&gt; - They move but aren&apos;t characters. Another branch?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;We need ghosts that pass through walls&quot;&lt;/strong&gt; - Enemies that don&apos;t collide normally. Special case?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;We need destructible walls&quot;&lt;/strong&gt; - Have health but don&apos;t move. Now what?&lt;/p&gt;
&lt;p&gt;Soon your clean hierarchy becomes a nightmare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class GameObject { };
class PhysicalObject : public GameObject { };
class AnimatedObject : public GameObject { };
class AnimatedPhysicalObject : public PhysicalObject, public AnimatedObject { };
class Character : public AnimatedPhysicalObject { };
// ... endless combinations leading to chaos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;ve seen real game projects with 147 GameObject subclasses, 23 levels of inheritance, and 40% of development time spent refactoring this mess. Adding a simple enemy type—something that should take hours—takes days of carefully threading it through the inheritance maze.&lt;/p&gt;
&lt;h2&gt;Enter ECS: A Radically Simple Solution&lt;/h2&gt;
&lt;p&gt;Entity Component System (ECS) completely reimagines game architecture. Instead of asking &quot;what IS this object?&quot; (inheritance), ECS asks &quot;what DATA does this object have?&quot; and &quot;what SYSTEMS operate on that data?&quot;&lt;/p&gt;
&lt;h3&gt;The Three Pillars of ECS&lt;/h3&gt;
&lt;h4&gt;Entities: Just an ID&lt;/h4&gt;
&lt;p&gt;In ColumbaEngine, an entity is remarkably simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Entity {
    _unique_id id;                    // Unique identifier
    std::list&amp;#x3C;EntityHeld&gt; components;  // What components this entity has
    std::string name;                  // For debugging
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it. An entity is just a unique ID that groups components together—a container, or even simpler, a sticky note saying &quot;these components belong together.&quot;&lt;/p&gt;
&lt;h4&gt;Components: Pure Data&lt;/h4&gt;
&lt;p&gt;Components hold your game&apos;s state with zero logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Position : public Component {
    float x, y;
    DEFAULT_COMPONENT_MEMBERS(Position)
};

struct Velocity : public Component {
    float dx, dy;
    DEFAULT_COMPONENT_MEMBERS(Velocity)
};

struct Health : public Component {
    float current, max;
    DEFAULT_COMPONENT_MEMBERS(Health)
};

struct Sprite : public Component {
    std::string texturePath;
    int width, height;
    DEFAULT_COMPONENT_MEMBERS(Sprite)
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No methods. No behavior. Just data. This separation is powerful—components are trivial to serialize, network, and modify with tools.&lt;/p&gt;
&lt;h4&gt;Systems: Pure Logic&lt;/h4&gt;
&lt;p&gt;Systems contain all your game logic, operating on entities with specific component combinations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Movement system - processes entities with Position AND Velocity
class MovementSystem : public System&amp;#x3C;Own&amp;#x3C;Position&gt;, Own&amp;#x3C;Velocity&gt;&gt; {
public:
    void execute() override {
        float deltaTime = timer.getDeltaTime();

        // Process ALL entities that have both Position and Velocity
        for (auto [entity, pos, vel] : getEntities()) {
            pos-&gt;x += vel-&gt;dx * deltaTime;
            pos-&gt;y += vel-&gt;dy * deltaTime;
        }
    }
};

// Render system - draws entities with Position AND Sprite
class RenderSystem : public System&amp;#x3C;Own&amp;#x3C;Position&gt;, Own&amp;#x3C;Sprite&gt;&gt; {
public:
    void execute() override {
        for (auto [entity, pos, sprite] : getEntities()) {
            drawSprite(sprite-&gt;texturePath, pos-&gt;x, pos-&gt;y);
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The system doesn&apos;t care what entity it&apos;s processing—player, enemy, projectile—if it has the required components, it gets processed.&lt;/p&gt;
&lt;h3&gt;The Power of Composition&lt;/h3&gt;
&lt;p&gt;Here&apos;s how those problematic game objects look in ColumbaEngine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Player - moves, renders, takes damage, player-controlled
Entity player = ecs-&gt;createEntity(&quot;Player&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(player, {100, 100});
ecs-&gt;attach&amp;#x3C;Velocity&gt;(player, {0, 0});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(player, {&quot;player.png&quot;, 32, 32});
ecs-&gt;attach&amp;#x3C;Health&gt;(player, {100, 100});
ecs-&gt;attach&amp;#x3C;PlayerController&gt;(player);

// Enemy - same as player but AI-controlled
Entity enemy = ecs-&gt;createEntity(&quot;Goblin&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(enemy, {300, 100});
ecs-&gt;attach&amp;#x3C;Velocity&gt;(enemy, {0, 0});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(enemy, {&quot;goblin.png&quot;, 32, 32});
ecs-&gt;attach&amp;#x3C;Health&gt;(enemy, {50, 50});
ecs-&gt;attach&amp;#x3C;AIController&gt;(enemy);  // Only difference!

// Treasure chest - renders, has position, but doesn&apos;t move
Entity chest = ecs-&gt;createEntity(&quot;TreasureChest&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(chest, {200, 200});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(chest, {&quot;chest.png&quot;, 32, 32});
ecs-&gt;attach&amp;#x3C;Container&gt;(chest, {{&quot;gold&quot;, 100}});

// Ghost - enemy that passes through walls
Entity ghost = ecs-&gt;createEntity(&quot;Ghost&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(ghost, {400, 400});
ecs-&gt;attach&amp;#x3C;Velocity&gt;(ghost, {0, 0});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(ghost, {&quot;ghost.png&quot;, 32, 32});
ecs-&gt;attach&amp;#x3C;Health&gt;(ghost, {30, 30});
ecs-&gt;attach&amp;#x3C;AIController&gt;(ghost);
// Note: No Collider component = passes through walls!

// Moving platform - moves but no health or AI
Entity platform = ecs-&gt;createEntity(&quot;MovingPlatform&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(platform, {150, 300});
ecs-&gt;attach&amp;#x3C;Velocity&gt;(platform, {50, 0});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(platform, {&quot;platform.png&quot;, 64, 16});
ecs-&gt;attach&amp;#x3C;Path&gt;(platform, {{150, 300}, {250, 300}});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No inheritance hierarchies. No virtual functions. No &quot;is-a&quot; relationships. Just entities with exactly the components they need.&lt;/p&gt;
&lt;p&gt;Want the chest to be destructible? Add a Health component. Want the ghost to leave a trail? Add a ParticleEmitter. Each change is one line that doesn&apos;t affect anything else.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Building Tetris with ECS&lt;/h2&gt;
&lt;p&gt;Let&apos;s build something real. Here&apos;s how Tetris works in ColumbaEngine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Tetris-specific components
struct GridPosition : public Component {
    int x, y;  // Position in game grid (not pixels)
    DEFAULT_COMPONENT_MEMBERS(GridPosition)
};

struct TetrisPiece : public Component {
    enum Type { I, O, T, S, Z, J, L } type;
    int rotation = 0;
    DEFAULT_COMPONENT_MEMBERS(TetrisPiece)
};

struct FallTimer : public Component {
    float timeUntilFall;
    float fallSpeed;
    DEFAULT_COMPONENT_MEMBERS(FallTimer)
};

// Create a falling piece
Entity piece = ecs-&gt;createEntity(&quot;ActivePiece&quot;);
ecs-&gt;attach&amp;#x3C;GridPosition&gt;(piece, {5, 0});        // Start at top
ecs-&gt;attach&amp;#x3C;TetrisPiece&gt;(piece, {Type::T, 0});   // T-shaped piece
ecs-&gt;attach&amp;#x3C;FallTimer&gt;(piece, {1.0f, 1.0f});     // Fall every second
ecs-&gt;attach&amp;#x3C;Sprite&gt;(piece, {&quot;t_piece.png&quot;, 30, 30});
ecs-&gt;attach&amp;#x3C;PlayerControllable&gt;(piece);          // Player can control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the systems that make it work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Handles piece falling
class TetrisFallSystem : public System&amp;#x3C;Own&amp;#x3C;GridPosition&gt;,
                                       Own&amp;#x3C;TetrisPiece&gt;,
                                       Own&amp;#x3C;FallTimer&gt;&gt; {
public:
    void execute() override {
        float deltaTime = timer.getDeltaTime();

        for (auto [entity, gridPos, piece, fallTimer] : getEntities()) {
            fallTimer-&gt;timeUntilFall -= deltaTime;

            if (fallTimer-&gt;timeUntilFall &amp;#x3C;= 0) {
                fallTimer-&gt;timeUntilFall = fallTimer-&gt;fallSpeed;

                if (canMoveTo(gridPos-&gt;x, gridPos-&gt;y + 1, piece)) {
                    gridPos-&gt;y += 1;
                } else {
                    lockPiece(entity);
                }
            }
        }
    }

private:
    void lockPiece(Entity entity) {
        ecs-&gt;detach&amp;#x3C;PlayerControllable&gt;(entity);
        ecs-&gt;detach&amp;#x3C;FallTimer&gt;(entity);
        ecs-&gt;sendEvent(PieceLocked{entity});
        ecs-&gt;sendEvent(CheckForLines{});
        ecs-&gt;sendEvent(SpawnNewPiece{});
    }
};

// Handles player input
class TetrisInputSystem : public System&amp;#x3C;Own&amp;#x3C;PlayerControllable&gt;,
                                        Own&amp;#x3C;GridPosition&gt;,
                                        Own&amp;#x3C;TetrisPiece&gt;&gt; {
public:
    void onKeyPressed(int key) {
        for (auto [entity, control, gridPos, piece] : getEntities()) {
            switch(key) {
                case KEY_LEFT:
                    if (canMoveTo(gridPos-&gt;x - 1, gridPos-&gt;y, piece))
                        gridPos-&gt;x -= 1;
                    break;

                case KEY_RIGHT:
                    if (canMoveTo(gridPos-&gt;x + 1, gridPos-&gt;y, piece))
                        gridPos-&gt;x += 1;
                    break;

                case KEY_UP:
                    rotatePiece(piece);
                    break;

                case KEY_DOWN:
                    // Speed up falling
                    if (auto timer = ecs-&gt;getComponent&amp;#x3C;FallTimer&gt;(entity))
                        timer-&gt;fallSpeed = 0.05f;
                    break;
            }
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the magic? The FallSystem doesn&apos;t know about input or rendering. The InputSystem doesn&apos;t know about the board state. The RenderSystem (which we already defined) will automatically draw anything with Position and Sprite. They communicate through events without knowing about each other.&lt;/p&gt;
&lt;h2&gt;Why This Changes Everything&lt;/h2&gt;
&lt;h3&gt;Performance That Scales&lt;/h3&gt;
&lt;p&gt;ECS provides massive performance benefits through data locality:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Traditional OOP - objects scattered in memory
GameObject* objects[1000];
for (int i = 0; i &amp;#x3C; 1000; i++) {
    objects[i]-&gt;update();  // Cache miss likely for each object
}

// ECS - components stored contiguously
Position positions[1000];
Velocity velocities[1000];
for (int i = 0; i &amp;#x3C; 1000; i++) {
    positions[i].x += velocities[i].dx * deltaTime;  // Cache-friendly!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Real measurements show 5-10x performance improvements for systems processing many entities.&lt;/p&gt;
&lt;h3&gt;Flexibility Without Complexity&lt;/h3&gt;
&lt;p&gt;Need to add a feature? Just create a new component and system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Add particle effects to any entity
struct ParticleEmitter : public Component {
    std::string particleType;
    float rate;
};

class ParticleSystem : public System&amp;#x3C;Own&amp;#x3C;Position&gt;, Own&amp;#x3C;ParticleEmitter&gt;&gt; {
    void execute() override {
        for (auto [entity, pos, emitter] : getEntities()) {
            spawnParticles(pos-&gt;x, pos-&gt;y, emitter-&gt;particleType, emitter-&gt;rate);
        }
    }
};

// Now ANY entity can have particles
ecs-&gt;attach&amp;#x3C;ParticleEmitter&gt;(player, {&quot;sparkle&quot;, 10.0f});
ecs-&gt;attach&amp;#x3C;ParticleEmitter&gt;(treasure, {&quot;glow&quot;, 5.0f});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Parallel Processing for Free&lt;/h3&gt;
&lt;p&gt;Systems that don&apos;t share components can run in parallel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// These can run simultaneously
parallel_execute(
    movementSystem,      // Modifies: Position
    animationSystem,     // Modifies: Sprite
    particleSystem,      // Modifies: ParticleEmitter
    audioSystem          // Modifies: AudioSource
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ColumbaEngine automatically detects parallelization opportunities, giving you free performance on multi-core processors.&lt;/p&gt;
&lt;h2&gt;The Industry Is Moving to ECS&lt;/h2&gt;
&lt;p&gt;This isn&apos;t just theory. Major engines are adopting ECS because it works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unity DOTS&lt;/strong&gt;: Complete ECS rewrite for 100x performance gains&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unreal Mass Entity&lt;/strong&gt;: ECS for massive crowds and simulations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bevy&lt;/strong&gt;: Rust engine built entirely on ECS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Godot&lt;/strong&gt;: Experimenting with ECS for Godot 4.x&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By learning ECS through ColumbaEngine&apos;s clean implementation, you&apos;re learning the future of game development.&lt;/p&gt;
&lt;h2&gt;Start Building with ColumbaEngine Today&lt;/h2&gt;
&lt;p&gt;Ready to experience ECS yourself? Here&apos;s how to start:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Clone and Build:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/columbaengine/columba
cd columba &amp;#x26;&amp;#x26; mkdir build &amp;#x26;&amp;#x26; cd build
cmake .. &amp;#x26;&amp;#x26; make
./examples/tetris  # Play the Tetris example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Create Your First Game:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto ecs = std::make_unique&amp;#x3C;EntitySystem&gt;();

// Create player
auto player = ecs-&gt;createEntity(&quot;Player&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(player, {100, 100});
ecs-&gt;attach&amp;#x3C;Velocity&gt;(player, {0, 0});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(player, {&quot;player.png&quot;, 32, 32});

// Create enemy
auto enemy = ecs-&gt;createEntity(&quot;Enemy&quot;);
ecs-&gt;attach&amp;#x3C;Position&gt;(enemy, {200, 100});
ecs-&gt;attach&amp;#x3C;Velocity&gt;(enemy, {-10, 0});
ecs-&gt;attach&amp;#x3C;Sprite&gt;(enemy, {&quot;enemy.png&quot;, 32, 32});

// Systems automatically process them!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Join the Community:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href=&quot;https://github.com/&quot;&gt;github.com/columbaengine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discord&lt;/strong&gt;: Active developers building real games&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docs&lt;/strong&gt;: Complete API reference and tutorials&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why ColumbaEngine?&lt;/h2&gt;
&lt;p&gt;ColumbaEngine isn&apos;t just another engine—it&apos;s a learning platform for modern game architecture. With clean, readable code and a pure ECS implementation, it&apos;s perfect for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Learning&lt;/strong&gt;: Understand how modern engines really work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prototyping&lt;/strong&gt;: Build games quickly with minimal boilerplate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production&lt;/strong&gt;: Ships with WebAssembly support for browser deployment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teaching&lt;/strong&gt;: Clear architecture perfect for education&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether you&apos;re building your first game or your hundredth, ColumbaEngine&apos;s ECS architecture provides the foundation you need without the complexity you don&apos;t.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;The best way to understand ECS is to use it. Download ColumbaEngine, run the examples, and build something. Start small—a Pong clone, a simple shooter—and watch how naturally features come together.&lt;/p&gt;
&lt;p&gt;In future articles, we&apos;ll explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced ECS patterns and optimizations&lt;/li&gt;
&lt;li&gt;Building multiplayer with component synchronization&lt;/li&gt;
&lt;li&gt;WebAssembly deployment for browser games&lt;/li&gt;
&lt;li&gt;Creating custom tools and editors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The game industry is moving toward data-driven architectures. Join us at the forefront with ColumbaEngine.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Ready to revolutionize how you build games? Star us on &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; and join our &lt;a href=&quot;https://discord.gg/&quot;&gt;Discord&lt;/a&gt; community.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>CMake for Complex Projects (Part 1): Building a C++ Game Engine from Scratch for Desktop and WebAssembly</title><link>https://columbaengine.org/blog/cmake-part1/</link><guid isPermaLink="true">https://columbaengine.org/blog/cmake-part1/</guid><description>A practical guide to modern CMake through a real-world C++ game engine project - Compilation &amp; Build Management</description><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A practical guide to modern CMake through a real-world C++ game engine project - Compilation &amp;#x26; Build Management&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you&apos;ve ever tried to build a C++ project with multiple dependencies and cross-platform support, you know the pain. Makefiles become unwieldy, Visual Studio solutions don&apos;t work on Linux, and don&apos;t even get me started on trying to distribute your library for others to use.&lt;/p&gt;
&lt;p&gt;This is where CMake shines. But most CMake tutorials show you toy examples with a single &lt;code&gt;hello.cpp&lt;/code&gt; file. Today, we&apos;re going deep with a &lt;strong&gt;complex project&lt;/strong&gt; - a complete game engine called ColumbaEngine (source code available &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine&quot;&gt;here&lt;/a&gt;) with 80+ source files, multiple platforms (including web compilation), and a sophisticated build system.&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;Part 1&lt;/strong&gt; of a two-part series. In this article, we&apos;ll focus on the &lt;strong&gt;compilation and build management&lt;/strong&gt; aspects: setting up the project, handling dependencies, cross-platform builds, and testing. In Part 2, we&apos;ll cover the &lt;strong&gt;deployment side&lt;/strong&gt;: installation systems, package generation, and distribution.&lt;/p&gt;
&lt;p&gt;By the end of this article, you&apos;ll understand how to structure CMake for medium to large projects, handle complex dependencies, and create professional build systems that compile reliably across platforms.&lt;/p&gt;
&lt;h2&gt;What Makes This Project Interesting?&lt;/h2&gt;
&lt;p&gt;Before we dive into CMake, let&apos;s understand what we&apos;re building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;80+ C++ source files&lt;/strong&gt; organized in modules (ECS, Renderer, Audio, UI, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform&lt;/strong&gt;: Windows, Linux, and Web (via Emscripten)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple dependencies&lt;/strong&gt;: SDL2, OpenGL, FreeType, custom libraries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Installable library&lt;/strong&gt;: Other projects can use it with &lt;code&gt;find_package()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example applications&lt;/strong&gt;: Several games and tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package generation&lt;/strong&gt;: Creates &lt;code&gt;.deb&lt;/code&gt;, &lt;code&gt;.rpm&lt;/code&gt;, and other installers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn&apos;t a toy project - it&apos;s a real game engine that needs a robust build system.&lt;/p&gt;
&lt;h2&gt;Project Structure: The Foundation&lt;/h2&gt;
&lt;p&gt;Here&apos;s how our game engine is organized:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ColumbaEngine/
├── CMakeLists.txt             # Main build file
├── src/
│   ├── Engine/                # Core engine code
│   │   ├── ECS/               # Entity-Component-System
│   │   ├── Renderer/          # Graphics rendering
│   │   └── Audio/             # Sound system
│   └── main.cpp               # Editor application
├── examples/                  # Example games
├── import/                    # Vendored dependencies
├── cmake/                     # CMake modules
└── test/                      # Unit tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;CMakeLists.txt&lt;/code&gt; is our blueprint. Let&apos;s build it step by step.&lt;/p&gt;
&lt;h2&gt;Step 1: Project Setup and Configuration&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;cmake_minimum_required(VERSION 3.18)
project(ColumbaEngine VERSION 1.0)

# User-configurable options
option(BUILD_EXAMPLES &quot;Build example applications&quot; ON)
option(BUILD_STATIC_LIB &quot;Build static library only&quot; OFF)
option(ENABLE_TIME_TRACE &quot;Add -ftime-trace to Clang builds&quot; OFF)

# Global settings
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)  # For IDE support
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key concepts here:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cmake_minimum_required&lt;/code&gt;&lt;/strong&gt;: We use 3.18+ for modern features like precompiled headers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;option()&lt;/code&gt;&lt;/strong&gt;: Creates user-configurable boolean flags. Users can run &lt;code&gt;cmake -DBUILD_EXAMPLES=OFF ..&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CMAKE_EXPORT_COMPILE_COMMANDS&lt;/code&gt;&lt;/strong&gt;: Generates &lt;code&gt;compile_commands.json&lt;/code&gt; for IDEs and tools like clangd&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 2: The Dependency Challenge&lt;/h2&gt;
&lt;p&gt;Real projects have dependencies. Lots of them. Our game engine needs SDL2 for windowing, FreeType for text rendering, OpenGL for graphics, and more.&lt;/p&gt;
&lt;p&gt;We use a &lt;strong&gt;vendoring approach&lt;/strong&gt; - including dependencies directly in our source tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Dependency paths - everything is self-contained
set(SDL2_DIR &quot;import/SDL2-2.28.5&quot;)
set(SDL2MIXER_DIR &quot;import/SDL2_mixer-2.6.3&quot;)
set(GLM_DIR &quot;import/glm&quot;)
set(TASKFLOW_DIR &quot;import/taskflow-3.6.0&quot;)
set(GTEST_DIR &quot;import/googletest-1.14.0&quot;)

# Configure dependencies before building them
set(TF_BUILD_EXAMPLES OFF)    # Don&apos;t build taskflow examples
set(TF_BUILD_TESTS OFF)       # Don&apos;t build taskflow tests
set(BUILD_SHARED_LIBS OFF)    # Force static linking
set(SDL2_DISABLE_INSTALL ON)  # We&apos;ll handle installation ourselves
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why vendor dependencies?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Pros&lt;/strong&gt;: Exact version control, works offline, simplified build&lt;/p&gt;
&lt;p&gt;❌ &lt;strong&gt;Cons&lt;/strong&gt;: Larger repo size, manual updates&lt;/p&gt;
&lt;p&gt;For a game engine where stability is crucial, vendoring makes sense. For web services that need frequent security updates, you might prefer package managers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Deep Dive: Dependency Management Strategies&lt;/h3&gt;
&lt;p&gt;Choosing how to handle dependencies is one of the most critical architectural decisions in C++. Let&apos;s compare the approaches:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Vendoring (Our Current Approach)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Everything is self-contained in import/
set(SDL2_DIR &quot;import/SDL2-2.28.5&quot;)
set(GLM_DIR &quot;import/glm&quot;)
add_subdirectory(${SDL2_DIR})
add_subdirectory(${GLM_DIR})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reproducible builds&lt;/strong&gt;: Exact same versions everywhere&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline builds&lt;/strong&gt;: No internet required after initial clone&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control&lt;/strong&gt;: Can patch dependencies if needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity&lt;/strong&gt;: No external tools or package managers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repository size&lt;/strong&gt;: Our import/ folder is 500MB+&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update overhead&lt;/strong&gt;: Manual process to update dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Must manually track and update vulnerable dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;License complexity&lt;/strong&gt;: Must distribute all licenses&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Package Managers (Conan, vcpkg)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# With Conan
include(conan)
conan_cmake_run(REQUIRES
    SDL2/2.28.5
    glm/0.9.9.8
    BASIC_SETUP CMAKE_TARGETS
    BUILD missing
)
target_link_libraries(ColumbaEngine PRIVATE CONAN_PKG::SDL2 CONAN_PKG::glm)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Smaller repos&lt;/strong&gt;: Dependencies downloaded on-demand&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy updates&lt;/strong&gt;: &lt;code&gt;conan install --update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary packages&lt;/strong&gt;: Pre-built binaries for faster builds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ecosystem&lt;/strong&gt;: Thousands of packages available&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;External dependency&lt;/strong&gt;: Requires internet and package manager&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version conflicts&lt;/strong&gt;: Diamond dependency problems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform support&lt;/strong&gt;: Not all packages support all platforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning curve&lt;/strong&gt;: Another tool to learn and maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. FetchContent (Modern CMake)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;include(FetchContent)

FetchContent_Declare(glm
    GIT_REPOSITORY https://github.com/g-truc/glm.git
    GIT_TAG 0.9.9.8
    GIT_SHALLOW TRUE
)

FetchContent_Declare(SDL2
    URL https://github.com/libsdl-org/SDL/releases/download/release-2.28.5/SDL2-2.28.5.tar.gz
    URL_HASH SHA256=332cb37d0be20cb9541739c61f79bae5a477427d79ae85e352089afdaf6666e4
)

FetchContent_MakeAvailable(glm SDL2)
target_link_libraries(ColumbaEngine PRIVATE glm SDL2::SDL2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMake native&lt;/strong&gt;: No external tools required&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible sources&lt;/strong&gt;: Git repos, URLs, local paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version pinning&lt;/strong&gt;: Exact commits/tags&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform&lt;/strong&gt;: Works everywhere CMake works&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build time&lt;/strong&gt;: Downloads and builds on first configure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internet required&lt;/strong&gt;: At least for first build&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No binary caching&lt;/strong&gt;: Always builds from source&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Hybrid Approach: The Best of Both Worlds&lt;/h3&gt;
&lt;p&gt;For ColumbaEngine, we could evolve to a hybrid approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Critical, stable dependencies: Vendor them
set(SDL2_DIR &quot;import/SDL2-2.28.5&quot;)
add_subdirectory(${SDL2_DIR})

# Development/testing dependencies: FetchContent
if(BUILD_TESTS)
    include(FetchContent)
    FetchContent_Declare(googletest
        GIT_REPOSITORY https://github.com/google/googletest.git
        GIT_TAG v1.14.0
        GIT_SHALLOW TRUE
    )
    FetchContent_MakeAvailable(googletest)
endif()

# Optional dependencies: find_package with fallback
find_package(Doxygen)
if(NOT Doxygen_FOUND AND ENABLE_DOCS)
    FetchContent_Declare(doxygen
        URL https://github.com/doxygen/doxygen/releases/download/Release_1_9_8/doxygen-1.9.8.src.tar.gz
    )
    FetchContent_MakeAvailable(doxygen)
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Cross-Platform Reality Check&lt;/h2&gt;
&lt;p&gt;Our engine supports three platforms, with different flags and need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native&lt;/strong&gt; (Windows/Linux): Build everything from source&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Emscripten&lt;/strong&gt; (Web): Use system-provided libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;if(${CMAKE_SYSTEM_NAME} MATCHES &quot;Emscripten&quot;)
    # Web build - use Emscripten&apos;s built-in libraries
    set(USE_FLAGS &quot;-O3 -sUSE_SDL=2 -sUSE_SDL_MIXER=2 -sUSE_FREETYPE=1 -fwasm-exceptions&quot;)
    set(CMAKE_EXE_LINKER_FLAGS &quot;${CMAKE_EXE_LINKER_FLAGS} ${USE_FLAGS}&quot;)
else()
    # Native build - compile dependencies from source
    add_subdirectory(${SDL2_DIR})
    add_subdirectory(${SDL2MIXER_DIR})
    add_subdirectory(${GLEW_DIR})
    add_subdirectory(${TTF_DIR})
endif()

# GLM is header-only, works everywhere
add_subdirectory(${GLM_DIR})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Platform detection patterns:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CMAKE_SYSTEM_NAME&lt;/code&gt;: Target platform (Windows, Linux, Darwin, Emscripten)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CMAKE_HOST_SYSTEM&lt;/code&gt;: Build machine platform&lt;/li&gt;
&lt;li&gt;Use generator expressions for conditional compilation: &lt;code&gt;$&amp;#x3C;$&amp;#x3C;PLATFORM_ID:Windows&gt;:WIN32_CODE&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 4: Creating the Main Target&lt;/h2&gt;
&lt;p&gt;Now for the heart of our build - the engine library itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# List all source files (80+ files in our case!)
set(ENGINESOURCE
    src/Engine/window.cpp
    src/Engine/configuration.cpp
    src/Engine/logger.cpp
    # ... 70+ more files
    src/Engine/UI/uisystem.cpp
)

# Create the static library
add_library(ColumbaEngine STATIC ${ENGINESOURCE})

# Modern CMake magic - precompiled headers for faster builds
target_precompile_headers(ColumbaEngine PUBLIC src/Engine/stdafx.h)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Precompiled headers&lt;/strong&gt; can cut build times by 50%+ in large projects. They compile commonly used headers once and reuse the result.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Precompiled Headers - The Build Speed Game Changer&lt;/h3&gt;
&lt;p&gt;Precompiled headers (PCH) are one of the most impactful optimizations for C++ build times, yet they&apos;re often overlooked. Let&apos;s understand how they work:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: Header Parsing Overhead&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Every .cpp file typically includes these
#include &amp;#x3C;iostream&gt;    // ~15,000 lines when fully expanded
#include &amp;#x3C;vector&gt;      // ~8,000 lines
#include &amp;#x3C;string&gt;      // ~12,000 lines
#include &amp;#x3C;memory&gt;      // ~6,000 lines
#include &amp;#x3C;SDL2/SDL.h&gt;  // ~25,000 lines
// Total: ~66,000 lines to parse per source file!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With 80 source files, that&apos;s &lt;strong&gt;5.2 million lines&lt;/strong&gt; of redundant header parsing!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: Precompiled Headers&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;target_precompile_headers(ColumbaEngine PUBLIC src/Engine/stdafx.h)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our &lt;code&gt;stdafx.h&lt;/code&gt; contains the most commonly used headers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// stdafx.h - precompiled header
#pragma once

// Standard library
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;string&gt;
#include &amp;#x3C;memory&gt;
#include &amp;#x3C;unordered_map&gt;
#include &amp;#x3C;algorithm&gt;

// Third-party libraries
#include &amp;#x3C;SDL2/SDL.h&gt;
#include &amp;#x3C;glm/glm.hpp&gt;
#include &amp;#x3C;glm/gtc/matrix_transform.hpp&gt;

// Engine fundamentals used everywhere
#include &quot;types.h&quot;
#include &quot;logger.h&quot;
#include &quot;configuration.h&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compilation&lt;/strong&gt;: CMake compiles &lt;code&gt;stdafx.h&lt;/code&gt; once into &lt;code&gt;stdafx.h.gch&lt;/code&gt; (GCC) or &lt;code&gt;stdafx.pch&lt;/code&gt; (MSVC)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reuse&lt;/strong&gt;: Every source file automatically uses the precompiled version&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt;: 66,000 lines → 0 lines to parse per file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Build Time Results&lt;/strong&gt; (80 source files):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Without PCH&lt;/strong&gt;: ~8 minutes clean build&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;With PCH&lt;/strong&gt;: ~3 minutes clean build&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental&lt;/strong&gt;: Single file changes drop from 15s to 3s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;PUBLIC vs PRIVATE PCH&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# PUBLIC: Users of your library get the PCH benefits too
target_precompile_headers(ColumbaEngine PUBLIC stdafx.h)

# PRIVATE: Only internal compilation uses PCH
target_precompile_headers(ColumbaEngine PRIVATE stdafx.h)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Best Practices for PCH&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Good PCH content: Stable, frequently used headers
#include &amp;#x3C;vector&gt;          // Used in 90% of files
#include &amp;#x3C;SDL2/SDL.h&gt;      // Core dependency

// Bad PCH content: Frequently changing headers
#include &quot;GameLogic.h&quot;     // Changes often, invalidates PCH
#include &quot;debug_temp.h&quot;    // Temporary debugging code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PCH Pitfalls&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Overuse&lt;/strong&gt;: Adding changing headers defeats the purpose&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform differences&lt;/strong&gt;: MSVC vs GCC handle PCH differently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;: PCH creates implicit dependencies between files&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step 5: Usage Requirements - The Secret Sauce&lt;/h2&gt;
&lt;p&gt;This is where modern CMake really shines. Instead of users manually figuring out include paths and linking, we specify &lt;strong&gt;usage requirements&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;target_include_directories(ColumbaEngine PUBLIC
    # When building this library
    $&amp;#x3C;BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/Engine&gt;
    $&amp;#x3C;BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/GameElements&gt;
    # When using installed version
    $&amp;#x3C;INSTALL_INTERFACE:include/ColumbaEngine&gt;
    $&amp;#x3C;INSTALL_INTERFACE:include/ColumbaEngine/GameElements&gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generator expressions&lt;/strong&gt; (&lt;code&gt;$&amp;#x3C;...&gt;&lt;/code&gt;) are evaluated during build generation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BUILD_INTERFACE&lt;/code&gt;: Only when building this project&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSTALL_INTERFACE&lt;/code&gt;: Only when using the installed version&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&amp;#x3C;CONFIG:Debug&gt;&lt;/code&gt;: Only in Debug builds&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&amp;#x3C;PLATFORM_ID:Windows&gt;&lt;/code&gt;: Only on Windows&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;Why Generator Expressions Matter&lt;/h3&gt;
&lt;p&gt;Let&apos;s understand why this is so powerful. Consider this traditional approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# The old, problematic way
if(CMAKE_BUILD_TYPE STREQUAL &quot;Debug&quot;)
    set(CMAKE_CXX_FLAGS &quot;${CMAKE_CXX_FLAGS} -DDEBUG_MODE&quot;)
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Problems with this approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build-time evaluation&lt;/strong&gt;: The condition is checked when CMake configures, not when you build&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global pollution&lt;/strong&gt;: Affects ALL targets in the project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-config generators&lt;/strong&gt;: Breaks with Visual Studio, Xcode which support multiple configs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Generator expressions solve this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Modern, correct approach
target_compile_definitions(MyTarget PRIVATE
    $&amp;#x3C;$&amp;#x3C;CONFIG:Debug&gt;:DEBUG_MODE&gt;
    $&amp;#x3C;$&amp;#x3C;CONFIG:Release&gt;:NDEBUG&gt;
    $&amp;#x3C;$&amp;#x3C;PLATFORM_ID:Windows&gt;:WIN32_LEAN_AND_MEAN&gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is evaluated &lt;strong&gt;per-target&lt;/strong&gt;, &lt;strong&gt;per-configuration&lt;/strong&gt;, &lt;strong&gt;at build time&lt;/strong&gt;. Much more flexible and robust.&lt;/p&gt;
&lt;h3&gt;Common Generator Expression Patterns&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Conditional compilation flags
target_compile_options(MyTarget PRIVATE
    $&amp;#x3C;$&amp;#x3C;CXX_COMPILER_ID:GNU,Clang&gt;:-Wall -Wextra&gt;
    $&amp;#x3C;$&amp;#x3C;CXX_COMPILER_ID:MSVC&gt;:/W4&gt;
)

# Debug vs Release libraries
target_link_libraries(MyTarget PRIVATE
    $&amp;#x3C;$&amp;#x3C;CONFIG:Debug&gt;:MyLib_d&gt;
    $&amp;#x3C;$&amp;#x3C;CONFIG:Release&gt;:MyLib&gt;
)

# Platform-specific linking
target_link_libraries(MyTarget PRIVATE
    $&amp;#x3C;$&amp;#x3C;PLATFORM_ID:Windows&gt;:ws2_32&gt;
    $&amp;#x3C;$&amp;#x3C;PLATFORM_ID:Linux&gt;:pthread&gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 6: Dependency Linking - Getting Scope Right&lt;/h2&gt;
&lt;p&gt;Here&apos;s a crucial concept: &lt;strong&gt;PUBLIC vs PRIVATE vs INTERFACE&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;if (${CMAKE_SYSTEM_NAME} MATCHES &quot;Emscripten&quot;)
    # Web build
    target_link_libraries(ColumbaEngine PUBLIC glm)
else()
    # Native build - note the scoping!
    target_link_libraries(ColumbaEngine PUBLIC
        SDL2::SDL2-static      # Users need SDL2 headers
        glm                    # Header-only math library
        OpenGL::GL            # Graphics API
    )

    target_link_libraries(ColumbaEngine PRIVATE
        libglew_static        # Internal OpenGL extension loading
        freetype              # Internal text rendering
    )
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Scope meanings:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PUBLIC&lt;/strong&gt;: &quot;I use this, and my users will need it too&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PRIVATE&lt;/strong&gt;: &quot;I use this internally, but my users don&apos;t need to know&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INTERFACE&lt;/strong&gt;: &quot;I don&apos;t use this, but my users will need it&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get this right, and users of your library automatically get the right dependencies. Get it wrong, and you&apos;ll have angry developers filing issues.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Understanding Transitive Dependencies&lt;/h3&gt;
&lt;p&gt;Let&apos;s look at a real example from our engine. Imagine this dependency chain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MyGame → ColumbaEngine → SDL2 → OpenGL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we declare SDL2 as &lt;code&gt;PRIVATE&lt;/code&gt; to ColumbaEngine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;target_link_libraries(ColumbaEngine PRIVATE SDL2::SDL2-static)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: &lt;code&gt;MyGame&lt;/code&gt; won&apos;t be able to use SDL2 types in the public API. If ColumbaEngine&apos;s headers include &lt;code&gt;&amp;#x3C;SDL2/SDL.h&gt;&lt;/code&gt;, users get compile errors.&lt;/p&gt;
&lt;p&gt;If we declare SDL2 as &lt;code&gt;PUBLIC&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;target_link_libraries(ColumbaEngine PUBLIC SDL2::SDL2-static)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: &lt;code&gt;MyGame&lt;/code&gt; automatically gets SDL2 headers and libraries. Perfect for our engine where users need SDL2 types.&lt;/p&gt;
&lt;h3&gt;The INTERFACE Scope Mystery&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;INTERFACE&lt;/code&gt; is the trickiest to understand. It means &quot;I don&apos;t use this, but my users will.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real-world example&lt;/strong&gt;: Header-only libraries with dependencies&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# HeaderOnlyMath library depends on Eigen, but is itself header-only
add_library(HeaderOnlyMath INTERFACE)
target_link_libraries(HeaderOnlyMath INTERFACE Eigen3::Eigen)

# Users of HeaderOnlyMath automatically get Eigen
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE HeaderOnlyMath)  # Gets Eigen too!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mixing Scopes: The Real World&lt;/h3&gt;
&lt;p&gt;Most real projects use mixed scopes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;target_link_libraries(ColumbaEngine
    # Users need these APIs
    PUBLIC
        SDL2::SDL2-static      # Window/input handling in public API
        glm                    # Math types in public headers
        OpenGL::GL            # Graphics API exposed to users

    # Internal implementation details
    PRIVATE
        libglew_static        # OpenGL extension loading (wrapped)
        freetype              # Text rendering (abstracted away)
        ${CMAKE_DL_LIBS}      # Dynamic library loading (platform detail)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 7: Multiple Executables and Examples&lt;/h2&gt;
&lt;p&gt;A game engine isn&apos;t just a library - it includes tools and examples:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;if(NOT BUILD_STATIC_LIB AND BUILD_EXAMPLES)
    # Main editor application
    add_executable(ColumbaEngineEditor src/main.cpp)
    target_sources(ColumbaEngineEditor PRIVATE
        src/application.cpp
        src/Editor/Gui/inspector.cpp
        src/Editor/Gui/projectmanager.cpp
    )
    target_link_libraries(ColumbaEngineEditor PRIVATE ColumbaEngine)

    # Game examples
    add_executable(GameOff examples/GameOff/main.cpp)
    target_sources(GameOff PRIVATE
        examples/GameOff/application.cpp
        examples/GameOff/character.cpp
        examples/GameOff/inventory.cpp
    )
    target_link_libraries(GameOff PRIVATE ColumbaEngine)
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pattern&lt;/strong&gt;: Create multiple executables that all link to your main library. This way, the library code is compiled once and reused.&lt;/p&gt;
&lt;h2&gt;Step 8: Testing Infrastructure&lt;/h2&gt;
&lt;p&gt;Professional projects need tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;if(NOT BUILD_STATIC_LIB)
    enable_testing()
    add_subdirectory(${GTEST_DIR})

    add_executable(t1 test/maintest.cc)
    target_sources(t1 PRIVATE
        test/collision2d.cc
        test/ecssystem.cc
        test/interpreter.cc
        test/renderer.cc
    )
    target_link_libraries(t1 PRIVATE gtest gtest_main ColumbaEngine)

    # Auto-discover tests
    include(GoogleTest)
    gtest_discover_tests(t1)
endif()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;gtest_discover_tests()&lt;/code&gt; automatically finds all test cases and creates individual CTest entries. Run with &lt;code&gt;ctest&lt;/code&gt; or &lt;code&gt;make test&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Common Build Pitfalls&lt;/h2&gt;
&lt;h3&gt;1. Global Variables vs Target Properties&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# BAD: Affects everything globally
set(CMAKE_CXX_FLAGS &quot;${CMAKE_CXX_FLAGS} -DDEBUG&quot;)

# GOOD: Only affects specific target
target_compile_definitions(MyTarget PRIVATE DEBUG)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Not Using Generator Expressions&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# BAD: Debug symbols in release builds
target_compile_options(MyTarget PRIVATE -g)

# GOOD: Only in debug builds
target_compile_options(MyTarget PRIVATE $&amp;#x3C;$&amp;#x3C;CONFIG:Debug&gt;:-g&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Wrong Dependency Scopes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# If users of your library need OpenGL headers:
target_link_libraries(MyLib PUBLIC OpenGL::GL)

# If only your implementation uses OpenGL:
target_link_libraries(MyLib PRIVATE OpenGL::GL)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What We&apos;ve Achieved: Build System Results&lt;/h2&gt;
&lt;p&gt;After implementing all these concepts, here&apos;s what we achieved:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Developer Experience:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Simple build
git clone https://github.com/user/ColumbaEngine
cd ColumbaEngine
mkdir build &amp;#x26;&amp;#x26; cd build
cmake ..
make -j8

# Custom configuration
cmake -DBUILD_EXAMPLES=OFF -DCMAKE_BUILD_TYPE=Release ..

# Run tests
ctest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cross-platform Support:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Same CMakeLists.txt works on Windows, Linux, and web&lt;/li&gt;
&lt;li&gt;Automatic dependency resolution per platform&lt;/li&gt;
&lt;li&gt;Platform-specific optimizations where needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Build Performance:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Precompiled headers cut build times by 60%&lt;/li&gt;
&lt;li&gt;Parallel compilation across all targets&lt;/li&gt;
&lt;li&gt;Incremental builds under 10 seconds for single-file changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Coming Up in Part 2&lt;/h2&gt;
&lt;p&gt;In the next article, we&apos;ll cover the &lt;strong&gt;deployment and distribution&lt;/strong&gt; side of CMake:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Installation Systems&lt;/strong&gt;: How to make your library usable by others with &lt;code&gt;find_package()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export/Import Mechanisms&lt;/strong&gt;: The complex but powerful system that makes modern CMake libraries work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package Configuration&lt;/strong&gt;: Creating relocatable, dependency-aware packages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPack Integration&lt;/strong&gt;: Generating professional installers for multiple platforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-world Distribution&lt;/strong&gt;: Getting your library into the hands of users&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Building a robust CMake build system for complex projects requires understanding several key concepts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Target-centric thinking&lt;/strong&gt; - Everything revolves around targets and their usage requirements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proper dependency management&lt;/strong&gt; - Choose the right strategy for your project&apos;s needs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform abstractions&lt;/strong&gt; - Let CMake handle platform differences&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generator expressions&lt;/strong&gt; - Enable conditional behavior without complex if/else logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build optimization&lt;/strong&gt; - Use precompiled headers and other modern features&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine&quot;&gt;game engine&lt;/a&gt; we&apos;ve built demonstrates these concepts in action with a real, production-ready build system that handles complex, multi-platform C++ projects.&lt;/p&gt;
&lt;p&gt;Remember: CMake is about expressing &lt;strong&gt;intent&lt;/strong&gt;, not &lt;strong&gt;implementation details&lt;/strong&gt;. Focus on what you want to achieve, and let CMake figure out how to do it on each platform.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Don&apos;t miss [Part 2] where we&apos;ll cover installation, packaging, and distribution - making your library actually usable by the world!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What&apos;s your biggest CMake build challenge?&lt;/strong&gt; Share your experiences in the comments below!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The complete source code for ColumbaEngine with all the CMake patterns from this series is available on &lt;a href=&quot;https://github.com/Gallasko/ColumbaEngine&quot;&gt;GitHub&lt;/a&gt;. These patterns scale from small libraries to large, complex applications.&lt;/em&gt;&lt;/p&gt;</content:encoded></item></channel></rss>