Shipping a game
Packs
balaur export my-game # writes my-game.bpak
balaur play my-game.bpak # run from bytecode only
- Every script is compiled to bytecode with dev mode's compiler configuration and bundled with the scenes and the manifest.
- A packed run has no compiler, no file watcher and no script sources.
- A pack is written in sorted key order. The same sources give the same bytes on any machine.
- TOML assets, animation clips and every other registered type resolve by the same references as a dev run.
- Binary assets are in the same pack, each verified against its SHA-256 on load.
- Packed execution interprets precompiled bytecode, with no JIT. CI cross-compiles to iOS, Android and WebAssembly on every push.
Splash Screen
A game's first frames have nothing to draw yet. [application] splash is the
picture that goes over them, on every target, a browser tab included. It is
project-relative, fits the screen and keeps its aspect.
[application]
splash = "art/logo.png"
splash_seconds = 1.5
A script that reports a load holds the splash past its seconds, and draws a bar under the picture. One that reports nothing leaves at its seconds.
ui::set_load_progress(0.4, "textures");
ui::finish_loading();
The seconds are engine time, so a recorded session replays the splash for as long as the run showed it.
Fused executables
balaur export my-game --target linux-x64 # or macos-universal, windows-x64
- Fusing appends the pack and a trailer to a runtime, the engine binary published per platform. ELF, Mach-O and PE ignore trailing bytes.
- On startup the engine reads its own executable: a pack inside means a shipped game, none means the CLI. One binary is the editor, the CLI and every game's runtime.
- The host platform's runtime ships with the editor download, in
runtimes/.
| Getting another platform's runtime | How |
|---|---|
| Download | balaur export offers to fetch it from the engine's release tag, verified against SHA256SUMS, into a per-user cache (<data dir>/balaur/runtimes/<version>) |
| Without a prompt | --download, or --no-download to forbid fetching (CI) |
| By hand | drop a balaur-runtime-* binary into runtimes/, point BALAUR_RUNTIMES at a directory, or pass --runtime <file> |
A runtime is pinned to the compiler version that wrote the pack.
- A flat fused binary cannot be code-signed. For a signed macOS game,
balaur export my-game --target macos-universal --bundle appbuilds a.appwith the pack as a sealed resource and signs it ad-hoc.--sign "Developer ID Application: …"uses a real identity. - The exporter sets the execute bits on its output.
Mobile
balaur export my-game --target ios # -> my-game.app
balaur export my-game --target android # -> my-game-android/ (an APK layout)
- The pack is inside the bundle:
game.bpakbeside the executable in an.app,assets/game.bpakin an APK. balaur exportsigns with the project's certificate or keystore. iOS:--sign <identity>and--provisioning-profile <file>, plus--bundle ipafor App Store Connect.- Android:
--bundle apkassembles and signs with the keystore[android] keystorenames, or Android's debug identity when none is named. - Without those flags the build is unsigned and installs on no device.
Web
balaur export my-game --target web # -> my-game-web/
- A directory for a static host: the wasm-bindgen glue, the engine module, the pack, and a page that loads them.
- A
web/index.htmlin the project replaces the page. - The module is the same for every game and most of the download. See Build size.
Apple
An .app is written from the [apple] table.
[apple]
bundle_id = "com.studio.game" # the identifier registered in App Store Connect
team_id = "AB12CD34EF" # the ten-character team identifier
version = "1.2" # CFBundleShortVersionString
build_number = "7" # CFBundleVersion
min_ios = "15.0" # iOS; min_macos for the Mac
capabilities = ["game-center", "icloud-kv", "in-app-purchase"]
[apple.plist] # anything else the bundle needs, as written
ITSAppUsesNonExemptEncryption = false
codesign --entitlements my-game.entitlements --sign "Apple Distribution: …" my-game.app
- The exporter writes the whole
Info.plistfrom that table, plus a<game>.entitlementsholding one key per capability. - It refuses a capability with no
bundle_id, anicloud-kvwith noteam(nothing expands$(TeamIdentifierPrefix)outside Xcode), or a deployment target below what a capability's API needs. - A macOS
.appis signed on export, entitlements included;--signis the whole step. - The capabilities are in Stores. Running this on a runner is in Continuous integration.
Updates
balaur update replaces the whole install as a set: the binary, the bundled
editor and its runtime.
- Verified against the release's checksums.
- An update follows the channel its own version names:
0.2.0-alpha.3tracks the alpha line, a plain version tracks stable, and a nightly follows the rolling nightly. A source build refuses and points at git. --checkonly reports.--channel alphacrosses to another line,--tagpicks one exact release, and--allow-downgradetakes a step backwards.
Save games
[save]
version = 3 # what this build writes
migrate_script = "scripts/saves.rn" # brings an older file forward
| Call | What it does |
|---|---|
save::write(slot, data) | stores a table per user, under the platform's user-data directory |
save::read(slot) | brings it back |
save::slots() | lists them |
save::remove(slot) | deletes one |
- A slot name is letters, digits,
-and_, and is validated. - A file is written beside its target and renamed over it.
- Every file is stamped with the build's
version. Reading an older one callsmigrate_save(version, data)once per step, 1 to 2 then 2 to 3. - A file written by a newer build is refused.
- Other per-user data (settings, key rebindings) goes through
engine::user_data_directory().
Embedding
fn main() -> anyhow::Result<()> {
balaur::boot_pack(include_bytes!("game.bpak"))
}
Embedding needs a Rust toolchain. Fused export does not.