Skip to content

Engine apps: Godot, Unity and web builds ​

An engine app is a game or interactive app you already built for the browser, run on a coin page as it is. You upload the build folder, and the platform starts it. Three kinds:

  • web: any finished static browser build, a folder with an index.html. Phaser, Three.js, PixiJS, Construct, GameMaker's HTML5 export, or plain JavaScript. See Web builds.
  • godot: a Godot 4.3+ web export.
  • unity: a Unity 2021.3+ WebGL build.

It talks to the page (sign-in, the token's store, a session token for your own server) through window.chaindaddy: see Talk to the page.

New here? Publish your first app is the whole path for a game: install the CLI, sign in, join, then the steps that follow, then ship.

Web builds ​

Put your build in engine/ beside a capp.json with an engine block, and run it:

bash
chaindaddy app init web my-game --build ./dist   # copies your build into my-game/engine/ and writes capp.json
cd my-game
chaindaddy app dev                               # runs it on your computer
json
{
  "engine": { "kind": "web", "entry": "index.html", "threads": false }
}

entry is your page, relative to engine/. Leave files out: chaindaddy app pack lists the folder for you. The page loads as it would anywhere: its scripts in order, modules, import maps, inline scripts, stylesheets, fonts, images, sounds and WebAssembly, all from your build's own folder.

Put every script in the build. A page that loads a script from another host (a CDN copy of Phaser, an analytics or ads SDK), or names a script or stylesheet that isn't in engine/, is refused at submission. So is a script that calls the wallet itself (window.ethereum.request): an app asks the page instead.

Where a web build runs ​

On a coin page your build runs at an address of its own, inside the page's frame: engine/ is the site's root and your entry page is the page. Paths from the site's root (Vite's default /assets/main.js), Web Workers started from a file, IndexedDB, the Cache API and .svg images all work.

Your preview, the boot check and the reviewer run each build at its own preview address, https://preview-<build>.play.chaindaddy.live. Once approved, every visitor gets it at https://<app>.play.chaindaddy.live, and it gains a server of its own (net:connect), signatures and transactions.

chaindaddy app dev differs. It runs your build on your computer in the sealed frame an ordinary app gets, which has no address, so four things are missing. chaindaddy app scan notes the first three when your build uses them:

In chaindaddy app devTo run there too
Paths from the site's root don't loadBuild with relative paths. Vite: base: './'. webpack: output.publicPath: ''. Create React App: "homepage": ".". Parcel: --public-url ./
A Web Worker started from a file is refusedStart it from a blob (Vite: import W from './worker.js?worker&inline')
.svg files don't show as imagesUse PNG or WebP, or fetch the SVG's text and draw it. Inline <svg> and data: URLs work
There is no IndexedDB or Cache API (indexedDB.open throws)Catch it, and save to localStorage: kept between runs with storage:local, up to about 1 MB

A default Vite build is accepted as it is: submit it, and try it in your preview.

