五聲音階組句:用音樂理論設計遊戲音效

五聲音階組句:用音樂理論設計遊戲音效

Crystal Hangul 的字塊點擊音效不是從素材包裡挑的。每點一個字塊,你聽到的音高由它在句子裡的位置決定。第一個字塊是 Do,第二個是 Re,第三個是 Mi,依此類推。拼完整句之後,會落一個 Do-Mi-Sol 和弦收尾。

拼句的過程聽起來像在彈一段上行旋律。拼得越順,旋律越流暢。答錯的時候聽到一個低沉的 deny 音效,旋律斷掉。combo 連續答對的時候,背景音樂會加入新的樂器層。整個聽覺體驗和遊戲表現綁在一起。

為什麼用五聲音階

五聲音階(pentatonic scale)只有五個音:宮、商、角、徵、羽,對應西方的 Do、Re、Mi、Sol、La。它的特色是沒有半音,任何兩個音放在一起都不會產生不和諧的感覺。

這個特性很適合遊戲音效。玩家點字塊的順序是固定的(句子的正確語序),但每句的長度不同,有的 3 塊,有的 5 塊。如果用完整的七聲音階,某些音程組合聽起來會很緊張(例如 Si 接 Do)。五聲音階避免了這個問題,不管句子多長,上行的旋律永遠是好聽的。

我用的音階跨兩個八度:Do Re Mi Sol La,然後高八度的 Do Re Mi Sol La。最長的句子用到第八、第九個音也不會超出範圍。

一個音色,用 pitch shift 做出整個音階

音效素材只有一個:一顆 music box(音樂盒)的單音 sample。所有音高都是用 AVAudioUnitTimePitch 即時變調出來的。

AVAudioUnitTimePitch 的 pitch 參數用 cents 為單位(100 cents = 一個半音)。五聲音階的音程換算成 cents:Do 是 0,Re 是 200,Mi 是 400,Sol 是 700,La 是 900,高八度 Do 是 1200,以此類推。

玩家點第一個字塊,pitch 設 0(原音),點第二個設 200(高一個全音),點第三個設 400。引擎收到「第 N 塊被點了」,直接查表拿到 cents 值,餵進 pitch unit,播放 buffer。

和弦收尾

整句拼完的瞬間,我播一個三音和弦:高八度的 Do、Mi、Sol(cents 值 1200、1600、1900)。三個音之間錯開 25 毫秒,模擬音樂盒的琶音質感。

這個和弦落在上行旋律的頂端,聽覺上有一個明確的「完成」感。玩家不需要看評分文字就知道這句拼完了,因為耳朵先接收到了結束訊號。

和弦需要三個聲部同時發聲。我準備了六組 player + pitch unit 的組合,用輪替的方式避免搶佔。music box 的尾音大約持續三秒,如果 player 池太淺,新的音會打斷還在響的舊音,聽起來會像被截斷。六組的深度讓快速連點的時候,輪回來時前一顆已經衰減完了。

垂直分層 BGM

背景音樂有三層:pad(環境鋪底)、rhythm(節奏)、climax(高潮)。三軌在戰鬥開始時同時起跑,相位對齊,但音量不同。

開局只有 pad 層在響,很安靜的水晶鈴環境音。玩家連續答對,combo 升到第一階,rhythm 層淡入,背景開始有節奏感。combo 繼續往上升到第二階,climax 層淡入,音樂變得飽滿。

漏怪或答錯會斷 combo,上層的音樂抽掉,背景重新回到安靜的 pad。壓力的變化可以用耳朵感受到。

我一開始想做的是「BPM 隨 combo 升高」,讓音樂越打越快。試了之後發現即時改變播放速度會連帶改變音高,整首曲子聽起來像加速播放的錄音帶。垂直分層不碰速度,只控制各軌的音量,效果一樣有爽感,實作上簡單很多。

三軌的淡入淡出用手動的音量 ramp。AVAudioPlayerNode 沒有內建的 volume ramp,我用一個 async Task 每 33 毫秒更新一次音量,線性插值到目標值。淡入淡出的時間設在 0.8 秒,夠平滑但又不會讓玩家等太久才感受到變化。

音量層級的拉鋸

Crystal Hangul 同時有三種聲音在爭搶玩家的耳朵:TTS 韓文發音、字塊點擊音效、背景音樂。

TTS 發音是學習的核心,音量必須最大。字塊音效是操作回饋,要聽得到但不能蓋住發音。BGM 是氛圍,要有存在感但不能搶走前兩者。

試玩回饋調了兩輪。第一輪:「水晶鈴聲好大,聽不到韓文發音」。我把 pad 層壓低。第二輪:「還是蓋住了」。再砍一半。最後 pad 層的音量壓得很低,聲音像是從很遠的地方傳來。字塊音效也壓在 TTS 之下。

選關畫面用了另一條獨立的環境音,跟戰鬥的水晶鈴區隔開。進入戰鬥時選關音淡出,戰鬥 pad 淡入。離開戰鬥時反過來。兩條環境音不會同時出現。

爆音:多聲部疊加的陷阱

戰鬥高潮的時候,同一瞬間可能有這些聲音在響:字塊和弦(三個聲部)、敵人碎裂音效、強射 whoosh、BGM 三層。所有聲部的波形加在一起,峰值超過 0dBFS,DAC 硬剪成方波,聽起來就是一聲刺耳的爆音。

我在主混音輸出後面接了一個 Apple 內建的 Peak Limiter:

let limiterDescription = AudioComponentDescription(
    componentType: kAudioUnitType_Effect,
    componentSubType: kAudioUnitSubType_PeakLimiter,
    componentManufacturer: kAudioUnitManufacturer_Apple,
    componentFlags: 0, componentFlagsMask: 0
)
let limiter = AVAudioUnitEffect(audioComponentDescription: limiterDescription)
engine.attach(limiter)
engine.connect(engine.mainMixerNode, to: limiter, format: nil)
engine.connect(limiter, to: engine.outputNode, format: nil)

Peak Limiter 在峰值超過閾值的時候自動壓縮振幅,避免硬剪。再把主混音的輸出音量留一些 headroom 給引擎外的 TTS,確保兩邊加起來也不會爆。

另一個爆音來源是 player 輪替。一次性音效(碎裂、大絕)的尾音可能持續五到六秒。如果池太淺,新音效排到還在響的 player 上,.stop() 會在波形中間截斷,產生不連續的 click 聲。我把池擴大到十組,每次選 player 時優先挑尾音已經衰減完的,全忙的話才挑剩餘尾音最短的,讓被截斷的聽感降到最低。

組句就是演奏

五聲音階讓拼句這個動作有了音樂性。你點字塊的速度決定旋律的節奏,combo 決定背景的厚度,答錯打斷旋律、斷 combo 抽掉配樂。玩家的操作表現直接映射成聽覺體驗,不需要額外的 UI 元素來傳達「你做得好」或「你做得差」。耳朵已經知道了。


水晶韓語塔防是一個用 RealityKit 做的 iOS 塔防遊戲,拼韓文句子建塔,打敗水晶怪物。App Store 下載。這篇是「費曼的實習生」系列,把遊戲開發中的設計思考記錄下來。