Game Devlog

DODGE MASTER 開發日誌:用 Canvas 寫一款賽博龐克閃避遊戲

2026 年 4 月 21 日 約 10 分鐘閱讀 作者:Hao0321 Studio

DODGE MASTER 是 Hao0321 遊戲大廳目前流量最高的一款小遊戲。玩法很直觀:控制一個霓虹方塊在賽博龐克風格的賽道上閃避障礙物、撿金幣、拚高分。但表面簡單的背後其實擠了很多細節 — 物件池、連擊系統、角色稀有度、金幣商店、廣告觸點、每日任務、轉盤獎勵。這篇把整個 921 行的 HTML 單檔拆開講,順便分享我對「街機小品 = 讓玩家每 30 秒都有驚喜」的設計哲學。

為什麼用純 HTML 單檔

遊戲大廳其他遊戲都是 React .jsx,只有 DODGE MASTER 是獨立的 dodge-master-v5.html,在大廳裡用 iframe 嵌入。這個選擇是刻意的:

代價是不能直接用 React 的好處(component reuse、hooks 之類),但 DODGE MASTER 整個遊戲 UI 大概 10 個畫面,用 class 切換 + 手寫 DOM 操作就足夠。

核心玩法設計

「30 秒驚喜」原則

街機小品的黏著度不能靠劇情,只能靠每一關都有新東西。我給自己立了一個規則:玩家從開始到死亡的平均 90 秒內,必須遇到至少 3 次「喔這個!」的瞬間。最後盤點下來,驚喜來源有:

這些系統彼此疊加:一局遊戲裡,玩家既在閃障礙物,又在追 combo、又在注意有沒有特定任務的道具出現。認知負荷剛好吃掉短期專注力,讓人停不下來。

難度曲線

遊戲沒有「關卡」概念,是無限長的賽道。難度隨時間遞增:

difficulty = 1 + gameTime * 0.008
spawnRate  = Math.max(35 - difficulty * 4, 12)  // 越後期越快生怪
objSpeed   = 4 + difficulty * 0.3

這個公式沒什麼魔法,就是 playtest 出來的。重點在於前 15 秒給玩家送分(生怪慢、速度慢),建立「我還撐得住」的錯覺,然後陡峭上升。平均每局時長落在 60~120 秒,剛好一個廣告的長度。

Canvas 效能細節

物件池(Object Pool)

一場遊戲裡會生數百個障礙物、金幣、粒子。如果每個都 new 一個 object 再丟棄,瀏覽器 GC 會在高頻時段造成明顯卡頓。我用 pool pattern:

const pool = [];
function getObs() {
  return pool.pop() || { x:0, y:0, vx:0, type:0, alive:false };
}
function returnObs(o) {
  o.alive = false;
  pool.push(o);
}

粒子效果(撞擊、連擊、金幣閃光)的池預分配 500 個,實測 GC pressure 降了 60% 以上。

繪圖順序(Layer Sorting)

Canvas 沒有 z-index 的概念,只有「後畫的蓋前面的」。我把每一幀的繪圖拆成:

  1. 背景漸層 + 掃描線效果(ctx.fillRect + 條紋 repeating pattern)
  2. 道路紋理 + 遠景粒子
  3. 障礙物(按 y 排序,製造近大遠小的深度感)
  4. 金幣、道具
  5. 玩家角色
  6. 粒子特效(打擊、連擊閃光)
  7. HUD(分數、連擊、金幣圖示)

所有繪圖每幀重畫一次,沒有用 requestAnimationFrame 以外的技巧。420px × 720px 的 canvas 在中階手機穩定 60 FPS。

角色 hitbox 縮放

玩家常覺得「明明看起來沒碰到為什麼死了」。我讓玩家的實際碰撞箱比視覺尺寸小 20%(hitbox.r = visual.r * 0.8)。這在動作遊戲裡是常識級的魔法 — 視覺上差 2 pixel 對玩家心理有巨大差別。

角色稀有度系統