Beyond what the frame allows, a web build differs from a page of your own in these ways:

  • eval and new Function are refused. PixiJS needs its unsafe-eval module for this: add import 'pixi.js/unsafe-eval' (v8) or @pixi/unsafe-eval (v7).
  • location.reload() stops the game, as leaving the page does. To restart, reset your game's state in place. Only your entry page is a page: another .html file in the build is served as text.
  • alert, confirm and prompt do nothing. Draw your own dialog, or send the page's modal action.
  • A service worker of your own does not register. navigator.serviceWorker.register fails, so catch it.
  • A link to another site asks the player first. When they click an https link, or your code calls window.open on their click, the coin page asks "Open example.com in a new tab?" and opens it when they say yes. window.open returns null. Cookies read empty.
  • The game runs only inside a coin page. Opened in a tab of its own, its address shows a short note and none of the game.
  • The game fills its slot. Size it to the window (100vw by 100vh, or your engine's resize mode), and set defaultSize in the manifest for its slot on the page.
  • The page itself may be at most 2 MB. Keep scripts and assets in files beside it.

What your game keeps in localStorage and IndexedDB, and how long it lasts: Saves.

A web build you publish from your own storage runs at the same addresses, read from your storage, and so does a Godot or Unity game published that way. It may list as many files as an upload, up to 2,048 for a web build.

What runs ​

Single-threaded exports only. A manifest with threads: true is refused, and so is a build made before Godot 4.3 or Unity 2021.3.

  • Godot: export for Web with Thread Support off (Godot 4.3 or later). The export's index.html must not set GODOT_THREADS_ENABLED = true.
  • Unity: the default WebGL build, which is single-threaded (Unity 2021.3 or later). Uncompressed, Brotli and Gzip builds all work, with Decompression Fallback off (Player Settings → Publishing Settings). A Decompression Fallback build (its files end .unityweb) is refused at submission.

The engine block ​

The engine block in capp.json says what to start. chaindaddy app init writes it:

bash
chaindaddy app init godot my-game     # or: unity, or web
chaindaddy app init godot my-game --build ./export   # you already have the web export: it is copied into engine/

It also prints the export settings to use and where the export goes. For Godot the entry is index (export to engine/index.html). Unity names its files after the folder it builds to, so building into engine/ gives Build/engine; with --build the name is read from your export:

json
{ "engine": { "kind": "godot", "entry": "index", "threads": false } }
json
{ "engine": { "kind": "unity", "entry": "Build/WebGL", "threads": false } }
FieldWhat it is
kind"godot", "unity" or "web"
entryRelative to engine/. Godot and Unity: the export's name without an extension (Godot index, Unity Build/<name>). Web: the page, index.html
filesOptional: the export's files, relative to engine/ (1 to 512; 1 to 2,048 for a web build). Leave it out and chaindaddy app pack lists the folder
threadsMust be false

File names may use letters, digits, spaces, ., _, - and / for folders. Folders are kept as they are, so a Unity StreamingAssets/ folder works.

Test locally ​

Run chaindaddy app dev in the project folder. It checks the build, starts it, and answers the page's actions from sample data: Test locally. A Godot or Unity export starts there the way a coin page starts it; a web build differs. Once you've submitted a build, you can play it on your own coin page before it's reviewed: Preview a build.

Talk to the page ​

Before your game starts (before a web build's first script runs), the page sets window.chaindaddy: the same actions a React app sends, under the same rules (your manifest's permissions, and an unreviewed build's limits). It isn't there anywhere else, such as in your engine's editor, so check for it.

CallWhat it does
chaindaddy.action(json)Sends one action, such as '{"type":"sign-in"}'. It returns a promise of the answer, { ok, data?, error? }, as a JSON string. Pass an object instead and the answer is an object. It does not reject: a failure answers ok: false
chaindaddy.context()The token and the viewer, now: chainKey, crownId, symbol, tokenName, tokenAddress, connectedAddress, signedIn, theme, and what a React app's props carry: market (price, 24h change, holders, sparkline7d; null until the page has it), settings (the page owner's settings for your app), isFullscreen and apiBase
chaindaddy.on('context', cb)Calls cb(context) each time any of it changes (sign-in, wallet, market, settings, fullscreen). Returns the call that stops it. The page also fires a chaindaddy:context event on window, with the context as event.detail

The actions a game uses most:

  • sign-in: the page's own sign-in: the player's wallet signs one message. Its answer arrives once context().signedIn is true.
  • session-token (needs net:connect): proves the signed-in wallet to your own server, so a save can follow the player to another device: Session tokens. Saves on one device need no server: see Saves.
  • iap-purchase (iap:purchase): sells an item from the token's store, {"type":"iap-purchase","sku":"…"}. The page prices it and shows its own purchase sheet. data.status says how it ended: confirmed, paid (it's on its way), cancelled or failed. Reading the store's items is in Selling from an app.
  • iap-consume (iap:consume): spends the player's own consumable with no server, from their click, key or tap: Spending from the app.
  • paid-file: content only buyers get: Paid files.

From a web build ​

Call it from your own code. The object form answers with an object:

