Playgama 上移植 5 款純 JavaScript 遊戲的實作筆記:不改動遊戲程式碼
Porting 5 vanilla-JS games to a web game portal without touching game code: notes on Playgama Bridge
作者以一個適配器檔案和建置流程,將 5 款純 JavaScript HTML5 遊戲接入 Playgama,並保持遊戲原始碼不變。適配器負責等待 Bridge 初始化及存檔載入後啟動遊戲,並處理平台儲存、音訊靜音和廣告回調;遊戲目前已上線 Playgama sandbox。進入主目錄需達到 100 次合資格遊玩、平均遊玩時間 5:00,以及每次遊玩 0.30 次廣告展示。
I had five small HTML5 games (no engine, plain JS + Web Audio) and wanted them on a web game portal that shares ad revenue. I picked Playgama because it has a self-serve sandbox: upload a zip and the game goes live on a sandbox page the same day. To graduate to the main catalog, a game has to hit three targets: 100 qualifying plays, 5:00 average play time, and 0.30 ad shows per play.
Disclosure: the games, the code and this write-up were produced by AI coding agents (Claude Code) working on my behalf; the notes below are from their build logs. Playgama's upload form asks which agent/model built the game, and the listings carry an AI badge.
What follows is the integration layer, because it ended up being more interesting than I expected. The rule I set: the game source stays untouched. Every portal-specific behaviour lives in one adapter file plus a build step.
1. A tiny portal API the games call
Each game only knows about a five-method object:
Portal.start() // gameplay began (level / match / song)
Portal.stop() // gameplay ended or paused
Portal.happy() // a "nice moment" (record, achievement)
Portal.midgame(cb) // natural break: maybe show an ad, then ALWAYS call cb exactly once
Portal.setAudio(ctx)// hand over the AudioContext so the adapter can mute it
With no SDK present every call is a no-op and midgame(cb) just runs cb, so the same build runs on GitHub Pages. The build script injects the five or six calls into each game with string patches, and the build fails loudly if a patch target is missing.
2. Load order: run the game only after the SDK is ready
Playgama's required steps are: initialize the Bridge, load saved progress, then send game_ready once the player can interact. Only game_ready is mandatory. The level_started / level_completed family is optional but useful.
The trick that made "don't touch game code" possible: the build rewrites every game <script> to type="text/x-pg-deferred", so the browser ignores them. The adapter then runs them in order after bridge.initialize() and the save read:
async function boot() {
await initBridge(); // with a 10 s timeout so a dead SDK never strands the player
await loadSaves(); // one storage.get for everything
await domReady();
await runDeferred(); // re-create each deferred <script> in order
await frames(); // wait for a painted frame
await B.platform.sendMessage('game_ready');
}
Gotcha: because game scripts now run after DOMContentLoaded has already fired, any game code doing document.addEventListener('DOMContentLoaded', init) silently never runs. Check document.readyState instead, or patch it in the build.
3. Saves: swap localStorage for platform storage
The games all use localStorage. The adapter replaces window.localStorage with an in-memory shim that is filled from one bridge.storage.get at startup and written back debounced to one key. No game code changed, and saves follow the player across devices where the platform supports it.
4. Audio: one controller, many reasons to be muted
Portals reject games that keep playing sound during ads, when the tab is hidden, or when the platform says audio is off. Several things can want silence at once, so the adapter keeps a set of reasons and suspends every tracked AudioContext while the set is non-empty:
function reason(name, on) {
on ? reasons.add(name) : reasons.delete(name);
applyAudio(); // suspend all if any reason, resume ones we suspended if none
}
// reasons: 'ad', 'hidden', 'platform-pause', 'platform-audio-off'
It also wraps the AudioContext constructor so contexts the game creates are tracked automatically. And if the game calls ctx.resume() while a reason is active, the call is held back until every reason clears.
5. Ads only at natural breaks, and never strand the player
midgame(cb) asks for an interstitial and always calls cb exactly once:
- if the SDK skips the ad (minimum delay not elapsed), no "loading" state ever arrives, so a 2.5 s timer continues the game;
- on
closedorfailed, continue; - a 45 s safety timeout as a last resort.
Rewarded ads (a hint for an ad in the puzzle game) are only offered where isRewardedSupported is true. Everywhere else hints stay free, so no button ever leads nowhere.
6. Small things that cost me time
- Google Fonts: self-host them at build time. Portals run games in iframes with strict policies, and one font CSS response came back without a woff2 URL and crashed the build. The build now skips such blocks.
- Cross-origin isolated hosts (COOP/COEP): one external request without CORP headers breaks the page, so the builds make zero external requests.
- Parallel test runs on one machine: fixed CDP/http ports collided and one test served the wrong game. Use random ports.
The games
They're live on Playgama's sandbox now. Feedback on the first minute (is it clear what to do?) is very welcome, because average play time is the target I'm least sure about.
- Seedsong: a rhythm garden. Hit seeds on the beat and a night garden blooms. Type any word and it grows its own song, plus a daily "Today's song". https://playgama.ai/play/h2lla7p4eujkc6
- Moth Party: one button each, four moths, one lamp: a party game for up to 4 players on one keyboard or phone, with CPU moths for solo play. https://playgama.ai/play/nuxkra3i4xiowc
- Unclaimed: one rainy night as the clerk of a tiny railway station's lost property office. https://playgama.ai/play/5sc6xnrxe4da5o
- Forecast for Nowhere: a shortwave radio for islands that aren't there. Turn the dial; the stations bleed into each other. https://playgama.ai/play/wydweupfpvcwcc
- Lantern Drift: a sliding-lantern puzzle, 28 nights + a daily lantern. https://playgama.ai/play/nvk6egiap5t7kd
If you've shipped to Playgama, CrazyGames or Poki: what moved your average play time the most?
來源:DEV Community · dev.to