# Gamedev Shipping

> Use when exporting, packaging, signing, or publishing a game — export presets per engine, macOS notarization, cross-origin isolation for threaded web builds, Android AAB/keystore, iOS provisioning, store submission (Steam, Play, App Store, itch.io), headless build CI. NOT gameplay code inside the artifact (that is `godot`, `unity`, `unreal`), NOT hosting a web app or container (that is `deployment`).

- Skill: `ericrisco/gamedev-shipping` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add ericrisco/gamedev-shipping`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ericrisco/gamedev-shipping/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ericrisco (https://skillmd.com/u/ericrisco)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ericrisco/gamedev-shipping

---


# Ship the game — export, build & release (per engine · per platform)

Take a finished project from editor → signed, correctly-configured artifact → live on the
right target (desktop / web / mobile / store), with the platform gotchas that silently break a
build handled up front. This skill owns the *packaging and release boundary*; the engine skills
own the code that runs inside the artifact.

```text
project ──[engine export/build]──▶ artifact ──[per-platform sign/config]──▶ target
Godot templates·Unity Build Profiles·Unreal Shipping    desktop·web·mobile    Steam·Play·App Store·itch
```

## Version anchors (what "current" means here — July 2026)

| Tool | Target | Ship note |
| --- | --- | --- |
| Godot | 4.5 stable (4.6 dev) | Export templates must match the editor build **exactly** (4.5.1 editor ⇒ 4.5.1 templates). **C#/.NET edition has NO web export** — GDScript/standard only. |
| Unity | Unity 6 (6000.x) | "Build Settings" is now **Build Profiles**. IL2CPP backend for iOS/Android/WebGL. |
| Unreal | 5.6 | Ship in the **Shipping** config; test packaged builds in **Development**. |
| Android | JDK **17**, target a recent API level | AAB required for Google Play (new apps + updates). APK only for sideload/itch/direct. |
| iOS/macOS | Xcode 16+, `notarytool` | `altool` notarization is dead — use `xcrun notarytool`. |

## When NOT to use — and who owns it instead

| Ask | Owner | Why the line is there |
| --- | --- | --- |
| Gameplay, shaders, netcode, physics — any runtime logic | `../godot/SKILL.md`, `../unity/SKILL.md`, `../unreal/SKILL.md`, `../gamedev-shaders/SKILL.md`, `../gamedev-multiplayer/SKILL.md`, `../gamedev-physics/SKILL.md` | This skill packages code, it does not write it. The boundary is the export button. |
| Hosting a normal web app, API or container (Dockerfile, Coolify, Vercel, VPS) | `../deployment/SKILL.md` | A WebAssembly game is *static files*; only its **headers** are special (below). Borrow its Actions + header-setting patterns. |
| Game feel, economy, narrative | `../game-design/SKILL.md`, `../game-storytelling/SKILL.md` | Design intent, not packaging. |
| Signing-key and CI secret handling in general | `../secure-coding/SKILL.md` | The practice this skill assumes for keystores/certs. |

## Decision rules (settle these before touching a build)

| Question | Rule |
| --- | --- |
| Web build uses threads / SharedArrayBuffer? | You **must** serve `COOP: same-origin` + `COEP: require-corp` (cross-origin isolation). No headers ⇒ no threads ⇒ boot failure. |
| Godot + C# + web? | **Impossible today.** Switch to GDScript for web, or ship that title desktop/mobile only. |
| Play Store? | **AAB**, signed with an upload key, Play App Signing on. Not APK. |
| macOS distribution outside the App Store? | codesign (Developer ID) → **notarize** → **staple**. Skipping = Gatekeeper blocks it. |
| Mac **App Store**? | Different cert (Apple Distribution) + sandbox entitlements; notarization is automatic, not manual. |
| Steam? | SteamPipe upload via `steamcmd` + an `app_build` VDF; set the default branch live in the partner site. |
| Quick public build / demo / jam? | **itch.io** + `butler push`. Tick "SharedArrayBuffer support" for threaded web builds. |
| Release config? | Godot **release** preset (debug off), Unity release/**IL2CPP**, Unreal **Shipping**. Never ship a debug/Development build. |

## Export / build per engine

**Godot** — `Editor ▸ Manage Export Templates` (match version), then `Project ▸ Export`, add one
preset per platform. Headless for CI:

```bash
godot --headless --export-release "Windows Desktop" build/game.exe
godot --headless --export-release "Web"             build/index.html
# Android AAB needs the Gradle build template + JDK 17 + a release keystore (see references)
```

Web preset toggles: **Thread Support** (⇒ needs COOP/COEP) and **Extensions Support**. Android
needs `Project ▸ Install Android Build Template` (enables *Use Gradle Build*).

**Unity** — `File ▸ Build Profiles` (formerly Build Settings). Create a profile per platform;
override **Player Settings** per profile (company/product name, bundle id, icons, scripting
backend). iOS/Android/WebGL force **IL2CPP**. CI headless:

```bash
Unity -quit -batchmode -nographics -projectPath . \
  -buildTarget Android -executeMethod BuildScript.PerformBuild -logFile -
