
這篇講的遊戲
- 線上即玩(可單機,也可開房 2-4 人連線):https://yazelin.github.io/ai-chant-magic/
- repo:https://github.com/yazelin/ai-chant-magic
這是一個用嘴施法的網頁遊戲:俯視角、WSAD 移動、滑鼠瞄準,對著麥克風喊出技能名稱就放招。惠惠喊「黑暗、深淵……爆裂魔法」蓄力炸場,御坂喊「超電磁砲」放一道貫穿雷射,貞德喊「聖盾」幫全隊上 buff。單機能玩,也能 2-4 人連線,已經上線。
第一次自己測就出兩個糗。喊一次「超電磁砲」,雷射放了兩道。換到我的開發機更慘,麥克風一直空轉,一招都喊不動。兩個原因後面講。
會想現在寫,是因為這遊戲卡在一個轉折:它本來只是一張工程師用的方格測試地圖,我想把它做成「一關一個原作世界」,但目前只做到第一關。語音施法和連線這些底層算是蓋好了,剩下三關是「做得出來、還沒做」。
所以這篇只能算序章。做好的、還沒提交的、我自己也還沒想清楚的,都照實寫。
地基一:用嘴施法,做起來全是邊角
語音施法用的是瀏覽器內建的 Web Speech API,不是另外訓練的模型。麥克風持續開著轉文字,文字拿去跟法術的「別名清單」做模糊比對,命中又不在冷卻就放招。
為什麼不做喚醒詞?「OK Google」那套不就是業界標準? wake-word 一開始就劃掉了,因為成本算不過來。法術清單要可擴充、要中英多別名,wake-word 每個詞都得訓練一個模型。「持續聽 + 關鍵字比對」體感上一樣是一直聽、喊出來就反應,卻不必為每招養一個模型。對一個一直在加法術的遊戲,這筆帳很清楚。
聽起來很單純,做起來全是邊角料。我驗收時撞到的有兩件。
一句喊出去,招式放兩次。 我第一次喊「超電磁砲」,雷射射出兩道;喊惠惠的蓄力,無冷卻的蓄力槽一次充兩格。一次語音輸入怎麼會觸發兩次施法?病根在 Web Speech 的 interimResults。同一句話它會先吐一次「暫定」結果、再吐一次「最終」結果,當時兩次都被當成一次施法。修法是只認 isFinal 的那一次:
// Only act on FINAL results. interimResults fire the same utterance as
// interim THEN final — casting on both double-triggered every spell
if (!e.results[i].isFinal) continue;
大量「誤聽別名」。 中文語音辨識常把法術名聽成同音的怪詞,於是別名清單裡塞了一堆「聽錯也算數」的詞。光是愛蜜莉雅的「冰柱魔線」,別名就長這樣:
aliases: ['冰柱魔線', '冰柱', '冰錐', '冰霜', '冰', 'frost', 'ice',
'賓住', '並註', '冰住', '賓柱', '賓住無限', '冰柱無限',
'因住無限', '並祝無限', '編著魔線', '奔著魔線']
這些「賓住」「並註」「因住無限」不是我亂打的,是辨識引擎真的會吐出來的東西。比對時還有一條「最長別名優先」的規則,免得某個短的通用別名把另一招更明確的長別名給蓋掉。
開發機上,麥克風像壞掉的繼電器。 我自己的開發機是 Linux + snap 版 Chromium,一開麥克風就「啟動、立刻結束、再啟動、又立刻結束」地空轉,一個字都吐不出來。症狀像硬體壞了,真因是軟體缺件。snap 版 Chromium 根本沒打包 Google 的語音後端,辨識一啟動就無處可去、瞬間收掉。處理方式是數失敗:連續 4 次「沒吐出任何文字又幾乎立刻結束」(MAX_BAD_ENDS = 4、IMMEDIATE_END_MS = 800)就判定後端不可用,停掉重啟迴圈,直接講白話:
此瀏覽器無法使用語音辨識(常見於 Linux 的 snap 版 Chromium,
沒有語音後端)。請改用 Google Chrome 或 Microsoft Edge。
而且不管有沒有語音,遊戲中都能按 1 / 2 / 3 放當前職業的三招。沒麥克風、瀏覽器不支援、或你就是不想喊出聲,都還是能玩。語音是賣點,但不能變成「沒語音就玩不了」的門檻。
各瀏覽器現況,跟想更穩可以怎麼做
順帶講一下 Web Speech API 的支援,其實很參差:
- Chrome / Edge(桌面、Android):支援,但辨識是送 Google 雲端做的,要連網;前面那台 snap 版 Chromium 沒打包 Google 後端,就直接用不了。
- Safari(macOS / iOS):支援,介面是
webkitSpeechRecognition。 - Firefox:不支援辨識(只有語音合成 TTS,沒有
SpeechRecognition)。
所以「開瀏覽器就能用」本身就不保證。它綁瀏覽器、綁 Google 後端,辨識品質和語言各家還不一樣。
辨識器一開始就做成可抽換的介面(client/src/voice/recognizer.ts 的 VoiceInput,Web Speech 只是第一個實作)。想要更穩、跨瀏覽器一致,下一步就是換掉這層:用 getUserMedia 錄一小段音訊,送雲端 STT 轉成文字,再丟進同一套模糊比對。最現成的是 Groq 的 Whisper(whisper-large-v3-turbo,OpenAI 相容的 /audio/transcriptions,快又便宜),或自架 whisper.cpp / faster-whisper。代價是要自備 key 或自架,UX 也從「持續聽」變成「按著講一段再轉」、多一點延遲;換來的是 Firefox、snap Chromium、各家瀏覽器都一致能用,語言和品質也自己控。
地基二:純 TS 模擬 + 權威伺服器 + 快照內插
連線這塊是真的做完了,值得記一下怎麼拆的。整個遊戲走 Input → Command → Simulation → Render 的單向流,拆成三個 workspace:
shared/—— 純 TypeScript 的世界模擬(世界 / 職業 / 法術 / 比對),零瀏覽器、零 Node 依賴。client 和 server 跑的是同一套step()。server/—— 權威的 Nodews伺服器,每 50ms 跑一次step()並廣播快照;有房間代碼和快速加入。(線上這台部署在 Render 免費方案,閒置 15 分鐘會休眠,所以開房連線的第一次可能要等十幾二十秒冷啟動,醒著之後就順了。)client/—— Vite + Phaser;單機時本地直接跑模擬,連線時收伺服器快照、做內插渲染,刻意不做客戶端預測——以一個生存遊戲的節奏,預測帶來的回滾複雜度划不來,收快照內插就夠順。
因為模擬是純函式、client/server 共用同一份,那套「無瀏覽器後端」的語音重啟判斷邏輯也抽進 shared,於是在沒有真實麥克風的情況下也能單元測試。
還有個決定我蠻喜歡:音效和音樂全是 Web Audio 程序合成,零音檔資產。 12 招各有一組能辨識的合成施法音,背景音樂用 look-ahead 排程器排出「微焰 → 星火 → 燎原 → 焚天」4 段強度,隨怪潮遞增、在小節邊界無縫切換。一個音檔都沒有。
這塊地基還有一個好處:因為「世界」被收斂成一份資料、模擬又是純函式,加一整關只要改兩行,主題表加一筆、再翻一下啟用哪一關。
轉折:地基蓋好了,多宇宙卻只走到第一站
這是我寫這篇的主因。地基穩了,接下來該往上長,但只長到一半。
遊戲場景原本是一張工程師方格地圖,深藍底、格線,適合一邊開發一邊看數值,但毫無氣氛。後來把「世界」抽象成一個可切換的主題:一段天空配色 + 一種繪製模式,集中在 GameScene.ts 的 THEMES 常數裡,再用一個 ACTIVE_THEME 切換目前是哪一關:
const THEMES: Record<string, SceneTheme> = {
// "developer's world" — 開發時用的陽春方格
engineer: { /* grid 模式 */ },
// Level 1 — slime / KonoSuba 夢境
slime: { /* dream 模式:史萊姆綠膠泡 + 漂浮氣泡 */ },
};
const ACTIVE_THEME = 'slime'; // 換關卡就改這裡
註解裡留了這段話,很能說明這個遊戲現在的狀態:
加一個未來的世界(Re:Zero / 學園都市 / 現代-貞德 / …,可以不只四個)= 在這裡加一筆 + 翻
ACTIVE_THEME。美術刻意做得很輕——重點是這套架構,不是精緻度。
換句話說:架構已經是為「每關一個原作世界」設計的,但實際只實作了關 1——史萊姆夢境(把《為美好世界》的史萊姆當成「美好世界」的夢境去畫)。Re:Zero、學園都市、Fate 這些,目前只是程式碼裡的藍圖和註解,不是能玩的關卡。
「多宇宙」聽起來不就是畫大餅嗎?說好四個原作世界,實際只有一關能玩,這跟爛尾有什麼兩樣? 一半算數:現在確實只有一關。但會不會爛尾,要看補一關的代價有多大。前面那塊地基的意思就是,加一關等於改兩行加一段繪製,不是重寫引擎;美術是刻意做輕的,不是做不出來,所以單關的內容門檻被壓得很低。代價低、地基不動,所以這是「做得出來、只是還沒做」,不是畫大餅。
四、正在發生的事(還沒提交)
現況是這樣:最新一筆 commit 是 06-19,之後的東西還躺在工作樹裡、沒提交。我驗收到一半:
- 大廳改用 AI 生成的世界背景圖:四張角色卡背景(
lobby-bg-pyro/cryo/storm/warden.png)還是 untracked。 - 貞德的「聖盾」從單純護盾改成全隊威力 buff:護盾範圍內、存活的隊友施法傷害 ×2(
aegisUntil標記 +skillPowerMultiplier)。測試已經寫好了(world.test.ts多了 50 行)但還沒提交。
所以如果你現在去翻 repo,會看到一個處於「能玩、但正在改」的中間狀態。這就是它真實的樣子。
待續
明確會做的:後三關的原作世界(架構已備好,缺的是內容和美術),還有語音辨識的持續改進。
明確不做的:wake-word 喚醒詞、以及帳號 / 排行榜後端——這兩個在設計文件的「非範圍」裡就劃掉了。
至於後三關各自長什麼樣、每個世界該配什麼機制,老實說還在想。這遊戲還沒做完,我也還沒想完。
所以這篇只能到序章。等後面的世界蓋起來,再回來寫續篇。
參考
- ai-chant-magic:https://github.com/yazelin/ai-chant-magic
- 線上即玩(可單機,也可開房 2-4 人連線):https://yazelin.github.io/ai-chant-magic/
- MDN:Web Speech API — SpeechRecognition