js
const page = window.chaindaddy;                       // undefined when you open the build as a file
if (page) {
  const { symbol, signedIn } = page.context();
  page.on('context', (ctx) => showPrice(ctx.market?.price));
  const bought = await page.action({ type: 'iap-purchase', sku: 'skin.gold' });
  if (bought.ok && bought.data.status === 'confirmed') unlockSkin();
}

Godot ​

chaindaddy app init godot puts chaindaddy.gd and saves.gd in the new project's godot/ folder. Add them as autoloads and call the page from GDScript: Godot bridge.

Unity ​

chaindaddy app init unity writes ChainDaddy.jslib and ChainDaddy.cs under unity/Assets/. Copy that folder into your Unity project and call the page from C#: Unity bridge.

On a coin's own site ​

With "embed": true your exported game also runs on a coin's verified sites. It has no threads and no pointer lock there, its saves are kept apart from the coin page's, and it may say it is busy when the coin's plan is used up: Games on other sites.

Saves ​

Declare the storage:local permission and what a Godot or Unity game saves stays in the player's browser, with no code. A web build needs no permission. Limits, what a player can lose, and saves that follow the wallet: Saves.

A level pack, a track or a skin only buyers should get goes in paid/<sku>/, beside engine/, not in it: everything in engine/ is public. Who is served, and the rules, are in Paid files. Ask the page for the file with the paid-file action, then download the URL it answers with (good for five minutes, for that file only):

  • Web builds: const res = await page.action({ type: 'paid-file', path: 'paid/track-pack/monza.bin' }), then fetch(res.data.url).
  • Godot: send the action through chaindaddy.gd, download data.url with an HTTPRequest whose download_file is in user://, then ProjectSettings.load_resource_pack("user://monza.pck").
  • Unity: send the action through ChainDaddy.jslib, then UnityWebRequestAssetBundle.GetAssetBundle(url) for an AssetBundle, or UnityWebRequest.Get(url) for raw data.

A refusal's code is SIGN_IN_REQUIRED (offer sign-in) or NOT_OWNED (offer iap-purchase).

Size tiers ​

Every reviewed engine app may be up to 16 MB to download, and up to 64 MB on request: Size tiers.

What the frame allows ​

Your game runs in its own frame. In that frame:

  • It loads its own version's files. To reach your server, declare net:connect with its origins (see App Manifest).
  • Saves: see Saves. The frame has no cookies.
  • Fullscreen: declare the ui:fullscreen permission and send the fullscreen action, '{"type":"fullscreen","enabled":true}' (false leaves), from the player's tap. The page goes straight to fullscreen and draws its HUD over the game's corners, so leave them clear. Your engine's own fullscreen call doesn't work in the frame, so have its fullscreen button send the action instead.
  • Pointer lock (for mouse-look games): declare the ui:pointer-lock permission, and the engine's own pointer lock works (Godot Input.mouse_mode = Input.MOUSE_MODE_CAPTURED, Unity Cursor.lockState = CursorLockMode.Locked). The browser locks the pointer only after the player clicks the game, and Esc releases it. Without the permission, the lock is refused.
  • Audio starts after the player's first tap on Safari, as on any web page.

What the frame does for you ​

Every game (an engine app, or any app filed under games) gets this from its frame, with nothing to add:

  • Gamepads work through the browser's Gamepad API.
  • The page stays put while your game has focus. Arrows, Space, PageUp/PageDown, Home/End, the wheel and a drag don't scroll it, and a right-click or long press opens no menu. A text field still types. In fullscreen, Chrome and Edge also keep Tab, F1 to F10, WASD and the digits for your game. Esc always leaves fullscreen.
  • Mute and volume are the visitor's. A speaker button on the frame sets them for your game, and they reach every sound it plays, in fullscreen too.
  • A hidden tab silences and suspends your audio until the visitor comes back. A game that should play on sends {"type":"audio","background":true}.

Ship it ​

Pack, sign and submit from the project folder, then preview the build on your own coin page, send it for review and install it: steps 6 to 9 of Publish your first app. For a game, the Game layout (Pro) puts it beside the price chart.