Help11 min read

Troubleshooting

Fixes for black screens, missing files, silent audio, ads that never show, blurry canvases, scrolling pages, lost progress and other common problems.

On this page7 sections

Find your problem below. Most of them come down to three things: absolute paths, files that aren't in the zip, and not handling pause.

Loading problems#

My game shows a black (or white) screen#

Work through these in order:

  1. Look at the console. A red error usually names the file or line.
  2. Check for 404s (files not found). That's almost always an absolute path or a missing file. See the next question.
  3. Is index.html at the top of the zip? If it were missing, the upload would have failed, but check you didn't zip an old build folder.
  4. Does your canvas have a size? A canvas with width="0" or CSS height: 0 draws nothing. Set its size from window.innerWidth and window.innerHeight at startup and on resize.
  5. Does your game wait for something that never happens? For example, waiting for a font or a sound that failed to load, or for a fetch to another server that doesn't work offline.
  6. Did a script fail early? One error at the top of your code stops everything after it. Fix the first error first.

My images, sounds or scripts don't load (404)#

You're almost certainly using absolute paths. On Twiddle Play, your game lives in a sub-folder, so a path starting with / points outside it.

game.jsJavaScript
img.src = '/assets/hero.png';   // broken: looks at the top of the server
img.src = 'assets/hero.png';    // works: relative to index.html
img.src = './assets/hero.png';  // also works

Check all of these places:

  • src and href in index.html;
  • image, sound and level paths in your JavaScript;
  • url(...) in your CSS (relative to the CSS file);
  • your bundler's settings: Vite needs base: './', webpack needs publicPath: ''. See Using game engines.

To catch this before uploading, serve the folder above your game and open http://localhost:8080/my-game/. Absolute paths break there exactly like on Twiddle Play.

It works on my computer but not after upload#

  • Upper and lower case. On a phone, Hero.png and hero.png are different files. Windows and macOS don't care, so the bug hides on your computer. Make the names in your code match the files exactly.
  • A file isn't in the zip. Maybe your code loads something from outside the game folder (../shared/sound.mp3), or you forgot to copy a new file into your build.
  • An absolute path that happens to work on your local server because you serve the game at the top. See the question above.
  • Something from the internet (a CDN, a font) that your computer has cached. Test with Wi-Fi off.

It works in the portal but not when I double-click index.html#

Browsers block some things on file:// pages:

  • <script type="module"> (ES modules, Vite and webpack builds);
  • fetch() or XMLHttpRequest to load your own JSON, levels or sounds;
  • most engines, which load assets with fetch.

That's a limit of file://, not a bug in your game: Twiddle Play serves your game from a real web address, where these work. Test with a local server instead (npx serve . or python3 -m http.server). See Testing.

It works in the browser but not in the app#

Check for things that need the internet. On your computer you're online; players often aren't.

  • Scripts from a CDN (<script src="https://cdn.jsdelivr.net/...">, an import map pointing at unpkg). Download the file into your game folder.
  • Google Fonts or other web fonts loaded from a link. Download the .woff2 files and use @font-face.
  • Data loaded from a server: levels, config, leaderboards. Put the data in your zip.
  • Links, pop-ups and redirects. The app blocks navigation to anything outside your game, including links and window.open.

The Works offline line on your version lists every outside server your files mention.

The upload says "N external hosts referenced"#

Your .html, .js or .css files contain http:// or https:// addresses. Each host is listed. For each one:

  • If your game loads something from it, download that file into your zip and use a relative path.
  • If it's just text (a link in a license comment, a URL in a credits screen that isn't clickable), it's harmless. Say so in the version notes for the reviewer.
  • If it's an ad network or analytics, remove it. Only Twiddle Play ads are allowed.

The preview says "Waiting for the SDK…" forever#

The preview couldn't find the SDK in your game.

  • Is the SDK loaded? Check for the two script tags, and that twiddleplay-sdk.js is really in your zip, next to index.html, with exactly that name.
  • Did the page load at all? Look for errors with Open in a new tab.
  • Does your index.html load the SDK only later, from your own code? Use plain <script> tags in index.html instead.

SDK problems#

The upload says "SDK not found"#

The check looks for the word TwiddlePlay in your .html, .js, .mjs and .cjs files. The SDK file itself contains it, so shipping twiddleplay-sdk.js in your zip is enough to pass. If it says "SDK not found":

  • you forgot to copy twiddleplay-sdk.js into the folder you zipped (common with engines that rebuild the folder on every export), and
  • none of your own scripts call TwiddlePlay either.

If your ads are on and the SDK really is missing, no ads can ever show and you earn nothing.

