互動入門・第一次上手網頁遊戲

7 課從零到懂:每一課都能玩

每課三段:🎮 玩玩看(真的在動)→ 📝 它是怎麼做的(20 行程式碼白話拆解)→ 🔗 在陀螺專案裡(同一個概念的真實版)。順著看完約一小時。

第 1 課遊戲的心臟:畫面為什麼會動

學會:遊戲迴圈(Game Loop)——所有遊戲的共同骨架,只有三句話。

🎮 玩玩看
一顆球在彈——按「暫停」看看:畫面靜止了。所謂「動」,其實是每秒重畫 60 張靜止的圖。

影片是一格一格的,遊戲也是。差別是:影片的每一格是拍好的,遊戲的每一格是「當場算出來的」。負責這件事的程式叫「遊戲迴圈」,全世界的遊戲(含 Unity 的 Update)都是這個結構:

// 這就是全部了——遊戲的心臟只有三件事
function 每一格() {
    更新world();   // 1. 算:球該移到哪(位置 += 速度)
    清空畫面();     // 2. 擦:把上一格擦掉
    畫出world();   // 3. 畫:在新位置重畫一次
    requestAnimationFrame(每一格);  // 請瀏覽器下一格再叫我(每秒約 60 次)
}
在陀螺專案裡 同一個結構在 GameEngine.ts:「更新」是 update()(4000 行——所有玩法:移動/碰撞/技能/計分),「畫」是 _renderFrame()。你在玩的每一秒,這個循環跑了 60 次。

第 2 課操作:讓玩家介入這個循環

學會:輸入處理——鍵盤按下去,只是改了一個變數。

🎮 玩玩看(點一下畫面取得焦點,再用 ← → ↑ ↓ 或 WASD 移動)
手機沒鍵盤?直接按住畫面拖曳也可以。

操作的原理出乎意料地簡單:按鍵不直接移動角色。按鍵只是把「右鍵有沒有被按著」這個變數設成 true;真正移動發生在遊戲迴圈的「更新」步驟裡:

// 按下/放開 → 只記錄狀態
按下('右') → 按著右 = true
放開('右') → 按著右 = false

// 每一格的「更新」步驟才真的動
if (按著右) 角色.x += 速度;

為什麼要繞這一手?因為按鍵事件的節奏(按下才觸發一次)和畫面節奏(每秒 60 格)不同步——「狀態變數」是兩者之間的橋。

在陀螺專案裡 App.tsx 監聽鍵盤 → 寫進按鍵狀態 → GameEngine.update() 每格讀狀態決定陀螺的推力。四個玩家各有一組按鍵配置。之後接機台按鈕,也只是把「按鈕訊號」寫進同一組狀態變數——遊戲邏輯完全不用改。

第 3 課碰撞與手感:陀螺對戰的核心

學會:碰撞檢測+反彈——「手感」就藏在這幾行數學裡。

🎮 玩玩看(拖曳藍球去撞紅球)
兩球相撞會互推+閃白一下——這就是陀螺撞怪的最小版本。

圓形碰撞只需要一句數學:「兩個圓心的距離 < 兩個半徑相加」= 撞到了。撞到之後怎麼反應,就是「手感」的全部祕密:

// 1. 有沒有撞到?(勾股定理)
距離 = √((甲.x - 乙.x)² + (甲.y - 乙.y)²)
if (距離 < 甲.半徑 + 乙.半徑) {
    // 2. 撞到了:沿「兩球連線方向」互推(動量交換)
    推力方向 = (乙 - 甲) / 距離
    乙.速度 += 推力方向 × 撞擊力道
    甲.速度 -= 推力方向 × 撞擊力道
    // 3. 手感調味料:閃白 0.15 秒+凍結一拍+畫面震動
    乙.閃白計時 = 0.15
}
在陀螺專案裡 physics.ts 是這段數學的完整版;CollisionSystem.ts(2100 行)是「手感調味料」的大全——同樣的碰撞,撞到木乃伊、撞到 Boss、超級狀態撞、都有不同的力道/演出/獎勵規則。我們修過的 hit-stop(凍結一拍)就是第 3 步的調味料之一。

第 4 課畫面分層:誰蓋住誰

學會:繪製順序 = 圖層。理解了這個,就理解了我們的「兩張紙問題」。

🎮 玩玩看
按按鈕看差別——同一個光圈,先畫它=被角色蓋住(貼地感)、後畫它=蓋住角色(就是你抱怨的「特效蓋最上面」)。

在同一張畫布上,後畫的蓋住先畫的——就這麼簡單。所以 2D 遊戲的「圖層」其實只是「畫的先後順序」:背景最先畫、角色其次、UI 最後畫。

