所有入門單元免費開放 · 課程內容持續更新中

遊戲的骨架:HTML、Canvas 與遊戲迴圈

請 Claude 搭出遊戲最基本的骨架:一張畫布、一個會一直重畫的迴圈,讓一個方塊在畫面上動起來。看懂這個骨架,之後所有功能都只是往裡面加東西。

第 8.2 堂約 14 分鐘

上一堂課,你在紙上把「接便當」設計好了,存成 bento-game 資料夾裡的 DESIGN.md。這堂課我們終於要打開電腦,請 Claude 搭出遊戲的骨架。

先說好:這堂課做完,畫面上只會有一個會左右移動的方塊。看起來很陽春,但它是整個遊戲的心臟。只要你看懂這顆心臟怎麼跳,下一堂課加托盤、加便當、加分數,你都會知道 Claude 在改哪裡。

網頁遊戲由什麼組成?

我們的遊戲不需要安裝任何遊戲引擎,瀏覽器本身就夠了。只要兩個檔案:

  • index.html:網頁本體。裡面放一塊 Canvas(畫布),可以把它想成網頁上的一張白紙,我們用程式在上面畫圖。
  • game.js:用 JavaScript 寫的遊戲邏輯。JavaScript 是瀏覽器看得懂的程式語言,負責「讓東西動起來」。

完成後的資料夾會長這樣:

bento-game/
├── DESIGN.md      ← 上一堂的設計書
├── index.html     ← 網頁與畫布
└── game.js        ← 遊戲邏輯

請 Claude 搭骨架

用上一個單元學過的方式,在 bento-game 資料夾打開終端機,輸入 claude 啟動 Claude Code。然後把下面這段話貼給它:

第一步:搭骨架

請先讀 DESIGN.md,了解我要做的遊戲。

這一步先不要做遊戲內容,只要幫我搭最基本的骨架:

  1. 建立 index.html,裡面有一個 480 x 640 的 canvas,置中顯示,在手機上會自動縮小到螢幕寬度
  2. 建立 game.js,寫一個用 requestAnimationFrame 的遊戲迴圈,分成 update 和 draw 兩個函式
  3. 畫面上放一個方塊,左右來回移動,碰到邊緣就反彈
  4. 移動速度要乘上 dt(距離上一格幾秒),讓不同螢幕上的速度一樣

不要用任何外部函式庫。寫完後用簡單的中文告訴我每個檔案在做什麼。

Claude Code 要建立檔案前會先問你同意,看一下它要做的事情合不合理,再按允許。完成後,到資料夾裡雙擊 index.html,用瀏覽器打開,應該會看到一個珊瑚色方塊在淺綠色畫布上來回跑。

參考版本:index.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>接便當</title>
  <style>
    body {
      margin: 0;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      background: #fff6df;
    }
    canvas {
      background: #e3f7f6;
      border-radius: 12px;
      max-width: 100%;
      max-height: 100vh;
      touch-action: none;
      user-select: none;
      -webkit-user-select: none;
    }
  </style>
</head>
<body>
  <canvas id="game" width="480" height="640"></canvas>
  <script src="game.js"></script>
</body>
</html>

幾個值得認識的地方:

  • width="480" height="640" 是畫布「內部」的大小,我們之後畫圖都用這個座標。
  • max-width: 100% 和 max-height: 100vh 讓畫布在手機上自動縮小,但比例不變。
  • touch-action: none 讓手指在畫布上拖曳時,頁面不會跟著捲動。這是為了下一堂課的觸控操作先準備好。

參考版本:game.js

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d'); // 拿到「畫筆」

const box = { x: 40, y: 300, size: 50, speed: 200 }; // speed:每秒移動 200 像素

// 1. 更新:算出這一格東西該在哪裡
function update(dt) {
  box.x += box.speed * dt;
  if (box.x < 0) {
    box.x = 0;
    box.speed = -box.speed; // 碰到左邊,反過來走
  }
  if (box.x + box.size > canvas.width) {
    box.x = canvas.width - box.size;
    box.speed = -box.speed; // 碰到右邊,反過來走
  }
}

// 2. 繪製:清空畫布,照最新的狀態重畫
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#ff6b4a';
  ctx.fillRect(box.x, box.y, box.size, box.size);
}

// 3. 遊戲迴圈:每一格都先更新、再繪製
let lastTime = 0;
function loop(time) {
  const dt = Math.min((time - lastTime) / 1000, 0.05); // 換算成秒,最多算 0.05 秒
  lastTime = time;
  update(dt);
  draw();
  requestAnimationFrame(loop); // 跟瀏覽器預約下一格
}
requestAnimationFrame(loop);

遊戲迴圈:遊戲的心跳

你看到的「動畫」,其實是瀏覽器一秒鐘重畫好幾十次畫面,每次方塊都比上一次往旁邊挪一點點,看起來就像在動,跟翻頁動畫是同一個原理。

上方是三格翻頁動畫,方塊每一格往右一點;下方是遊戲迴圈:requestAnimationFrame 呼叫 update,再呼叫 draw,然後回到 requestAnimationFrame

記住這張圖:之後所有新功能,不是加在 update,就是加在 draw。

requestAnimationFrame 的意思是「瀏覽器,下一次要更新畫面時叫我一下」。一般螢幕大約一秒 60 次,有些高更新率的螢幕會更多。每次被叫到,我們就做兩件事:

  1. update:只負責「算」。方塊該移到哪?有沒有撞到牆?
  2. draw:只負責「畫」。把畫布擦乾淨,照最新狀態重畫。

把「算」和「畫」分開,是遊戲程式最重要的好習慣。之後出 bug 時,你就能先問:是位置算錯了,還是畫錯了?

Canvas 座標:y 是往下的

在畫布上,每個東西都用 x、y 記錄位置。要特別注意,原點 (0, 0) 在左上角,而且 y 越大越下面,跟數學課的座標剛好相反。

一張 480 乘 640 的畫布,左上角是原點,x 往右變大、y 往下變大;畫面中有一個往下掉的方塊和底部的托盤,標出 x、y、寬度 w 與高度 h

下一堂課的便當「往下掉」,在程式裡就是 y 一直變大。

為什麼要乘上 dt?

dt 是「距離上一格過了幾秒」,通常是 0.016 秒左右。

如果我們寫「每一格移動 3 像素」,在一秒 60 格的螢幕上是每秒 180 像素,在一秒 120 格的螢幕上就變成 360 像素,遊戲難度整整差了一倍。改成「速度 × dt」,意思就變成「每秒移動 200 像素」,不管螢幕快慢都一樣。

程式裡的 Math.min(..., 0.05) 是一個保險:如果你切到別的分頁再切回來,dt 可能突然變成好幾秒,方塊會「瞬間移動」穿牆。限制最多 0.05 秒,就不會出這種怪事。

行動練習
  1. 照上面的提示詞請 Claude 搭出骨架,打開 index.html 確認方塊在動。
  2. 自己動手改一個數字:把 speed 從 200 改成 500,存檔後重新整理網頁,感受速度的差別。
  3. 請 Claude 再加一個藍色方塊,讓它「上下」反彈。想一想:這次要改的是 x 還是 y?
  4. 挑一行你看不懂的程式碼,問 Claude:「這一行在做什麼?請用國中生聽得懂的方式解釋。」

讀完了嗎?

標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。

卡住了?讓 AI 當你的家教複製這段提問範本,貼到 Claude 或其他 AI 工具,再補上你卡住的地方。

留言與討論

卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。

留言區即將開放。現在可以先到 GitHub 討論區 發問,或看看 交流頁 的其他管道。