Game Services

Let players earn achievements and post scores while keeping your game playable offline. iOS uses Game Center; Android uses Google Play Games Services v2. Desktop and web builds compile the same source and report an unsupported provider.

Try Target Sprint

Open TargetSprint.bb. Hit five gold targets, then start another round. A configured mobile export can connect, unlock first_win, submit best_score and show the native screens. The service buttons are disabled when their prerequisites are missing.

Configure your app

In the macOS IDE, choose Program > Create Executable, select a mobile platform and open App Setup. Enter your app name, application ID, version and build number. Games and purchases have independent switches. More Mobile Services configures the optional cloud and device features. Edit Service IDs holds the optional achievement, leaderboard and product mappings. Saving creates game.b3app.json beside your program and selects it for export; disabling a service preserves its advanced settings.

For command-line builds, pass a version-1 game.b3app.json to the compiler with --app-config game.b3app.json. Use your real application ID and IDs from the platform consoles. Enable services.games, enable apple.gameCenter for iOS and set android.gamesProjectId for Android. Map achievements and leaderboards to their platform IDs. Configuring an ID does not create that console resource.

{
  "schemaVersion": 1,
  "application": {"id": "com.example.targetsprint", "name": "Target Sprint", "version": "1.0", "build": 1},
  "services": {"games": true, "purchases": false},
  "apple": {"gameCenter": true},
  "android": {"gamesProjectId": "1234567890"},
  "achievements": {"first_win": {"apple": "first_win", "android": "your_achievement_id", "total": 1}},
  "leaderboards": {"best_score": {"apple": "best_score", "android": "your_leaderboard_id"}}
}

Configure tester access and the certificates used to sign the game in the appropriate console. iOS device exports need an authorized Game Center provisioning profile. Android service exports use the dependency-aware Gradle packaging path. Store-account and physical-device qualification are required before distributing your game. Keep credentials and signing keys outside the assets directory.

Requests and events

Call InitGameServices() once near startup. A positive return value is a request ID, not a successful sign-in. Poll PollGameServiceEvent() in your loop, inspect its request, kind and status, then free it. GameServiceEventData returns an independent JSON copy which you free with FreeJson. Requests time out after 120 seconds. Freeing an event never changes a store transaction.

The shared bound is 128 event/request slots, with one reserved for account changes; provider payloads are at most 64 KiB. Excess new requests return zero with a diagnostic. A duplicate callback cannot complete a request twice, and callbacks after shutdown are ignored. Polling works without a render loop. All service commands are prohibited in Worker Functions.

Player changes and scores

An account-change event has kind 8 and request 0. Update the displayed player and discard any assumption about their previous account. Outstanding score and achievement work reports account-changed instead of being replayed under another player. Platform IDs are not game accounts and do not establish purchase ownership.

Achievement progress is absolute and monotonic, with a total matching your console catalog. Google standard achievements use UnlockAchievement; incremental achievements use SetAchievementProgress. Scores are signed 64-bit Long values. Their event JSON uses decimal Strings so external JSON consumers do not round them. The initial native board view uses global/all-time scope; Apple and Google do not share leaderboard entries.

Purchases and the server ledger

Enable services.purchases independently of Games, configure a public HTTPS backend.endpoint and map permanent/consumable/subscription products to their Apple and Android store IDs. Deploy the reference service in services/game-services with the same application identity, environment and server-authoritative product catalog. Keep its credentials on the server. Native guest credentials remain in Keychain or Keystore; commands never expose them.

Purchase recovery starts with the mobile runtime, even before a shop opens. Call InitPurchases() to retry initialization, then QueryPurchaseProducts(). Display the returned localized title and offer price. Only call PurchaseProduct(key,offer) after the player selects that exact offer. A pending result does not establish ownership.

The backend verifies store evidence and commits the grant and duplicate marker together. Apple transactions finish only after that confirmation; the backend acknowledges or consumes Google purchases after committing delivery. The durable client journal stores recovery metadata, not receipt tokens. Store requery and the ledger recover transactions after interruptions. Event snapshots and request timeouts never finish or cancel a financial transaction.

RefreshPurchaseEntitlements() returns authoritative balances and permanent ownership. SpendPurchaseCredits(entitlement,quantity,operationId) spends online using an ID the game saves before calling. Keep that ID through retries. Apply the returned receipt's effect and its processed ID in the same atomic game save. The sample demonstrates this with saved wide-target rounds. RestorePurchases() is a player action; ReconcilePurchases() retries recovery.

The guest account is bound to its secure credential, not a Game Center or PGS profile. A lost guest credential cannot silently claim another guest's purchases. Store restore does not recreate spent consumable balances. A game promising recovery after reinstall or on another device must provide its own verified account-recovery policy. Offline balance spending is not supported.

Purchase event data