```

Android: tick **Build App Bundle (Google Play)** for AAB; set the keystore in *Publishing Settings*.
WebGL: threads ⇒ COOP/COEP; keep **decompression fallback** for hosts that can't set gzip/br headers.

**Unreal** — build **configurations**, not "modes": Debug, DebugGame, **Development** (test packaged
builds — keeps logs/console), Test, **Shipping** (release — strips logging, most optimized). Package
via `Platforms ▸ <target> ▸ Package Project`, the Project Launcher, or UAT for CI:

```bash
RunUAT BuildCookRun -project="Game.uproject" -platform=Win64 \
  -clientconfig=Shipping -cook -stage -pak -package -archivedirectory=build
```

Android adds `-distribution` for an AAB; iOS requires a Mac + provisioning. Run the platform
**Turnkey**/SDK setup once (Android Studio + NDK, or Xcode) or the cook fails.

→ full per-engine preset/flag tables: [references/mobile-and-desktop.md](references/mobile-and-desktop.md)
and [references/stores-and-ci.md](references/stores-and-ci.md).

## Per-platform

### Desktop (Windows / macOS / Linux) — the simplest path

| OS | Sign? | Gotcha |
| --- | --- | --- |
| Windows | Optional (Authenticode; EV avoids SmartScreen warm-up) | Unsigned still runs but SmartScreen warns; ship `.exe` + engine data next to it, or an installer. |
| Linux | No | Ship binary + data, or wrap as **AppImage**/**Flatpak** for a portable single file. |
| macOS | **Required** | Must codesign (Developer ID Application) **+ notarize + staple**, or Gatekeeper refuses to open it. |

macOS notarization (the step everyone forgets):

```bash
codesign --deep --force --timestamp --options runtime \
  --sign "Developer ID Application: Studio (TEAMID)" Game.app
ditto -c -k --keepParent Game.app Game.zip
xcrun notarytool submit Game.zip --keychain-profile "AC" --wait   # store creds once via store-credentials
xcrun stapler staple Game.app                                     # staple so it verifies offline
```

`--options runtime` (Hardened Runtime) is **mandatory** for notarization. Codesign every bundled
`.dylib`/helper *before* the outer `.app`, inside-out.

### Web — the header trap (read this before blaming your build)

Threaded WebAssembly builds (Godot with Thread Support, Unity WebGL with threads) use
**`SharedArrayBuffer`**, which browsers gate behind **cross-origin isolation**. The server must send:

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

Missing them ⇒ `SharedArrayBuffer is not defined`, a hang, or a blank canvas — even though the
files are fine. Where to set them:

- **Netlify** `_headers`, **Vercel** `vercel.json` headers, **nginx** `add_header` — set both on the game path.
- **itch.io** — tick **"This file will be played in the browser" ▸ SharedArrayBuffer support**; itch sets the headers for you.
- **GitHub Pages** — **cannot** set headers. Use a non-threaded export, or the `coi-serviceworker` shim (a service worker that re-serves with the headers).

`COEP: require-corp` also means every cross-origin subresource (fonts, analytics, embeds) must
send CORP/CORS or it's blocked. **Godot C#/.NET has no web export at all** — use GDScript.

→ per-host configs, the service-worker shim, COEP subresource fallout, a self-test snippet and the
Godot vs Unity web specifics: [references/web-export-headers.md](references/web-export-headers.md).

### Mobile

**Android** — needs **JDK 17** and the engine's Gradle build template. Ship **AAB** to Play
(Google re-signs and generates per-device APKs), **APK** for sideload/itch/direct. Generate a
release keystore once and reuse it forever (lose it = you can't update the app without Play App
Signing key reset):

```bash
keytool -genkey -v -keystore release.keystore -alias game \
  -keyalg RSA -keysize 2048 -validity 10000
