← 開發日誌

M2:打擊感的科學——連擊、hit-stop 與命中判定

「打擊感」聽起來很玄,其實是一堆可以量化的小技巧疊出來的。這一篇把它們一個個拆開。

命中判定:形狀查詢,不是生碰撞體

揮砍時要知道「打到誰」。做法不是生一個攻擊碰撞體再等物理引擎回報,而是在命中幀用 Rapier 的 intersectionsWithShape 主動查詢:

world.intersectionsWithShape(center, rot, new RAPIER.Ball(radius), (collider) => {
  const health = combatants.get(collider.handle);
  if (health?.alive) targets.push(health);   // 先收集,別在 callback 裡改物理
  return true;
});

扇形攻擊 = 一個球形範圍 + 角度過濾。關鍵原則:在 callback 裡只收集,迴圈結束後才結算傷害——在物理查詢的回呼中改動物理世界會出事。

三段連擊與連段窗口

普攻不是單一動作,而是「攻擊佇列 + 連段窗口」:

  • 按左鍵 → 播第一段
  • 在動作後段的「連段窗口」內再按 → 接第二段、第三段
  • 窗口外才按 → 重新從第一段開始

這讓連擊有節奏感,而不是無腦連點。

Hit-stop:命中瞬間的時停

這是打擊感最關鍵的一招。砍中敵人的瞬間,把 timeScale 設成 0.05,整個世界凝滯約 60 毫秒再恢復。

engine.timeScale = 0.05;
setTimeout(() => { if (engine.timeScale === 0.05) engine.timeScale = 1; }, 60);

那個 if 判斷很重要:如果這 60ms 內剛好觸發了升級暫停(timeScale = 0),恢復時不能粗暴設回 1,否則會把暫停吃掉。全域狀態的互相干擾,是這類「juice」最容易出 bug 的地方。

人腦會把這一瞬間的凝滯解讀為「打中了、很有力」。沒有它,攻擊會像揮在空氣裡。

Hit-flash:受擊閃白

被打中的敵人材質瞬間 emissive 變白再淡出。要注意 KayKit 模型用 SkeletonUtils.clone 複製時材質是共用的,所以閃白前要先 clone 一份專屬材質,否則整個畫面的同種敵人會一起閃。

擊退與無敵幀

  • 擊退:受擊時沿「攻擊者 → 自己」方向給一個瞬間位移,再指數衰減。
  • 無敵幀:受擊後短暫無敵(約 0.05–0.6 秒),避免一次被多段傷害瞬間秒殺。Health.damage() 在無敵期間直接回傳 false。

敵人 AI:四狀態機

第一隻骷髏的 AI 是經典的 Idle → Chase → Attack → Death

  • Chase:朝玩家移動,外加「鄰兵分離力」——彼此推開,避免怪疊成一坨。
  • Attack:先放**前搖(telegraph)**再出手。前搖讓玩家有反應時間,這是公平性的來源;沒有前搖的攻擊會讓人覺得「被偷打」。

傷害數字

命中飄出傷害數字,用物件池的 sprite(預先建好 32 個循環重用,零持續配置)。透過事件總線觸發:戰鬥系統發 damageDealt,飄字元件訂閱它。這種事件解耦讓後面加粒子、音效、螢幕震動時,全都只要訂閱同一個事件就好。


到這裡,砍怪已經「脆」了——有頓挫、有閃白、有飄字、有擊退。下一篇讓這些戰鬥發生在每次都不一樣的隨機地城裡。

← 回開發日誌立即遊玩 →

看更多開發日誌 →