Skip to main content

Platform-Specific Code: Seams First, Honest Guards Second

Cocos2d-Mono multi-targets DesktopGL (Windows/Linux/macOS), WindowsDX, Android, and iOS from one codebase, and the engine's platform story keeps growing. Two rules keep platform-specific code from rotting.

Rule 1: Prefer a runtime seam over #if

Where the engine offers a pluggable seam, use it instead of conditional compilation — a seam is testable, works per-instance, and doesn't multiply build flavors. The model example is storage: rather than #if-ing file APIs per platform, CCUserDefault.Storage accepts any ICCUserDefaultStorage implementation.

The same shape applies to your own game code: put the platform difference behind an interface, select the implementation once at startup.

Rule 2: When you do #if, guard on what you mean

State the platforms a branch is for — never the platforms it isn't:

// ✅ Says what it means: this is the mobile-sensor path.
#if ANDROID || IOS
ReadAccelerometer();
#endif

// ❌ "Everything that isn't desktop" - silently captures every FUTURE platform too.
#if !WINDOWSGL && !WINDOWS && !MACOS && !LINUX
ReadAccelerometer();
#endif

The exclusion style is not hypothetical: an accelerometer guard written as "not desktop" silently mis-slotted a new platform during the engine's console bring-up — the code compiled, the intent was wrong. Every new platform an exclusion list has never heard of lands in its else bucket by default; an inclusion list fails safe (the new platform gets nothing until someone decides).

The constants

The build defines these per target (see the per-TFM blocks in the engine csproj):

ConstantWhere
DESKTOPGL, MONOGAME, OPENGLthe DesktopGL build (all three desktop OSes)
WINDOWSGL / MACOS / LINUXadditionally, per desktop host OS
WINDOWS, XNAthe WindowsDX build
ANDROID, GLESAndroid
IOS, __IOS__, GLESiOS

Guard on the smallest set that expresses your intent — usually one platform constant or one capability pair (ANDROID || IOS).