Skip to main content

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 runtimeHow
Downloadbalaur 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 handdrop 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 app builds a .app with 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.bpak beside the executable in an .app, assets/game.bpak in an APK.
  • balaur export signs with the project's certificate or keystore. iOS: --sign <identity> and --provisioning-profile <file>, plus --bundle ipa for App Store Connect.
  • Android: --bundle apk assembles and signs with the keystore [android] keystore names, 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.html in 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.plist from that table, plus a <game>.entitlements holding one key per capability.
  • It refuses a capability with no bundle_id, an icloud-kv with no team (nothing expands $(TeamIdentifierPrefix) outside Xcode), or a deployment target below what a capability's API needs.
  • A macOS .app is signed on export, entitlements included; --sign is 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.3 tracks the alpha line, a plain version tracks stable, and a nightly follows the rolling nightly. A source build refuses and points at git.
  • --check only reports. --channel alpha crosses to another line, --tag picks one exact release, and --allow-downgrade takes a step backwards.

Save games​

[save]
version = 3 # what this build writes
migrate_script = "scripts/saves.rn" # brings an older file forward
CallWhat 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 calls migrate_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.