Integrate

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 }>
OptionRequiredWhat it is
THREEyesThe 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.
sceneyesYour main THREE.Scene. The ambient prop is added here (or to worldRoot when you pass one).
camerayesThe camera players see the game through. Used for viewability: a prop only counts as seen when it's on screen.
rendereryesYour THREE.WebGLRenderer (gl in React Three Fiber).
worldRootnative, recommendedThe 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.
hostnativeThe 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 modeNative mode
You passTHREE, scene, camera, renderer…plus worldRoot, host
Where the round runsA full-screen layer over your canvas, drawn by the SDKInside your scene, in a pocket arena far from your level
The player's characterA 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 physicsWASD/arrows, jump and sprint, at the walking speed measured in your game (or learned by our play agent)Yours, unchanged
Integration workOne lineOne 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 methodContract
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”.
netOptional, 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.
Keep your loop running

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

CallWhat 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) / offListen 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, .pubLoader version and the publisher id it read from the tag.

Safety guarantees