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 anindex.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:
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{
"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 dev | To run there too |
|---|---|
| Paths from the site's root don't load | Build with relative paths. Vite: base: './'. webpack: output.publicPath: ''. Create React App: "homepage": ".". Parcel: --public-url ./ |
| A Web Worker started from a file is refused | Start it from a blob (Vite: import W from './worker.js?worker&inline') |
.svg files don't show as images | Use 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:
evalandnew Functionare refused. PixiJS needs itsunsafe-evalmodule for this: addimport '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 yourentrypage is a page: another.htmlfile in the build is served as text.alert,confirmandpromptdo nothing. Draw your own dialog, or send the page'smodalaction.- A service worker of your own does not register.
navigator.serviceWorker.registerfails, so catch it. - A link to another site asks the player first. When they click an
httpslink, or your code callswindow.openon their click, the coin page asks "Open example.com in a new tab?" and opens it when they say yes.window.openreturnsnull. 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 (
100vwby100vh, or your engine's resize mode), and setdefaultSizein 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.htmlmust not setGODOT_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:
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:
{ "engine": { "kind": "godot", "entry": "index", "threads": false } }{ "engine": { "kind": "unity", "entry": "Build/WebGL", "threads": false } }| Field | What it is |
|---|---|
kind | "godot", "unity" or "web" |
entry | Relative to engine/. Godot and Unity: the export's name without an extension (Godot index, Unity Build/<name>). Web: the page, index.html |
files | Optional: 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 |
threads | Must 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.
| Call | What 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 oncecontext().signedInis true.session-token(needsnet: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.statussays how it ended:confirmed,paid(it's on its way),cancelledorfailed. 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:
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.
Paid files
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' }), thenfetch(res.data.url). - Godot: send the action through
chaindaddy.gd, downloaddata.urlwith anHTTPRequestwhosedownload_fileis inuser://, thenProjectSettings.load_resource_pack("user://monza.pck"). - Unity: send the action through
ChainDaddy.jslib, thenUnityWebRequestAssetBundle.GetAssetBundle(url)for an AssetBundle, orUnityWebRequest.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:connectwith its origins (see App Manifest). - Saves: see Saves. The frame has no cookies.
- Fullscreen: declare the
ui:fullscreenpermission and send thefullscreenaction,'{"type":"fullscreen","enabled":true}'(falseleaves), 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-lockpermission, and the engine's own pointer lock works (GodotInput.mouse_mode = Input.MOUSE_MODE_CAPTURED, UnityCursor.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.