實(shí)現(xiàn)與調(diào)試:從偽相機(jī)到節(jié)拍聯(lián)動(dòng))
Scratch 做的 FNF 模組做到 Part 4 時(shí)鏡頭系統(tǒng)通常比音符邏輯更讓人頭疼。Plutos Reprisal 這個(gè)模組的 Part 4核心任務(wù)是實(shí)現(xiàn)鏡頭移動(dòng)、放大和縮小角色登場(chǎng)時(shí)鏡頭推近連續(xù)命中時(shí)鏡頭輕微放大場(chǎng)景切換時(shí)鏡頭平滑過(guò)渡。這些需求放在 Unity 或 Godot 里不算復(fù)雜但在 Scratch 里背景層要當(dāng)成普通角色控制角色坐標(biāo)要根據(jù)鏡頭位置重新計(jì)算縮放還要同步影響每個(gè)角色的大小。因此實(shí)際開發(fā)中整個(gè) Part 4 幾乎是在邊錄屏邊修 bug 的過(guò)程中完成的。下面把鏡頭系統(tǒng)的變量設(shè)計(jì)、積木結(jié)構(gòu)、節(jié)拍聯(lián)動(dòng)和調(diào)試工作流完整拆開方便同樣在 Scratch 里做節(jié)奏游戲模組的人直接參考。1. 為什么要在 Scratch 里自己造“鏡頭”1.1 FNF 的鏡頭是游戲反饋不只是裝飾在 FNFFriday Night Funkin這類節(jié)奏游戲中玩家最關(guān)心的是音符是否按節(jié)拍命中。音符軌道、角色動(dòng)作、鏡頭運(yùn)動(dòng)三者必須同步玩家才能獲得明確的“打中 / 沒(méi)打中”的反饋。如果把鏡頭放大縮小單純理解為視覺(jué)效果會(huì)直接影響開發(fā)方向當(dāng)玩家打出連續(xù) hit 時(shí)鏡頭輕輕推近可以強(qiáng)化節(jié)拍和成就感當(dāng) miss 出現(xiàn)時(shí)鏡頭恢復(fù)原狀又是一個(gè)明顯的視覺(jué)變化。在 Plutos Reprisal Part 4 里鏡頭移動(dòng)主要負(fù)責(zé)轉(zhuǎn)場(chǎng)和焦點(diǎn)切換當(dāng)前是誰(shuí)在演唱、舞臺(tái)中央是誰(shuí)、歌曲進(jìn)入高潮后畫面是否推近。實(shí)現(xiàn)時(shí)不能只做一次性的“移到某個(gè)位置”而要支持平滑移動(dòng)、按節(jié)奏觸發(fā)、中途被打斷等場(chǎng)景。這個(gè)需求決定了實(shí)現(xiàn)方案的復(fù)雜度。一個(gè)關(guān)鍵認(rèn)識(shí)是Scratch 中“把某個(gè)角色的位置改一下”很容易但“讓整個(gè)舞臺(tái)像一個(gè)攝像機(jī)鏡頭一樣平移縮放”就不自然了因?yàn)?Scratch 沒(méi)有把“攝像機(jī)”當(dāng)作一類對(duì)象提供。接下來(lái)需要明確為什么必須自己實(shí)現(xiàn)。1.2 Scratch 缺少原生攝像機(jī)需要建立“偽相機(jī)”模型Scratch 的舞臺(tái)模型很輕量每個(gè)角色有 x、y、方向、大小、造型編號(hào)和視覺(jué)效果舞臺(tái)本身只負(fù)責(zé)背景切換。沒(méi)有“攝像機(jī)”對(duì)象不能直接綁定一個(gè)視口也沒(méi)有一個(gè)圖層會(huì)自動(dòng)跟隨鏡頭。如果要做鏡頭效果常見(jiàn)做法是把所有畫面元素都放在普通角色中而不是只使用舞臺(tái)背景。用一個(gè)全局的“鏡頭位置”和“鏡頭縮放”描述當(dāng)前誰(shuí)在畫面中心、畫面有多大。每個(gè)可見(jiàn)角色每一幀根據(jù)鏡頭參數(shù)重新計(jì)算自己應(yīng)該出現(xiàn)在屏幕哪個(gè)位置、應(yīng)該顯示多大。這套思路和傳統(tǒng)游戲里的 Camera 概念一致但實(shí)現(xiàn)全落在變量和角色坐標(biāo)上所以叫它“偽相機(jī)”很合適。它的優(yōu)點(diǎn)是完全透明鏡頭參數(shù)改到哪里畫面上所有物體立刻同步缺點(diǎn)是每個(gè)角色都要執(zhí)行一次坐標(biāo)換算如果角色數(shù)量非常多會(huì)成為性能負(fù)擔(dān)。1.3 方案取舍整體縮放與逐角色重算有兩種實(shí)現(xiàn)路線。方案 A把所有要參與鏡頭的角色一起平移和放大。具體做法是設(shè)置一個(gè)透明的“鏡頭中心”角色所有可見(jiàn)角色把位置都建立在它周圍通過(guò)改變鏡頭中心的位置和所有角色的大小實(shí)現(xiàn)移動(dòng)縮放。這個(gè)方案操作簡(jiǎn)單積木少適合只有一兩個(gè)角色的小演示。但做不到視差背景也很難單獨(dú)控制某一層是否參與縮放。方案 B每個(gè)角色都存一套世界坐標(biāo)每一幀用鏡頭參數(shù)換算出最終屏幕位置和大小??梢越o不同圖層設(shè)置不同視差系數(shù)也可以讓 UI 層完全不參與鏡頭。適合 FNF 模組這種需要多層角色、多場(chǎng)景切換的項(xiàng)目。缺點(diǎn)是需要設(shè)計(jì)變量和公式調(diào)試時(shí)也多一步。對(duì)比項(xiàng)方案 A整體平移縮放方案 B逐角色坐標(biāo)換算實(shí)現(xiàn)難度低中視差背景難容易單獨(dú)控制某個(gè)層級(jí)很難靈活性能開銷低每個(gè)角色多幾組運(yùn)算適合場(chǎng)景小演示、原型驗(yàn)證FNF 模組、多場(chǎng)景演出Part 4 之所以把鏡頭移動(dòng)、放大縮小全做完原因也在這里一旦采用方案 B后續(xù)加新場(chǎng)景、新角色都只需給角色配置世界坐標(biāo)不需要重新處理鏡頭邏輯。2. 開工前先把工程分層、命名和變量設(shè)計(jì)好2.1 按層級(jí)組織角色不要混成一個(gè)場(chǎng)景攝像機(jī)制作完成之前工程里的角色應(yīng)該先按“層級(jí)”而不是按“功能”分組。建議這樣區(qū)分背景層遠(yuǎn)處的墻體、燈光、剪影。根據(jù)不同的視差系數(shù)移動(dòng)。角色層玩家角色、NPC、敵人。完全跟隨鏡頭。音符層下落音符和判定線。跟隨鏡頭的主體縮放也有自己的軌道邏輯。UI 層血量條、分?jǐn)?shù)、按鈕。一般不參與鏡頭縮放或只做輕微跟隨。操作上推薦在角色命名時(shí)加前綴比如BG_背景1、CHAR_主角、UI_血量條。Scratch 中角色從上到下順序就是圖層順序舞臺(tái)背景在最底層角色需要先“移至最后層”才能保證不會(huì)壓住其他元素。這個(gè)細(xì)節(jié)在這些項(xiàng)目的初期經(jīng)常出錯(cuò)鏡頭一動(dòng)前排角色突然蓋住后排看起來(lái)就像穿幫。2.2 鏡頭變量和軌道列表是整套系統(tǒng)的主干建議在建工程時(shí)先創(chuàng)建這些變量變量名默認(rèn)值作用注意事項(xiàng)camX0鏡頭中心在世界坐標(biāo)系中的 X對(duì)應(yīng)世界坐標(biāo)原點(diǎn)camY0鏡頭中心在世界坐標(biāo)系中的 Y對(duì)應(yīng)世界坐標(biāo)原點(diǎn)camZoom1當(dāng)前縮放倍率1 為原始畫面不建議長(zhǎng)期小于 0.5 或大于 3targetX0鏡頭移動(dòng)目標(biāo) X用來(lái)做平滑插值targetY0鏡頭移動(dòng)目標(biāo) Y用來(lái)做平滑插值targetZoom1希望縮放到的目標(biāo)倍率用來(lái)做平滑插值z(mì)oomPulse0命中瞬間的臨時(shí)縮放疊加量用于 FNF 的連擊反饋logEnabled0是否記錄調(diào)試日志在正式發(fā)布前可以關(guān)閉除了變量還需要用列表存放軌道數(shù)據(jù)。FNF 有多個(gè)音符軌道每一軌的判定線 x 坐標(biāo)可以放在一個(gè)列表里。這樣切歌、切角色時(shí)不需要改角色腳本只改列表內(nèi)容就行。Scratch 中的變量和列表在積木層面就是存儲(chǔ)單元使用習(xí)慣越接近真實(shí)編程里的“配置表”后面排錯(cuò)越輕松。2.3 素材導(dǎo)入與準(zhǔn)備背景、角色、音效和亮度特效寫 Scratch 項(xiàng)目時(shí)可縮放的背景素材一定要放進(jìn)“角色”而不是只放在“舞臺(tái)背景”里。原因很簡(jiǎn)單舞臺(tái)背景不能單獨(dú)設(shè)置 x、y 和大小無(wú)法參與鏡頭公式。正確做法是把一張背景 PNG 導(dǎo)入為一個(gè)新角色放在背景層然后讓它和其他角色一樣執(zhí)行同樣的鏡頭坐標(biāo)換算。如果素材來(lái)自網(wǎng)絡(luò)要檢查授權(quán)范圍確認(rèn)可以二次創(chuàng)作后再使用。導(dǎo)入時(shí)注意位圖分辨率鏡頭放大后位圖素材會(huì)出現(xiàn)明顯模糊。建議準(zhǔn)備比舞臺(tái)顯示范圍更大一倍的素材或者使用矢量圖。尤其是背景如果標(biāo)準(zhǔn)舞臺(tái)是 480x360用于做鏡頭的背景可以做成 960x720 甚至更大邊緣多留出安全區(qū)域。聲音素材方面Scratch 的計(jì)時(shí)器、音樂(lè)播放邏輯和角色渲染是分開的不用特別壓縮音頻但要注意導(dǎo)入時(shí)長(zhǎng)與歌曲對(duì)拍。亮度特效也值得提前了解Scratch 的“亮度”效果取值一般在 0 到 100設(shè)為 100 時(shí)角色顯示為全白可以用來(lái)做命中瞬間的閃白反饋。3. 實(shí)現(xiàn)鏡頭移動(dòng)、放大和縮小的核心積木3.1 世界坐標(biāo)到屏幕坐標(biāo)的換算公式鏡頭系統(tǒng)的核心是一個(gè)公式。把相機(jī)中心理解為世界坐標(biāo)系中的一個(gè)點(diǎn) (camX, camY)。屏幕中心顯示的世界點(diǎn)就是相機(jī)的中心點(diǎn)。如果相機(jī)縮放倍率是 camZoom那么任意世界點(diǎn) (wx, wy) 換算到屏幕上的位置是screenX (wx - camX) * camZoom screenY (wy - camY) * camZoomScratch 的舞臺(tái)坐標(biāo)原點(diǎn)默認(rèn)在屏幕中央所以這個(gè)公式可以直接通過(guò)“將 x 坐標(biāo)設(shè)為”“將 y 坐標(biāo)設(shè)為”實(shí)現(xiàn)。角色最終的顯示大小由 camZoom 決定size 100 * camZoom 單位是 %下面是一段示意性的積木結(jié)構(gòu)實(shí)際項(xiàng)目里把世界x、世界y換成自己角色的變量名即可// 角色“任意可見(jiàn)物體”內(nèi)部的渲染邏輯 當(dāng)綠旗被點(diǎn)擊 重復(fù)執(zhí)行 將 x 坐標(biāo)設(shè)為 (((世界x) - (camX)) * (camZoom)) 將 y 坐標(biāo)設(shè)為 (((世界y) - (camY)) * (camZoom)) 將大小設(shè)為 ((100) * (camZoom)) %這段循環(huán)只要持續(xù)執(zhí)行鏡頭參數(shù)變化時(shí)角色位置和大小就會(huì)同步變化這就是鏡頭移動(dòng)與縮放的底層原理。提示如果角色在不開啟鏡頭時(shí)位于舞臺(tái)中央世界坐標(biāo)就是它的 x/y 坐標(biāo)。你可以繼續(xù)用鼠標(biāo)在舞臺(tái)上擺初始位置實(shí)際顯示位置交給鏡頭公式計(jì)算。3.2 用視差系數(shù)讓多層背景產(chǎn)生立體感如果所有圖層移動(dòng)量完全相同畫面會(huì)顯得很平。加入“視差系數(shù)”后遠(yuǎn)處背景移動(dòng)慢一點(diǎn)近處背景移動(dòng)快一點(diǎn)鏡頭移動(dòng)時(shí)就能產(chǎn)生立體感。對(duì)任意背景角色screenX (wx - camX) * 視差系數(shù) * camZoom screenY (wy - camY) * 視差系數(shù) * camZoom實(shí)現(xiàn)時(shí)給每個(gè)背景角色增加一個(gè)變量視差系數(shù)然后在渲染腳本里乘上它。遠(yuǎn)處的背景通常用 0.3 到 0.5近景用 1最前面的裝飾層可以用 1.1 到 1.2。這里要注意視差系數(shù)不應(yīng)該和縮放完全綁定。如果你的鏡頭要放大到 1.3 倍遠(yuǎn)處背景如果也放大到 130%那么它原本覆蓋的安全范圍會(huì)被破壞容易出現(xiàn)露白。更穩(wěn)妥的做法是讓遠(yuǎn)處背景只做輕微平移縮放幅度小于角色層或者干脆給遠(yuǎn)處背景單獨(dú)準(zhǔn)備更大的圖。3.3 平滑移動(dòng)目標(biāo)值、當(dāng)前值與插值直接修改 camX畫面會(huì)瞬間“跳”到新位置。做演出效果時(shí)必須引入平滑移動(dòng)。通常把鏡頭參數(shù)拆成“目標(biāo)值”和“當(dāng)前值”目標(biāo)值由事件設(shè)置當(dāng)前值每幀向目標(biāo)靠近。// 鏡頭全局循環(huán) 當(dāng)綠旗被點(diǎn)擊 重復(fù)執(zhí)行 將 camX 設(shè)為 ((camX) (((targetX) - (camX)) * (0.1))) 將 camY 設(shè)為 ((camY) (((targetY) - (camY)) * (0.1))) 將 camZoom 設(shè)為 ((camZoom) (((targetZoom) - (camZoom)) * (0.1)))公式里的 0.1 表示每幀往目標(biāo)靠近 10%。數(shù)值越大鏡頭移動(dòng)越快數(shù)值越小鏡頭越慢。需要做快速轉(zhuǎn)場(chǎng)時(shí)用 0.3需要慢速介紹場(chǎng)景時(shí)用 0.03。實(shí)際調(diào)試時(shí)如果鏡頭總是“到不了”目標(biāo)檢查插值系數(shù)與循環(huán)執(zhí)行頻率是否匹配。在 Scratch 中循環(huán)頻率會(huì)受到瀏覽器幀率影響所以固定幀率下穩(wěn)定的參數(shù)換到低幀率環(huán)境里可能顯得遲鈍。這時(shí)應(yīng)該把插值系數(shù)提高或者按時(shí)間比例計(jì)算移動(dòng)距離而不是完全依賴循環(huán)次數(shù)。3.4 命中反饋放大脈沖與亮度閃白FNF 在命中音符時(shí)鏡頭會(huì)產(chǎn)生一個(gè)短暫的放大脈沖玩家會(huì)明顯感受到畫面“振了一下”。實(shí)現(xiàn)方法是在命中瞬間給 camZoom 疊加一個(gè)臨時(shí)量 zoomPulse然后讓它逐漸衰減回 0。// 當(dāng)判定為 hit 時(shí)廣播“命中” 當(dāng)接收到 [命中 v] 將 [zoomPulse v] 設(shè)為 (0.08) // 全局渲染循環(huán) 重復(fù)執(zhí)行 將 [camZoom v] 設(shè)為 (((targetZoom) * (1)) (zoomPulse)) 將 [zoomPulse v] 設(shè)為 ((zoomPulse) * (0.8))zoomPulse 初始不建議超過(guò) 0.1否則畫面會(huì)像突然閃跳。0.05 到 0.08 是比較穩(wěn)妥的區(qū)間。衰減系數(shù) 0.8 會(huì)讓脈沖在幾幀內(nèi)快速消失不會(huì)影響下一次命中。命中時(shí)的光亮反饋同樣重要??梢园蚜炼忍匦R時(shí)調(diào)到 50 或 80然后每幀減小回 0將角色的亮度特效設(shè)定為 (50) 將 [flash v] 設(shè)為 (1) // 每幀執(zhí)行 將 [flash v] 設(shè)為 ((flash) * (0.8)) 將角色的亮度特效設(shè)定為 ((flash) * (50))亮度特效不要用在大量背景和角色上否則瀏覽器渲染壓力會(huì)上升容易出現(xiàn)掉幀。先只對(duì)角色層或判定線附近的元素做閃白測(cè)試效果穩(wěn)定后再擴(kuò)大范圍。4. 鏡頭事件和歌曲節(jié)拍怎么聯(lián)動(dòng)4.1 用節(jié)拍計(jì)數(shù)器不要用直覺(jué)等待Scratch 里最常見(jiàn)的錯(cuò)誤是在鏡頭轉(zhuǎn)場(chǎng)里堆等待 0.5 秒。等待積木會(huì)累積誤差中間如果還插入其他邏輯誤差會(huì)更明顯。音符和鏡頭只要對(duì)不上整個(gè)手感就會(huì)崩。推薦使用時(shí)間基準(zhǔn)計(jì)算節(jié)拍。先準(zhǔn)備一個(gè)拍頻變量表示每秒多少拍拍頻 BPM / 60然后在項(xiàng)目開始時(shí)記錄起始時(shí)間每幀根據(jù)當(dāng)前時(shí)間計(jì)算 tick當(dāng)綠旗被點(diǎn)擊 將 [startTime v] 設(shè)為 (計(jì)時(shí)器) // 全局循環(huán) 將 [currentTick v] 設(shè)為 (((計(jì)時(shí)器) - (startTime)) * (拍頻))如果一個(gè)段落從第 0 拍開始那么 tick 從 0 開始累加。鏡頭事件只要指定在第幾拍觸發(fā)就能與音樂(lè)穩(wěn)定同步。4.2 把鏡頭事件定義成“關(guān)鍵幀列表”如果一個(gè)場(chǎng)景要做幾十次鏡頭移動(dòng)直接用廣播事件會(huì)很難維護(hù)。更好的做法是維護(hù)一張鏡頭關(guān)鍵幀表每行數(shù)據(jù)描述一個(gè)鏡頭行為字段示例作用tick32在第幾拍觸發(fā)eventTypemove / zoom / flash事件類型targetX-80移動(dòng)終點(diǎn) XtargetY20移動(dòng)終點(diǎn) YtargetZoom1.3縮放目標(biāo)duration120插值幀數(shù)或拍數(shù)在 Scratch 里可以用多個(gè)列表并行存儲(chǔ)這些字段也可以用“列表套列表”的方式組織。鏡頭調(diào)度器每一幀遍歷列表匹配到當(dāng)前 tick 后執(zhí)行對(duì)應(yīng)事件。這樣做的好處是錄制回放發(fā)現(xiàn)第 64 拍鏡頭位置不對(duì)只需要修改列表里對(duì)應(yīng)行的參數(shù)不用去積木里反復(fù)找廣播。4.3 鏡頭跟不上節(jié)奏時(shí)的調(diào)參方向如果鏡頭移動(dòng)明顯慢于節(jié)拍首先檢查插值速度是不是太慢。把 3.3 里的 0.1 改成 0.15 或 0.2反應(yīng)會(huì)變快但畫面會(huì)顯得更硬。更合理的方向是讓目標(biāo)值提前變化例如希望第 64 拍鏡頭到位那么目標(biāo)值在第 60 拍就開始移動(dòng)給插值過(guò)程留出時(shí)間。另一個(gè)調(diào)整維度是 duration。duration 可以理解為“移動(dòng)所用的幀數(shù)”。Scratch 動(dòng)畫幀率通常按 30 幀/秒估算duration 120 約等于 4 秒。如果想讓鏡頭運(yùn)動(dòng)速度穩(wěn)定不要用固定次數(shù)而是根據(jù)當(dāng)前幀間隔計(jì)算移動(dòng)比例。排查時(shí)先打開變量顯示觀察currentTick是否準(zhǔn)確增長(zhǎng)再觀察targetX是否按計(jì)劃改變最后看camX是否接近目標(biāo)值。三段對(duì)照基本能判斷問(wèn)題出在調(diào)度還是運(yùn)動(dòng)插值。5. “邊拍邊修 bug”的調(diào)試工作流5.1 錄屏回放是發(fā)現(xiàn)問(wèn)題最快的方式標(biāo)題里“邊拍邊修 bug”實(shí)際上點(diǎn)出了 Part 4 最重要的調(diào)試手段屏幕錄制和回放。鏡頭縮放和移動(dòng)是動(dòng)態(tài)過(guò)程在編輯器里看變量很難直觀感受到“畫面突然抖了一下”或“鏡頭推到邊緣露出空白”。只有把完整過(guò)程錄下來(lái)再以播放視角反復(fù)看才能發(fā)現(xiàn)視覺(jué) bug。建議不要看著角色在舞臺(tái)上來(lái)回移動(dòng)就覺(jué)得萬(wàn)事大吉每隔一段時(shí)間錄制一段試玩視頻看完回放再繼續(xù)改。錄制時(shí)固定瀏覽器窗口尺寸保持 Scratch 項(xiàng)目的顯示比例一致否則回放時(shí)對(duì)穿幫范圍的判斷會(huì)失真。從回放中經(jīng)常能發(fā)現(xiàn)這些問(wèn)題鏡頭轉(zhuǎn)到右邊時(shí)背景露白、縮放變大后角色位置明顯偏離軌道、某個(gè) miss 出現(xiàn)時(shí)鏡頭沒(méi)有恢復(fù)。實(shí)時(shí)運(yùn)行中這些畫面往往一閃而過(guò)但回放后每一幀都清清楚楚。5.2 復(fù)現(xiàn) bug 時(shí)要記錄哪些信息到了 Part 4最靠譜的方法不是指望“代碼無(wú) bug”而是把每次回放的問(wèn)題登記下來(lái)。每次試玩時(shí)建議記錄一張簡(jiǎn)單的調(diào)試表字段示例作用復(fù)現(xiàn)時(shí)間第 0:18 秒對(duì)應(yīng)歌曲進(jìn)度當(dāng)前 tick第 64 拍與音符調(diào)度對(duì)應(yīng)音符狀態(tài)連續(xù) hit 12 次后出現(xiàn)判定狀態(tài)是否相關(guān)角色動(dòng)作主角換到第二個(gè)造型造型切換是否觸發(fā)瀏覽器幀率約 28 FPS性能問(wèn)題線索調(diào)試日志已輸出 camera 事件日志內(nèi)容一致性有了這張表修 bug 時(shí)就不需要反復(fù)把整首歌放完。對(duì) Part 4 這種邊錄屏邊修 bug 的項(xiàng)目穩(wěn)定復(fù)現(xiàn)是修復(fù)的前提。提示所有出現(xiàn)過(guò)的鏡頭事件都可以追加到一個(gè)“日志”列表里。這個(gè)列表就是排查斷點(diǎn)的第一現(xiàn)場(chǎng)。5.3 從現(xiàn)象逆向定位根因的排查順序定位 bug 時(shí)按以下順序檢查輸入是否正確鏡頭目標(biāo)值是否被正確廣播與接收。文件路徑和命名角色是否使用了正確名稱變量是否讀錯(cuò)例如把 camX 寫成 camY。依賴順序所有參與縮放的角色的“收到事件”是否在同一次廣播后執(zhí)行順序不同重算結(jié)果也不同。參數(shù)范圍camZoom 是否過(guò)大或過(guò)小插值速度是否超出畫面比例。性能循環(huán)里是否有大量特效、延遲積木或透明 PNG導(dǎo)致幀率下降。Scratch 沒(méi)有控制臺(tái)但可以用“調(diào)試列表”代替日志輸出。程序里所有鏡頭事件都向日志列表追加一條記錄例如當(dāng)前tick, 事件類型, 目標(biāo)X位置。出 bug 時(shí)打開日志列表對(duì)比“事件發(fā)出”和“事件生效”的記錄基本能找到斷點(diǎn)。5.4 鏡頭改動(dòng)后必須做回歸驗(yàn)證鏡頭是全局系統(tǒng)camZoom 或 camX 一改動(dòng)所有角色顯示位置都會(huì)變。回歸驗(yàn)證建議使用這個(gè) checklist背景邊緣不穿幫。前景角色和背景相對(duì)位置不變。音符判定線與軌道圖片重合。亮度閃白結(jié)束后恢復(fù)到原圖。連續(xù)命中后縮放脈沖不會(huì)疊加到失控。不同瀏覽器窗口比例下都沒(méi)有明顯偏移。這個(gè)清單每次改完鏡頭后都要跑一遍。Part 4 里很多問(wèn)題都發(fā)生在“鏡頭移動(dòng)改好了但縮放出錯(cuò)使軌道錯(cuò)位”這種關(guān)聯(lián)回歸上。6. 常見(jiàn)鏡頭 bug 與排查鏈路6.1 高頻問(wèn)題速查表問(wèn)題現(xiàn)象常見(jiàn)原因檢查點(diǎn)處理建議背景邊緣露白或穿幫背景圖尺寸覆蓋不足視差系數(shù)過(guò)大背景在世界坐標(biāo)的可視范圍放大背景圖或增加延伸層縮放后角色錯(cuò)位坐標(biāo)換算漏乘 camZoom或把 camZoom 當(dāng)加法后臺(tái)變量數(shù)值統(tǒng)一公式用自定義積木封裝鏡頭劇烈抖動(dòng)插值系數(shù)過(guò)大camZoom 精度不穩(wěn)定變量變化曲線降低插值系數(shù)限制單幀變化音符判定不同步使用 wait 累積誤差tick 與歌曲時(shí)間對(duì)照改用計(jì)時(shí)器與拍頻計(jì)算亮度閃白不恢復(fù)flash 變量沒(méi)有被插值歸零flash 值是否遞減每幀乘 0.8 直到小于閾值瀏覽器掉幀透明 PNG 過(guò)多或特效濫用檢查瀏覽器幀率減少角色數(shù)量使用矢量圖關(guān)閉舞臺(tái)特效6.2 案例一鏡頭推到邊緣后背景穿幫現(xiàn)象錄制回放時(shí)鏡頭移動(dòng)到底部或右邊緣背景后面露出一條白線。原因通常是背景角色初始尺寸只覆蓋了舞臺(tái)基本范圍。鏡頭移動(dòng)后背景的世界坐標(biāo)范圍不足以覆蓋整個(gè)屏幕于是邊緣露底。處理辦法是把背景素材做成比標(biāo)準(zhǔn)舞臺(tái)大不少。例如標(biāo)準(zhǔn)舞臺(tái)為 480x360背景可以做成 960x720 的可視范圍再配合視差系數(shù)讓背景層移動(dòng)更平緩。這樣鏡頭在常規(guī)移動(dòng)范圍內(nèi)不會(huì)漏出空白。6.3 案例二縮放后角色錯(cuò)位和抖動(dòng)現(xiàn)象鏡頭從 1 倍縮放到 1.3 倍時(shí)角色 x/y 沒(méi)有按縮放比例向外擴(kuò)散像是粘在屏幕上。這個(gè)現(xiàn)象說(shuō)明角色沒(méi)有參與世界坐標(biāo)換算而是在固定屏幕位置直接放大。正確公式是屏幕位置 (世界位置 - 鏡頭位置) * 縮放如果角色先被擺在屏幕習(xí)慣位置然后又乘 camZoom就會(huì)把“屏幕位置”誤當(dāng)成“世界位置”坐標(biāo)自然亂掉。正確做法是讓角色的世界坐標(biāo)固定由鏡頭公式統(tǒng)一決定它在屏幕上出現(xiàn)的位置。抖動(dòng)問(wèn)題多半來(lái)自 camZoom 單幀變化過(guò)大。如果 zoomPulse 一次設(shè)為 0.3畫面就會(huì)出現(xiàn)閃跳。建議把脈沖幅度控制在 0.08 以內(nèi)并用每幀乘 0.8 的方式衰減。6.4 案例三幀率不穩(wěn)導(dǎo)致音符判定不同步現(xiàn)象低配電腦或?yàn)g覽器后臺(tái)切換時(shí)鏡頭特效密集的地方幀率下降同時(shí)音符 hit 判定出現(xiàn)偏移。原因是鏡頭渲染循環(huán)和音符判定循環(huán)都受幀率影響。如果一幀更新一次游戲狀態(tài)幀率高就快幀率低就慢最終導(dǎo)致音樂(lè)和畫面脫節(jié)。解決方向是把計(jì)算基準(zhǔn)從“幀數(shù)”改為“時(shí)間”。音符出現(xiàn)時(shí)間用 tick 與歌曲時(shí)間對(duì)比鏡頭事件也用 tick 觸發(fā)而不是按“第幾次循環(huán)”觸發(fā)。同時(shí)在判定上增加容錯(cuò)窗口例如允許 150ms 內(nèi)的誤差可以緩解低幀率下的判定偏移。7. 把鏡頭系統(tǒng)模塊化后續(xù) Part 才能繼續(xù)用7.1 參數(shù)集中在變量和列表里不要散落到積木中如果一個(gè)模組后續(xù)要換一首歌、換一個(gè)場(chǎng)景已經(jīng)做好的鏡頭系統(tǒng)是否還能直接復(fù)用如果答案是不能說(shuō)明參數(shù)和邏輯耦合太緊。Part 4 做完后的整理里建議把所有鏡頭相關(guān)參數(shù)集中在 camX、camY、camZoom以及鏡頭關(guān)鍵幀列表里。任何演出變化都只改表格和變量不改每個(gè)角色的腳本。這樣換歌、換角色時(shí)鏡頭系統(tǒng)本身是穩(wěn)定不變的。7.2 用一個(gè)相機(jī)角色負(fù)責(zé)寫參數(shù)其他角色只負(fù)責(zé)讀工程結(jié)構(gòu)上建議新建一個(gè)隱藏的“相機(jī)角色”它接收廣播事件并更新 camX、camY、camZoom 等全局變量。所有可見(jiàn)角色不直接處理鏡頭事件只在自己的循環(huán)里讀取這些變量并更新位置和大小。這樣做最大的好處是鏡頭邏輯集中在同一個(gè)角色里。出問(wèn)題的時(shí)候只需要檢查相機(jī)角色和事件列表不需要在幾十個(gè)角色里找“如果收到某某事件”這樣的積木。提示讓鏡頭參數(shù)只由“相機(jī)角色”寫入項(xiàng)目后續(xù)修改會(huì)輕松非常多。7.3 擴(kuò)展方向抖動(dòng)、跟隨、多人焦點(diǎn)和過(guò)場(chǎng)運(yùn)鏡在已有變量體系上可以繼續(xù)擴(kuò)展抖動(dòng)效果每幀給 camX、camY 疊加一段隨機(jī)偏移并逐漸衰減適合爆炸和打擊反饋。跟隨目標(biāo)不需要額外復(fù)雜框架只需要把 targetX 設(shè)為某個(gè)角色的世界坐標(biāo)。多人焦點(diǎn)把 targetX 設(shè)為兩個(gè)角色的世界坐標(biāo)平均值鏡頭會(huì)自動(dòng)居中兩人。過(guò)場(chǎng)動(dòng)畫把鏡頭關(guān)鍵幀表擴(kuò)展出“緩動(dòng)類型”字段支持線性、加速、減速等不同運(yùn)動(dòng)曲線。這些擴(kuò)展都基于同一套 camX、camY、camZoom 變量不會(huì)推翻已有結(jié)構(gòu)。7.4 給新手的練習(xí)順序如果剛開始做 Scratch FNF 模組建議先把核心公式單獨(dú)做成一個(gè) 3 分鐘的小 demo只做一個(gè)背景角色、一個(gè)玩家角色、兩個(gè)變量 camX 和 camZoom。讓兩個(gè)角色每幀按公式重算位置。用按鍵改變 camX 和 camZoom觀察畫面移動(dòng)和縮放。加入第二層背景測(cè)試視差效果。接入 tick 調(diào)度器讓鏡頭事件按節(jié)拍觸發(fā)。最后在真實(shí)歌曲里走一遍“錄屏-回放-修復(fù)-回歸”流程。這套順序做完Scratch 階段的核心難點(diǎn)基本都能掌握。之后再回到 Plutos Reprisal 這種模組系列里繼續(xù)擴(kuò)展時(shí)可以把更多精力放在表演編排和觀感優(yōu)化上而不是反復(fù)修位置和縮放造成的 bug。