// 每一格的「畫」步驟,順序就是圖層
畫背景();      // 最底
畫地面特效();   // 想被角色踩住 → 比角色先畫
畫角色();
畫空中特效();   // 想蓋住角色 → 比角色後畫
畫UI();        // 最上
在陀螺專案裡(這就是「兩張紙問題」的根源) 改 3D 之後畫面變成兩張畫布疊放:下層 WebGL(3D 怪物)+上層 Canvas(特效)。同一張紙上可以排順序,但兩張紙疊著——上面那張永遠蓋住下面。這就是為什麼特效會蓋在怪物頭上、為什麼要把地面特效搬進 3D 世界(搬進去它才有「深度」,怪物才能站在它上面)。

第 5 課特效:一堆會動的小點

學會:粒子系統——所有華麗特效的基本款,資料+更新+畫,沒有魔法。

🎮 玩玩看(在畫面上點擊 / 拖曳——每次點擊爆一團粒子)
每顆粒子只是一筆資料:位置、速度、剩餘壽命。壽命到了就消失。
// 「爆一團火花」= 生 30 筆資料
for (30 次) {
    粒子們.push({ x: 點擊處x, y: 點擊處y,
                  速度: 隨機方向 × 隨機力道,
                  壽命: 0.6秒 })
}
// 每一格:所有粒子照物理走一步、快死的變透明
for (每顆粒子) {
    粒子.位置 += 粒子.速度
    粒子.速度.y += 重力
    粒子.壽命 -= 時間
    畫小方塊(粒子.位置, 透明度 = 剩餘壽命比例)
}

衝擊波、斬擊、光環都是同一思路的變體:一筆資料描述特效狀態,每格更新,每格畫。差別只在「畫」的方式——這正是報告裡「特效四種做法」在講的事(CPU 畫/GPU 粒子/shader 公式/貼圖)。

在陀螺專案裡 GameEngine 的 particles / shockwaves / slashLines 陣列就是「資料」;EffectRenderer.ts 是 2D 的「畫」;我們正在做的特效 3D 化 = 「資料完全不動、只換『畫』的方式」。

第 6 課2D 與 3D 的差別到底在哪

學會:3D = 多一個「高度」維度+一台「相機」。用假 3D 示範最容易懂。

🎮 玩玩看
同一群柱子,切「透視」後:越靠畫面邊緣的柱子越「往外倒」,看得到側面——這就是我們給陀螺專案開透視鏡頭後的效果。

2D 世界每個東西只有 (x, y);3D 多了「高度」。而要把 3D 世界變成螢幕上的 2D 圖,需要一台相機做投影。兩種投影方式:

正交投影透視投影
比喻建築平面圖:不管遠近,一樣大人眼/照片:近大遠小
俯視遊戲的效果完全平面感(2D 時代的樣子)邊緣物體露出側面、有立體感
我們的專案2D 版+3D 化初期現在(輕透視,強度可調)
在陀螺專案裡 CameraSystem.ts 就是那台相機。3D 化的難點也在這裡具象化了:怪物有了「身高」,所以會在透視下產生偏移;貼在平面上的特效沒有身高,所以會「對不上」——第一次上手最容易暈的坑,本質只是「有沒有高度」的差別。

第 7 課陀螺專案完整地圖

把前六課學的概念,對到真專案的每個部分——之後想改什麼,就知道去哪裡。

你學到的概念真專案的位置規模
遊戲迴圈(心臟)1GameEngine.ts 的 update + _renderFrame~4700 行(含全部玩法)
輸入處理2App.tsx 鍵盤層 → 按鍵狀態4 玩家配置
碰撞數學3physics.ts~500 行
手感規則(調味料)3CollisionSystem.ts~2100 行特例
2D 繪製與分層4renderers/(Arena/Entity/Effect/UI 四支)共 ~8000 行
3D 渲染(新路)6render-pipeline/(場景/相機/怪物/道具/特效 adapter)~5000 行
特效資料5GameEngine 的 particles/shockwaves 等陣列
2D↔3D 的翻譯官stateMapper.ts(遊戲狀態→渲染狀態)~700 行
怪物 AI 與生成systems/(Spawn/AI/技能)
機率水位(業務核心)ProbabilityManager票數/中獎控制

學習路徑建議:玩完這 7 課(1 小時)→ 回去讀報告站的「前言+總藍圖」(這時每個名詞都有畫面了)→ 想深入哪塊,對照上表打開那個檔案,或直接問 AI「帶我看懂 XX」。

最重要的一件事 這 7 課的每個範例都是「完整的遊戲程式」——你剛剛已經看懂了 6 個微型遊戲的全部原始碼。陀螺專案只是同樣的概念 × 規模放大 × 加上美術。網頁遊戲沒有黑盒子,每一行都看得到——這就是 AI 能直接開發它的原因。