```

Keep the keystore + passwords out of git (CI secret). `versionCode` must increase every upload.

**iOS** — Mac + Xcode + Apple Developer account. Bundle ID must match an **App ID**; pick a
**provisioning profile** (development / ad-hoc / App Store) and a signing certificate. Archive,
then upload to App Store Connect / TestFlight via Xcode Organizer or **Transporter**. `CFBundleVersion`
must increase per upload.

→ full keystore + Play App Signing, provisioning/TestFlight, Windows Authenticode and Linux
AppImage/Flatpak walkthroughs: [references/mobile-and-desktop.md](references/mobile-and-desktop.md).

## Store prep

- **Assets**: platform icons (Android adaptive, iOS icon set, Windows `.ico`, macOS `.icns`), splash/launch, store screenshots per device class, a trailer where required.
- **Metadata**: title, short + full description, keywords, category, support/privacy URLs, localized strings.
- **Age ratings**: fill the **IARC** questionnaire (Play, Microsoft, Nintendo) or ESRB/PEGI/USK; Apple has its own age-rating questionnaire. Wrong answers = removal.
- **Google Play**: AAB + **Play App Signing**, a recent **target API level**, content rating, and the **Data safety** form.
- **Steam**: Steamworks partner + appid; define **depots**; upload with **SteamPipe** (`steamcmd +run_app_build app_build_APPID.vdf`); store page assets; set the branch live.
- **itch.io**: `butler push build/ user/game:channel --userversion 1.2.3` — channel name (e.g. `windows`, `html5`) picks the platform.
- **Mac App Store**: Apple Distribution cert + App Sandbox entitlements (not Developer ID); notarization is handled by App Store submission.

→ Steamworks/SteamPipe VDF, butler channels, Play/App Store submission checklists:
[references/stores-and-ci.md](references/stores-and-ci.md).

## CI/CD for builds

Headless export in GitHub Actions; version from the run/commit so every artifact is traceable.

- **Godot**: run in a container that bundles matching export templates (e.g. a `barichello/godot-ci`-style image), `godot --headless --export-release`.
- **Unity**: `game-ci/unity-builder` + a license activation step (secrets); matrix over targets.
- **Unreal**: needs a **self-hosted runner** with the engine installed (multi-GB); UAT `BuildCookRun`.
- **Signing in CI**: import the keystore/cert from an encrypted secret at runtime, never commit it. macOS notarization runs on a `macos-latest` runner with `notarytool` + an App Store Connect API key.
- **Versioning**: semver tag for releases; a monotonic build number from `github.run_number` or the git commit count feeds `versionCode`/`CFBundleVersion`/Steam build description.

→ ready per-engine workflows with signing: [references/stores-and-ci.md](references/stores-and-ci.md).

## Ship-time optimization

- **Texture compression per platform**: ASTC (mobile), BCn/DXT (desktop), ETC2 (GLES fallback). Set per-platform import overrides — never ship uncompressed PNGs to mobile.
- **Strip the fat**: Unity **Managed Stripping Level: High** + IL2CPP; Unreal **Shipping** drops logs/console; Godot — export only used resources and disable unused modules in a custom template.
- **Startup**: small boot scene, stream/preload heavy assets, compress the `.pck`/data, use Ogg Vorbis over WAV. Web: enable gzip/brotli on `.wasm`/`.pck`.
- **Budgets**: set a per-platform size budget (web especially — every MB is download latency) and check the final artifact against it before submitting.

## Anti-patterns

| Anti-pattern | Do instead |
| --- | --- |
| Web build works locally, host it later | Set COOP/COEP now; `file://` and simple servers hide the `SharedArrayBuffer` failure. |
| Planning a Godot C# web export | It doesn't exist in 4.x. GDScript for web, or no web. |
| Treating notarization as optional | On macOS, no notarize + staple = Gatekeeper blocks the app for every user. |
| Shipping an APK to the Play Store | Play requires **AAB** for new apps and updates. |
| Committing the keystore so CI can sign | Keystore/cert live in encrypted CI secrets, never in git. |
| Shipping the Development/Debug config | Ship Godot release / Unity IL2CPP release / Unreal **Shipping** — debug leaks console + is slow. |
| Export templates "close enough" (4.5 vs 4.5.1) | Godot templates must match the editor build exactly or export breaks. |
| Uncompressed textures | Per-platform compression (ASTC/BCn/ETC2) — raw textures blow memory + size. |
| Reusing an old versionCode/CFBundleVersion | Both must strictly increase or the store rejects the upload. |

## Checklist

- [ ] Release/Shipping config, not Debug/Development.
- [ ] (Godot) export templates match the editor version exactly.
- [ ] (Web, threaded) COOP `same-origin` + COEP `require-corp` served; boots without `SharedArrayBuffer` errors.
- [ ] (Godot + web) confirmed GDScript, not C#/.NET.
- [ ] (macOS) codesigned (Hardened Runtime) + notarized + stapled.
- [ ] (Android) AAB for Play, JDK 17, release keystore in CI secrets, `versionCode` bumped.
- [ ] (iOS) provisioning profile + signing cert correct, `CFBundleVersion` bumped.
- [ ] Store assets: icons, splash, screenshots, metadata, age rating done.
- [ ] Version/build number set from CI and traceable.
- [ ] Textures compressed per platform; build size within budget.