"TwiddlePlay is not defined"#

Your code ran before the SDK loaded. Put the two SDK <script> tags above your own scripts in index.html, without async or defer on them. If your own script is a module or uses defer, that's fine: it runs after them anyway.

Ads never show#

Go through this list:

  1. Are ads on? Check your game's Settings. With ads off, every ad call answers no.
  2. Is it too early? No interstitial shows in the first 60 seconds of play, none within 90 s of the last, and at most one per 3 minutes. On your own computer the console says why: [TwiddlePlay] interstitial not shown: first 60 s of a session. In the portal preview, the limits are off by default, so an interstitial should show every time.
  3. Do you wait for the promise? If you start the next level without await, the ad may show behind your game or get cut off.
  4. Is the SDK in your game? See "SDK not found" above.
  5. In the app: ads come from Google, and sometimes there's no ad to show. { shown: false } is normal; your game should just carry on.

See Why showInterstitial() returned { shown: false }.

My rewarded button never appears#

You probably hide it when hasRewarded() is false, which is right. It's false when:

  • your game's ads are off;
  • another ad is on screen;
  • in the app, no rewarded ad is loaded yet. Check again the next time you show the button, not just once at startup.

On your own computer, hasRewarded() is always true, so if the button doesn't show there, the bug is in your code.

Look at what showBanner() returned, then go through this list:

  1. Are ads on? With ads off, showBanner() returns { shown: false } and nothing appears. That's by design.
  2. Where are you testing? On your own computer and in the portal preview you get a test bar labelled "Test banner — real ads only in the Twiddle Play app", never a real ad. Real banners only show in the app. If there's no test bar either, your code never reached showBanner(), or a hideBanner() ran right after it.
  3. In the app, was there an ad to give? Sometimes Google has no banner for that phone right now ("no fill"). showBanner() returns { shown: false }; try again the next time a menu opens.
  4. No answer in time? After 15 seconds without an answer, showBanner() gives up with { shown: false }.

The banner cuts off or covers the bottom of my game#

  • In the app and the preview, the banner sits below your game and your game's area gets shorter. If your game is cut off, it read the screen size only once. Re-fit your canvas on every resize event, from window.innerWidth and window.innerHeight. See Banners.
  • On your own computer, the test bar covers the bottom (or top) strip of the page. That's expected: a web page can't make its own window smaller. Check the real layout in the portal preview.

The player got the reward without watching (or didn't get it after watching)#

Give the reward only when the result's rewarded is true:

game.jsJavaScript
const result = await TwiddlePlay.showRewarded();
if (result.rewarded) giveReward();   // not: if (result)

result is always an object, so if (result) is always true. Use Close early (no reward) in the preview to test the "no" path.

Sound problems#

There's no sound until I tap#

Browsers don't let a page play sound until the player has touched it. If you use the Web Audio API, start or resume your AudioContext inside the first tap:

game.jsJavaScript
const audio = new AudioContext();

window.addEventListener('pointerdown', function unlock() {
  if (audio.state === 'suspended') audio.resume();
}, { once: true });

A title screen that says "Tap to play" solves this neatly: the tap that starts the game also unlocks sound.

Sound keeps playing during ads or after leaving the app#

Listen for mute and unmute:

game.jsJavaScript
TwiddlePlay.on('mute', function () { platformMuted = true; applyVolume(); });
TwiddlePlay.on('unmute', function () { platformMuted = false; applyVolume(); });

If you use <audio> elements, pause them on mute too.

Sound comes back after an ad, but the player had muted it#

Keep two separate flags: one for the platform (mute / unmute) and one for the player's own sound button. Play sound only when both allow it. See the on() example.

Gameplay problems#

My game keeps running during ads#

Listen for pause and stop everything: your game loop, physics, and timers like setTimeout and setInterval.

game.jsJavaScript
TwiddlePlay.on('pause', function () { paused = true; });
TwiddlePlay.on('resume', function () { paused = false; lastTime = performance.now(); });

function frame(now) {
  requestAnimationFrame(frame);
  if (paused) return;            // freeze: don't update anything
  const dt = (now - lastTime) / 1000;
  lastTime = now;
  update(dt);
  draw();
}

You can test this without ads: switch to another tab and back.

After a pause, everything jumps forward#

Your game measures time from before the pause. On resume, reset your "last frame" time, as above. It also helps to cap the time step: const dt = Math.min(0.05, (now - lastTime) / 1000);.

My game stutters or feels laggy on phones#

It's smooth on your computer, but on a phone it skips frames now and then. The usual causes, most common first:

  • A huge canvas. devicePixelRatio is 3 or more on many phones. Cap it at 2.
  • shadowBlur or ctx.filter in the game loop. Pre-render the glow or blur once, then drawImage it.
  • New objects every frame (arrays, particles, gradients). The browser has to clean them up, and that shows as a hiccup. Reuse them.
  • Big images. Shrink them to the size you actually draw.

Keep your game smooth shows how to fix each one, with a small example.

My game runs too fast on some phones#

Some phones redraw 90 or 120 times a second, and requestAnimationFrame follows them. If your game moves things by a fixed amount per frame, it runs up to twice as fast there. Move by elapsed time instead: x += speed * dt, where dt is the seconds since the last frame (the loop in My game keeps running during ads does exactly that).

The round timer ran out during an ad#

Count your round timer down by dt inside the game loop, not with setTimeout. Then it stops automatically while you're paused.

My game looks blurry (or tiny) on phones#

Phones have more pixels than CSS pixels (window.devicePixelRatio is often 2 or 3). Make the canvas's real size bigger, and scale the drawing:

game.jsJavaScript
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(innerWidth * dpr);
canvas.height = Math.round(innerHeight * dpr);
canvas.style.width = innerWidth + 'px';
canvas.style.height = innerHeight + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);   // now draw in CSS pixels