KindPayload (all include schemaVersion 1)
20 initialize; 25 inventory; 28 inventory changed{balances:{coins:100},entitlements:{extra_level:true}}
21 catalog{products:[{key,id,type,title,description,price,offers:[{id,price}]}]}
22 payment{product,state:"pending"} awaits store approval; {product,state:"verification_pending"} awaits backend verification. Neither is a grant.
23 restore; 27 reconciliationCompletion describes the recovery request; transactions may continue separately.
24 transaction{transactionId,product,state,balances,entitlements}; fulfilled/completed success or revoked/denied. A locally unverified transaction has status 8 and only {product,state:"unverified"}.
26 spending{key,entitlement,spent,balance}; key is the caller's persisted operation ID.

Unsolicited transaction, inventory and recovered-spend events have request 0. Always inspect kind and status; use authoritative inventory on revocation as well as success. Pending transactions remain recoverable when the public event queue is full. Native callbacks never execute Blitz code.

Distribution

Open Distribution Options in the mobile export screen to choose an Android release APK/AAB or an iOS archive/local IPA. The form asks for the required signing files, artwork and privacy declarations. Development mode preserves inactive release settings. Android keystore passwords come from B3D_ANDROID_STORE_PASSWORD and B3D_ANDROID_KEY_PASSWORD in the IDE's environment; they are never saved in app setup.

Android purchase/service exports use pinned Gradle dependencies and support signed AABs. Test an installation from a Play track, including the Play App Signing certificate. On Apple, use an App Store distribution profile and the compiler's --ios-archive options with a 1024px opaque store icon and an authored privacy manifest. Optional Xcode export produces a local IPA and matching debug symbols; it never uploads. Console configuration, disclosures and physical store qualification remain the game's release responsibilities.

Cloud saves, subscriptions and optional device integrations are described below. Their native flows still need qualification with your configured devices and store test accounts.

Game Services command reference ยท In-App Purchases command reference

Subscriptions and store management

Use product type subscription with the same authoritative backend. The catalog's exact offer includes localized price phases, ISO billing periods, recurrence, and any commitment payments. Show every phase before accepting payment. Pass precisely that selected offer to PurchaseProduct or ChangeSubscription; the provider never substitutes a paid offer. Apple supports ordinary and eligible introductory offers; unsigned promotional discounts are not advertised.

Inventory includes subscriptions with state, entitled, expires, graceExpires, renews, purchaseId, lineageId, renewalId and cacheUntil. Active, cancelled-but-entitled and valid grace periods can grant access. Expired, retry, hold, paused, pending and revoked states do not. Cancellation does not remove a paid period early. The client refreshes in the background and expires cached access using a monotonic deadline, at most five minutes after the last verified server observation and never beyond the paid/grace deadline. Reinstall/restart does not restore an indefinite subscription grant. Immutable old event snapshots are historical; keep current inventory, consume expiry events, and never persist entitled=true as permanent access.

The server tracks Apple original/renewal transaction identities and Google purchase-token lineage separately from renewal order/period identity. Replayed notifications and replaced Google tokens cannot grant another renewal. This reference policy grants time-limited access, not currency per renewal. Family-shared Apple transactions are explicitly denied because the guest account model cannot identify the recipient safely. ManageSubscriptions opens native management. RequestPurchaseRefund uses an owned backend purchase ID; Apple presents its refund sheet and Google opens official refund guidance. A UI result never changes ownership without authoritative verification.

Cloud saves and explicit conflicts

Enable services.cloudSaves. Apple requires apple.cloudProvider="gamekit", exactly one apple.iCloudContainers entry, Game Center and an iCloud Drive capable profile. This explicit provider works on the iOS 17 baseline; newer GameSave providers are not silently substituted. Android needs the Play Games project ID and Saved Games enabled in its console. Games and cloud flags are independent, but both use the platform authentication service.

Import blitz.saves for named local slots, recovery generations, migrations and tombstones. The separate blitz.savecloud adapter exports a validated local record as JSON values rather than wrapping an escaped disk envelope again. Give the local store a persistent path under SystemProperty$("datadir"). Local save limits remain unchanged. The cloud adapter adds schema, slot, logical revision, parent revision, writer identity, origin account and deletion metadata, then measures the complete UTF-8 record against 1 MiB. Oversized records return status 14 and remain saved locally.

InitCloudSaves returns the current provider account and whether adoption is needed. Show the player a choice before AdoptCloudSaveAccount, including on first use. Changing cloud accounts blocks writes until they choose again. On Apple, the local binding also detects iCloud account changes; its opaque identity is not assumed to serialize identically on a second device. Downloaded records retain their origin account as metadata. Native provider scope and current-account rechecks enforce where reads and writes occur.

