M0–M1:引擎骨架、Rapier 物理與玩家手感
動作遊戲的成敗,八成在「手感」。而手感的地基,是引擎迴圈與玩家控制器。這一篇講怎麼把它們搭起來。
引擎迴圈:可變渲染、穩定物理
最核心的問題:高刷新率螢幕(120Hz、144Hz)不能讓物理跑得比 60Hz 螢幕快。解法是把渲染與物理解耦。
const loop = (now) => {
requestAnimationFrame(loop);
// 鉗制 dt:分頁切回或卡頓時,最多推進 1/30 秒,避免穿牆與邏輯跳躍
const rawDt = Math.min((now - lastTime) / 1000, 1 / 30);
const dt = rawDt * timeScale; // timeScale 讓 hit-stop / 慢動作成為可能
physics.step(dt);
scene.tick(dt);
renderer.render(scene, camera);
};
這裡有兩個關鍵設計:
- dt 鉗制:切到別的分頁再回來,
now - lastTime會是好幾秒。不鉗制的話角色會瞬移穿牆。鉗到 1/30 秒,最壞情況就是慢一點,不會爆。 timeScale全域時間倍率:把它乘進 dt,之後做命中時停(timeScale = 0.05)、升級暫停(timeScale = 0)就只是改一個數字。這個小決定後面省了非常多事。
Rogue Engine 風格的 Component 系統
遊戲邏輯不寫死在引擎裡,而是掛在 GameObject 上的 Component,生命週期是 awake → start → update(dt) → onDestroy。這借鏡 Unity / Rogue Engine 的設計,好處是每個行為(控制器、血量、AI)都是獨立可組合的小元件,日後要遷移到別的引擎也容易。
玩家控制器:用官方角色控制器,別自己造輪子
移動用 Rapier 的 KinematicCharacterController。它幫你處理了牆壁滑動、台階、斜坡——這些自己寫會痛不欲生。角色是 kinematic body(不被力推動,由程式控制位移),每幀把「想移動的向量」交給控制器,它回傳「修正後實際能移動的向量」。
移動方向與面向分離
這是動作遊戲的靈魂細節:你移動的方向,和你面對的方向,是兩回事。
- 移動方向 = WASD
- 面向 = 滑鼠位置
要拿到滑鼠在地面的位置,不用對整個場景做 raycast(貴又不穩),而是對一個 y = 0 的數學平面求交點:
raycaster.setFromCamera(mouseNDC, camera);
raycaster.ray.intersectPlane(groundPlane, aimPoint);
於是你可以一邊往左跑、一邊面向右邊的敵人揮砍——這就是 Hades 那種流暢感的來源。
翻滾與無敵幀
空白鍵朝移動方向衝刺約 0.3 秒,期間附帶 i-frames(無敵幀):受傷判定直接略過。這是 Roguelike 的核心生存技巧,也讓戰鬥從「站樁互砍」變成「進退走位」。
第三人稱鏡頭
鏡頭在玩家上方後傾(類 Hades 的俯角),用幀率無關的阻尼跟隨:
const t = 1 - Math.exp(-8 * dt); // 不管幾 fps,跟隨速度一致
focus.lerp(playerPos, t);
再加一點「朝滑鼠方向的預視偏移」——鏡頭微微往你要打的方向挪,讓你看得到目標。這個偏移很小,但少了它就會覺得「視野很憋」。
到這裡,已經有一個能跑、能翻滾、鏡頭跟得很順的騎士站在地板上了。下一篇進入真正的核心:戰鬥的打擊感。