attach() & the host adapter
attach() hands the SDK your three.js objects. With just those, Bonus Rounds play in overlay mode. Add a worldRoot and a host adapter and they play in native mode: inside your own scene, with your player, your controls and your physics.
Signature
BonusRound.attach({ THREE, scene, camera, renderer }); // overlay mode
BonusRound.attach({ THREE, scene, camera, renderer, worldRoot, host }); // native takeover
// → Promise<{ mode: 'overlay' | 'native-local' | 'native-net', playerId }>
| Option | Required | What it is |
|---|---|---|
THREE | yes | The three.js namespace your game uses (import * as THREE from 'three'). The SDK builds everything with it, so there's never a second copy of three.js. |
scene | yes | Your main THREE.Scene. The ambient prop is added here (or to worldRoot when you pass one). |
camera | yes | The camera players see the game through. Used for viewability: a prop only counts as seen when it's on screen. |
renderer | yes | Your THREE.WebGLRenderer (gl in React Three Fiber). |
worldRoot | native, recommended | The Group that holds your level. It's hidden while the Bonus Round arena is shown, then restored. The ambient prop is added to it, so it moves with your level. |
host | native | The host adapter below: how the SDK moves your player and hooks your frame loop. Native mode turns on when host has getPlayerPosition, teleport and setBounds. |
Call it once, after all four objects exist. Later calls are ignored and resolve to { mode, already: true }, so a hot reload or a re-mounted React component can't attach twice. The resolved mode tells you which takeover you got: overlay, native-local (host adapter, the round runs in the page) or native-net (host adapter with net, your server coordinates the round).
Overlay mode vs native mode
| Overlay mode | Native mode | |
|---|---|---|
| You pass | THREE, scene, camera, renderer | …plus worldRoot, host |
| Where the round runs | A full-screen layer over your canvas, drawn by the SDK | Inside your scene, in a pocket arena far from your level |
| The player's character | A stand-in character in the brand's colours, at your game's scale, with your camera style (first or third person) | Your actual player object, moved by your own code |
| Controls and physics | WASD/arrows, jump and sprint, at the walking speed measured in your game (or learned by our play agent) | Yours, unchanged |
| Integration work | One line | One line plus a ~10-line adapter |
Start with overlay mode: it's one line and works in any three.js game. Move to native mode when you want players to keep their own character, animations, controls and collision in the round, which is what makes a Bonus Round feel like part of your game.
The host adapter (native takeover)
During a native round the SDK builds a branded pocket arena at [0, 0, 2000], far away from your level. It hides worldRoot, moves your player into the arena, keeps them inside it, runs the 15-second round, shows a sponsored leaderboard, and moves them back. Your camera and player code keep working unchanged because nothing about them changes except the position.
const frameCallbacks = [];
BonusRound.attach({
THREE, scene, camera, renderer,
worldRoot: level, // your level's Group
host: {
getPlayerPosition: () => player.position.clone(), // THREE.Vector3, feet position
getPlayerId: () => myPlayerId, // optional: needed for multiplayer
teleport: (v) => { player.position.copy(v); player.velocity?.set(0, 0, 0); },
setBounds: (b) => { arenaBounds = b; }, // { center, radius, obstacles? } or null
onFrame: (cb) => frameCallbacks.push(cb), // call cb(dt) every frame, in seconds
net: { send: socket.send, on: socket.on }, // optional: multiplayer only
},
});
function animate(now) {
const dt = clock.getDelta();
updatePlayer(dt); // your controls + physics, clamped to arenaBounds
for (const cb of frameCallbacks) cb(dt);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
| Host method | Contract |
|---|---|
getPlayerPosition() | Returns the local player's position as a THREE.Vector3 (the SDK reads it every frame to detect pickups). Units are meters; your player is assumed to be about 1.8 m tall. The arena is scaled to what our agent measured in your game. |
teleport(vec3) | Move the player there instantly. Reset velocity so they don't fly off. Called when the round starts and again when it ends. |
setBounds(b) | While in the arena, b = { center: Vector3, radius, obstacles?: [{ x, z, r }] }: keep the player within radius of center on the ground plane, and outside each obstacle circle if you can (the hero's pedestal). null means no limit; restore your normal limits. |
onFrame(cb) | Optional but recommended. Register cb(dt) to run once per frame, before you render; dt is in seconds. Without it the SDK runs its own requestAnimationFrame loop. |
getPlayerId() | Optional. The local player's id, so the SDK knows which pickups and leaderboard rows are “you”. |
net | Optional, multiplayer only: { send(msg), on(type, cb) } over your game's existing socket. Relay Bonus Round messages through your server so every player sees the same round and the same item layout. Single-player games leave it out and the SDK runs the round locally. |
During a native round your render loop, input and movement must keep running: the player is playing. Pause your rules instead: round timers, enemies, damage, scoring. In overlay mode your game is covered, so pausing everything is fine.
The rest of the SDK
| Call | What it does |
|---|---|
await BonusRound.break('intermission') | Plays a Bonus Round at a natural break. Resolves to { filled, completed, reason?, score? } when the round ends, or immediately when there is no ad, the player is frequency-capped, or the format is off. Details. |
await BonusRound.break('test') | Always plays the Fizzpop test round, even on a live game. Never billed. |
BonusRound.rewarded({ onReward, label, button }) | Player-chosen round for a reward you define. onReward runs only if the player completes the round. Details. |
BonusRound.safe(true | false | null) | Tell the SDK whether an interval offer may appear right now. null (default) means automatic: after 1.5 s without input. |
BonusRound.placeAmbient(hint) | Suggest where the ambient prop should go. Details. |
BonusRound.on(type, cb) / off | Listen to SDK events: attach, start, end, reward, ambient, event. Details. |
BonusRound.config({ muted }) | Change settings at runtime, e.g. follow your game's mute button. |
BonusRound.consent(true | false) | From your consent tool: false keeps the anonymous player id in memory only, nothing in localStorage. Privacy. |
await BonusRound.ready() | Resolves once the SDK core has loaded. |
await BonusRound.debug() | A JSON snapshot for support and for AI agents verifying an install: mode, attached, testMode, settings, the current run, ambient, the three.js revision, and the last ad requests and events. |
BonusRound.version, .pub | Loader version and the publisher id it read from the tag. |
Safety guarantees
- Every SDK entry point is wrapped: an error inside the SDK is logged with a
[bonusround]prefix and never thrown into your game. - Every asset has a procedural fallback, so a round always renders even if a model or texture fails to load.
- If the SDK can't load at all (ad blocker, offline),
break()resolves at once with{ filled: false, reason: 'sdk_unavailable' }, andwindow.BonusRound?.…calls do nothing.