LoadCloudSave returns a revision and private recordPath, or status 13 with a conflict token and candidate paths. Inspect candidates using Saves.ReadCloudRecord, then explicitly import or merge. Saves.ImportCloudRecord creates a new local generation; it does not replace the local store by copying cloud bytes. Deletions travel as tombstones. After inspecting all versions, ResolveCloudSaveConflict submits the chosen record and rechecks the competing revisions. Copies are retained through failure and can be released explicitly after resolution. A runtime restart invalidates conflict handles; reload the slot.

Expected revisions detect conflicts; neither provider promises universal atomic compare-and-swap. GameKit fetches and checks before saving, with native conflict resolution for divergent versions. Play Games uses manual Snapshot conflicts, never a latest-timestamp winner. Both SDKs can cache and synchronize later: successful writes mean accepted by the provider, with confirmedSynced=false. Cross-platform Apple/Google progress needs a separate account and backend provider. Two-device offline divergence remains a required device qualification test.

Haptics, sharing and reviews

In App Setup > More Mobile Services, enable only the services the game uses. Try Tap Garden: complete five-flower rounds offline, toggle phone haptics, share a selected screenshot, schedule or cancel a reminder, and optionally request a review after three rounds. Disabled controls need a configured native export. These features do not require a purchase account.

PlayGameHaptic accepts light, medium, heavy, selection, success and failure. SetGameHapticsEnabled persists a native player preference; system/hardware restrictions still apply. Controller rumble stays separate. ShareGameContent takes only selected text, an HTTPS URL and/or a PNG/JPEG path. Generate images with SaveBuffer in private writable storage. The native adapter copies selected images to temporary storage, retains them for the share operation and scopes Android content-URI read grants. A share-sheet result cannot prove a post was published. RequestGameReview is contextual and best effort: a store may suppress its prompt, and the game cannot discover whether a rating was submitted.

Local reminders, verified links and device signals

Enable services.notifications. Query LocalNotificationPermission(); pass 1 only after the player chooses to enable reminders. ScheduleLocalNotification accepts a stable ID, title, body, channel and either a relative delay or calendar components with an explicit time zone (local follows the current zone). The default configured channel is reminders. List and cancel reminders by ID. Scheduling is bounded and does not request exact-alarm access. Android restores inexact schedules after reboot, package replacement and clock/time-zone changes. Delivery may be delayed or disabled by system settings. Open events are kind 60; they authorize no reward. APNs/FCM remote push remains a separate phase.

Enable services.links and list exact HTTPS host/path-prefix entries in linkRoutes. Apple also needs matching applinks:host associated domains, the signed entitlement and your server's apple-app-site-association file. Android emits autoVerify App Link filters; publish assetlinks.json for the actual package and signing-certificate fingerprint. Both cold and warm links are checked against the runtime allowlist. Encoded path aliases, traversal, credentials, unsupported schemes and malformed URLs are rejected. Android 12+ additionally checks the OS's verified-domain status; earlier Android versions report unverified links rather than assuming verification. A link is routing data, never identity or permission for a private session.

Enable services.deviceSignals to receive kind 61 events containing thermal (0 nominal, 1 fair, 2 serious, 3 critical; -1 unavailable), lowPower and audioInterrupted. Initialization reports current capabilities and signals. The existing input/audio suspension paths handle native sheets and interruptions. A game may lower its animation/effect budget when appropriate. The sample reduces its loop rate. Incoming device events have a bounded queue; drain them each frame. An overflow reports a busy signal event with droppedEvents instead of silently pretending every open was delivered.

Integrity evidence and server decisions

Enable services.integrity. Android uses the linked Google Cloud android.integrityProjectNumber and standard Play Integrity requests. Apple needs the App Attest capability and apple.appAttestEnvironment matching its signed provisioning profile (production for distribution). Unsupported hardware/simulators return an ordinary unavailable result. No integrity result blocks the offline game automatically.

Your server issues a short-lived, account/action/request-digest-bound challenge. Pass its JSON handle to RequestGameIntegrity, then send the returned evidence to that server. Apple's public key ID can be retained for the next challenge; its secret key never leaves the SDK. A registration challenge generates a new key; subsequent challenges name an already registered key. The server verifies Apple certificate/nonce/app/environment/counter bindings or decodes Google's standard token and checks request hash, package, signing certificate, licensing and device policy. The reference service stores single-use challenges and short-lived proof IDs. A protected action must compute its own request digest and consume the proof atomically with its synchronous database write. Evidence acquisition is not a client-side trust flag, a grant or proof that cheating is impossible.

The reference backend documents challenge and verification routes, root-certificate pinning, server credentials, optional version policies and replay behavior. A game may use its own authenticated backend session; the native purchase credential remains private and is not exposed to Blitz. A transient reference guest session may be held in memory for an integrity exchange. Durable game-account recovery and cross-platform identity are separate integrations. Never save a bearer credential in an asset or ordinary JSON save file.