遊戲的骨架:HTML、Canvas 與遊戲迴圈
請 Claude 搭出遊戲最基本的骨架:一張畫布、一個會一直重畫的迴圈,讓一個方塊在畫面上動起來。看懂這個骨架,之後所有功能都只是往裡面加東西。
上一堂課,你在紙上把「接便當」設計好了,存成 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,了解我要做的遊戲。
這一步先不要做遊戲內容,只要幫我搭最基本的骨架:
- 建立 index.html,裡面有一個 480 x 640 的 canvas,置中顯示,在手機上會自動縮小到螢幕寬度
- 建立 game.js,寫一個用 requestAnimationFrame 的遊戲迴圈,分成 update 和 draw 兩個函式
- 畫面上放一個方塊,左右來回移動,碰到邊緣就反彈
- 移動速度要乘上 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);
遊戲迴圈:遊戲的心跳
你看到的「動畫」,其實是瀏覽器一秒鐘重畫好幾十次畫面,每次方塊都比上一次往旁邊挪一點點,看起來就像在動,跟翻頁動畫是同一個原理。
記住這張圖:之後所有新功能,不是加在 update,就是加在 draw。
requestAnimationFrame 的意思是「瀏覽器,下一次要更新畫面時叫我一下」。一般螢幕大約一秒 60 次,有些高更新率的螢幕會更多。每次被叫到,我們就做兩件事:
- update:只負責「算」。方塊該移到哪?有沒有撞到牆?
- draw:只負責「畫」。把畫布擦乾淨,照最新狀態重畫。
把「算」和「畫」分開,是遊戲程式最重要的好習慣。之後出 bug 時,你就能先問:是位置算錯了,還是畫錯了?
Canvas 座標:y 是往下的
在畫布上,每個東西都用 x、y 記錄位置。要特別注意,原點 (0, 0) 在左上角,而且 y 越大越下面,跟數學課的座標剛好相反。
下一堂課的便當「往下掉」,在程式裡就是 y 一直變大。
為什麼要乘上 dt?
dt 是「距離上一格過了幾秒」,通常是 0.016 秒左右。
如果我們寫「每一格移動 3 像素」,在一秒 60 格的螢幕上是每秒 180 像素,在一秒 120 格的螢幕上就變成 360 像素,遊戲難度整整差了一倍。改成「速度 × dt」,意思就變成「每秒移動 200 像素」,不管螢幕快慢都一樣。
程式裡的 Math.min(..., 0.05) 是一個保險:如果你切到別的分頁再切回來,dt 可能突然變成好幾秒,方塊會「瞬間移動」穿牆。限制最多 0.05 秒,就不會出這種怪事。
- 照上面的提示詞請 Claude 搭出骨架,打開
index.html確認方塊在動。 - 自己動手改一個數字:把
speed從 200 改成 500,存檔後重新整理網頁,感受速度的差別。 - 請 Claude 再加一個藍色方塊,讓它「上下」反彈。想一想:這次要改的是 x 還是 y?
- 挑一行你看不懂的程式碼,問 Claude:「這一行在做什麼?請用國中生聽得懂的方式解釋。」
讀完了嗎?
標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。
留言與討論
卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。