10+ 個角色分 5 種稀有度,每個有不同視覺與屬性修飾:

稀有度數量解鎖方式屬性範圍
COMMON4初始 / 看廣告乘數 1.0x
RARE3開寶箱乘數 1.1~1.2x
EPIC2抽卡乘數 1.3~1.4x
LEGENDARY2任務累積乘數 1.5~1.6x
MYTHIC1全成就乘數 1.7~2.0x

屬性差異不能太大,否則玩家會覺得免費角色被逼退。我訂的準則是:MYTHIC 比 COMMON 強 70%,但不會讓 COMMON 玩家「無法破紀錄」。實際數據:MYTHIC 玩家平均分數比 COMMON 高約 50%,符合設計。

為什麼稀有度系統對留存重要

免費街機遊戲最大的挑戰是「玩家為什麼今天還要開」。稀有度系統提供了兩層動機:

  1. 短期:每局的寶箱開箱 + 轉盤的驚喜(變動報酬 schedule,心理學上最上癮的結構)
  2. 長期:蒐集所有角色的成就感(完成率 = 終極牽引力)

AdSense 整合觸點

免費遊戲的收益來源就是廣告。在不破壞體驗的前提下,我設了 7 個「看廣告領獎勵」的選擇性觸點:

  1. Revive(死亡畫面) — 看一支廣告復活一次。
  2. 2x Coins(結算畫面) — 看廣告把當局金幣翻倍。
  3. Bonus Spin(結算畫面) — 看廣告多轉一次大轉盤。
  4. Free Gacha(商店) — 看廣告免費抽一次角色。
  5. Free Wheel(主選單) — 看廣告轉每日幸運輪。
  6. Daily x2(任務完成) — 看廣告把獎勵加倍。
  7. Shop Deal(商店) — 看廣告領 50 金幣特惠。

所有觸點都是選擇性(opt-in),從不打斷遊戲中期、從不強制插入。這是我對自己立的一條紅線:寧願賺少一點,也不要讓玩家在撞到關鍵障礙物前三秒跳出廣告

資料持久化

整個遊戲沒有伺服器(不像大廳其他遊戲要上傳分數到 Cloudflare D1)。所有資料存在 localStorage:

D = {
  co: 0,            // coins
  best: 0,          // all-time best score
  bt: 0,            // best time
  bc: 0,            // best combo
  tg: 0,            // total games played
  td: 0,            // total dodges
  chars: {},        // unlocked characters
  activeChar: 'basic',
  missions: {},
  lastDaily: null,
  skinsPurchased: {}
};
saveData = () => localStorage.setItem('DM', JSON.stringify(D));

簡單直接。缺點是換裝置或清快取會掉資料,但這是免費瀏覽器遊戲可接受的權衡。

和遊戲大廳的整合

DODGE MASTER 作為 iframe 嵌在 play.html?game=dodge-master 裡。結算時要上傳分數到 Hao0321 遊戲大廳的排行榜:

function endGame() {
  try {
    const h = window.parent && window.parent.haoGame;
    if (h && h.reportScore) h.reportScore(score);
  } catch (e) {}
}

踩過一個坑:Cloudflare Pages 預設的 _headersX-Frame-Options: DENY,連同源 iframe 都擋掉。改成 SAMEORIGIN 才解決。

數據回饋

目前 DODGE MASTER 的數據(過去一週):

留存曲線符合預期:第一次開玩玩 3-4 局平均、回流率(隔天再開)約 35%。對於一款完全免推廣的瀏覽器小品,這個數字我很滿意。

結語

DODGE MASTER 證明了一個人週末可以做出讓人想連玩 5 局的小遊戲。關鍵不是引擎、不是預算,是設計上的克制:把該做的 5 個系統做好,砍掉 10 個沒有必要的系統。每一條動線都要對玩家有清楚的意義,不能只是「因為其他遊戲都這樣做」。

想親自玩:前往 DODGE MASTER。玩完覺得有什麼可以改進的,歡迎寄信 lo246179268@gmail.com