If the whole page is tiny, you're missing the viewport tag:

index.htmlHTML
<meta name="viewport" content="width=device-width, initial-scale=1">

The page scrolls, zooms or bounces when I play#

Stop the browser from treating your game's touches as scrolling:

style.cssCSS
html, body { margin: 0; height: 100%; overflow: hidden; overscroll-behavior: none; }
canvas { display: block; touch-action: none; }
body { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; }

Touch doesn't work (but the mouse does)#

You're probably listening for click or mousedown only. Use pointer events, which work for touch and mouse with one listener:

game.jsJavaScript
canvas.addEventListener('pointerdown', function (e) {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  handleTap(x, y);
});

Also make sure nothing invisible (a transparent <div>, a hidden menu) covers the canvas.

My game is the wrong way round#

Set Orientation on your store page: Portrait, Landscape or Any. The app locks the screen to it. If you choose Any, your game must handle resize events and look right both ways. Check both with the Portrait/Landscape buttons in Test it.

alert() or confirm() doesn't show#

They're blocked in the portal preview's frame, and they look out of place in a game anyway. Draw your own dialog with HTML or on your canvas.

My game closes by itself in the app#

The app closes a game whose page crashes, usually because it ran out of memory, with the message "The game stopped unexpectedly." Phones have much less memory than your laptop. Make images smaller, unload levels you're done with, and avoid huge canvases (cap devicePixelRatio at 2).

Saving problems#

My game forgets the player's progress#

Save with localStorage (or IndexedDB). In the app, every game gets its own private storage, and it survives updates of your game.

game.jsJavaScript
const SAVE_KEY = 'firefly-jar.save';    // include your game's name

function save(data) {
  try { localStorage.setItem(SAVE_KEY, JSON.stringify(data)); } catch (e) { /* storage full or blocked */ }
}
function load() {
  try { return JSON.parse(localStorage.getItem(SAVE_KEY)) || null; } catch (e) { return null; }
}

Things to check:

  • Save at the right moments: when a level ends, and on pause (the app may be closed while paused).
  • Use a unique key with your game's name. In the portal preview, all games share one storage area, so a key like save or level can clash with another game's.
  • Wrap it in try / catch. Storage can be full or blocked; your game shouldn't crash because of it.
  • Don't change your save format without a plan. An update must still read the old save, or start fresh without crashing.

There's no cloud save: progress stays on the phone. If a player uninstalls your game, their progress may or may not be kept.

Upload problems#

My upload was rejected#

The message says why. Find it in the error table, which lists every upload message with its fix.

My zip is too big#

The zip can be at most 50 MB, and the files inside at most 150 MB together.

  • Convert big PNGs and JPGs to WebP.
  • Convert WAV audio to OGG or MP3, and use mono for sound effects.
  • Remove assets you don't use, and source files (.psd, .blend, .aseprite), which aren't allowed anyway.
  • For engines (Unity, Godot), strip unused modules and check your engine's size options.

The preview stopped working after a while#

A preview link is valid for 2 hours. Close the preview and press Test it again.

Still stuck?#

  • Compare with a working game: download the Firefly Jar example and the starter template.
  • Re-read the tutorial, which builds a full game with every SDK feature.
  • Use the version notes to ask the reviewer a question when you submit.