Engineering · July 6, 2026
Cross-Platform Builds for a 2D Shmup: One Codebase Across Windows, macOS, and Linux
A shmup that only ever ran on the developer's own Windows machine will find dozens of small assumptions baked into it the first time it's built for macOS or Linux — most of them about file paths, case sensitivity, and which libraries are actually guaranteed to exist.
Published July 6, 2026
The most common source of cross-platform breakage has nothing to do with graphics or gameplay code — it's file paths. Windows treats paths case-insensitively and accepts both forward and back slashes; Linux is strictly case-sensitive and only accepts forward slashes; macOS sits in between depending on the filesystem format. A project that runs `Assets/Sprites/Ship.png` on Windows and references it as `assets/sprites/ship.png` somewhere else in code will build and run fine on Windows for the entire development cycle and then fail to load a single texture the first time it's built on Linux. Standardizing on lowercase, forward-slash paths from day one, even on a Windows dev machine, removes this bug class entirely rather than catching it late.
Don't assume a library exists just because it does on your machine
Audio backends, controller input libraries, and windowing systems all have platform-specific implementations under a common API, and it's easy to accidentally depend on a codec, driver, or system library that happens to be installed on the developer's machine but isn't guaranteed on a clean install elsewhere. This is especially common with audio format support — an MP3 decoder that's present system-wide on one platform may need to be bundled explicitly on another. The safest approach for a small team is to bundle a single cross-platform library (SDL2 is the default choice for a reason) for windowing, input, and audio rather than hand-rolling platform-specific code paths for each target, and to test that the bundled dependencies actually ship inside the build output rather than being picked up from the dev machine's system libraries.
Input devices report differently per platform
The same physical gamepad can report a different button order, different axis ranges, or even a different vendor ID string depending on the operating system's driver stack. A control scheme validated only on Windows with an Xbox controller is not validated for a Linux user with the same controller plugged into a different kernel driver. This overlaps heavily with the abstraction work covered in cross-platform gamepad input, and the two problems should be solved together rather than treating gamepad abstraction and OS portability as separate projects — a controller mapping table that's hardcoded to one platform's button ordering will silently misfire the moment the same build runs somewhere else.
Packaging and distribution differ more than the code does
Once the game itself runs cross-platform, packaging it for distribution is its own separate problem per platform. Windows wants a self-contained folder or installer; macOS wants a signed and notarized `.app` bundle, and skipping notarization means Gatekeeper blocks the game outright on a clean machine with a scary-looking warning that kills a meaningful fraction of first launches; Linux distribution is fragmented enough that most small teams ship either a self-contained AppImage or rely entirely on a platform like Steam's Linux runtime to handle library compatibility rather than targeting every distribution's package manager directly. Budget real time for this stage — it is routinely underestimated, and macOS notarization in particular involves a submission and review step that isn't instantaneous.
Text and file encoding differences bite quietly
Line-ending conventions differ between Windows and Unix-like systems, and a config file, save file, or localization string table generated on one platform and read on another can fail to parse correctly if the code doing the reading assumes a specific line-ending format rather than handling both. Similarly, a source file saved with an unusual text encoding on one developer's machine can render correctly for that developer and produce garbled or missing characters for a teammate on a different platform with different default encoding assumptions. Standardizing explicitly on UTF-8 for all text assets and Unix-style line endings in version control, rather than leaving it to each platform's default, removes an entire class of "it works on my machine" bugs that otherwise only show up once a build actually runs somewhere else.
Continuous testing beats a final cross-platform pass
Waiting until near the end of development to try a build on a second platform guarantees a large backlog of platform-specific bugs discovered all at once, right when there's the least time to fix them. A build pipeline that produces at least a smoke-tested build on every target platform on a regular cadence — even just weekly, even without full manual testing of every build — catches path and dependency issues within days of the change that introduced them instead of months later. This same infrastructure investment pays off again when it comes time to think about porting a shmup to the web, since the discipline of not hardcoding platform assumptions is the same skill in both directions.
None of this requires exotic tooling. A small team can get most of the benefit from a single continuous-integration job that builds all three desktop targets on every commit to the main branch and fails loudly the moment a platform-specific assumption creeps back in.