音游制作雜談(230411)
本次的雜談涉及到制作下落式音游時(shí)的三個(gè)常見(jiàn)問(wèn)題,先從最基礎(chǔ)的“為什么引入狀態(tài)模式、狀態(tài)模式如何優(yōu)化”談起,然后聊到速度系統(tǒng)和差速系統(tǒng)的實(shí)現(xiàn)。
1、怎么實(shí)現(xiàn)一個(gè)可任意跳轉(zhuǎn)的Autoplay?
// 如果從“調(diào)用游戲引擎的API”為切入點(diǎn)考慮怎么制作音游,那么很容易陷入“實(shí)例化一批新的Note,釋放一批舊的Note”的迷思,然后順理成章地把Note的擊中處理成“分?jǐn)?shù)、Combo等等相加一下,然后調(diào)個(gè)函數(shù)播放一下動(dòng)畫,最后銷毀下物件”什么的。
// 如果需要實(shí)現(xiàn)“可任意跳轉(zhuǎn)的Autoplay”,那么這個(gè)思路會(huì)很麻煩。跳轉(zhuǎn)了之后,需要去考慮哪些Note還在屏幕上,哪些Note已經(jīng)不在屏幕上,…最致命的是,如果有個(gè)Note處于“已經(jīng)判定,動(dòng)畫播放了一半”的狀態(tài)的話,復(fù)現(xiàn)的效果會(huì)很差。
// 因此,非常提倡把Note處理為“狀態(tài)機(jī)+對(duì)象池”的模式。跳轉(zhuǎn)到一個(gè)特定的時(shí)間時(shí),先取出一批當(dāng)時(shí)認(rèn)為“需要顯示在畫面上”的Note,然后從對(duì)象池里取出一批游戲?qū)ο螅O(shè)置下參數(shù),引擎在當(dāng)前Gameloop把腳本邏輯跑完一遍之后就會(huì)自動(dòng)把你想要的“畫在屏幕上”,完事。
2、分段優(yōu)化
// 上面的工作模式講到了“取一批Note”的過(guò)程,這個(gè)過(guò)程通俗地理解,就是把手頭的Note都問(wèn)一遍“你是否符合我的要求?”的過(guò)程。
// 假定一張譜有1000個(gè)Note,設(shè)備是60fps,那么每秒就是60000次這樣的“發(fā)問(wèn)”。
① 如果設(shè)備是PC,或者使用了il2cpp,乃至原生C庫(kù)一類比較高效的架構(gòu),這個(gè)量級(jí)不會(huì)遇到太大的問(wèn)題;但如果是移動(dòng)設(shè)備的話,對(duì)于如此大量的“發(fā)問(wèn)”,即使SoC的極限性能能支撐起這個(gè)量級(jí),也有很大可能促成發(fā)熱、費(fèi)電、卡頓掉幀等。
② 另外,假設(shè)1分鐘的譜面有1000個(gè)Note,10分鐘的譜面有10000個(gè)Note。游戲在處理“10分鐘的譜面”時(shí),每個(gè)Gameloop在Note輪詢方面都要承擔(dān)10倍于處理“1分鐘的譜面”的性能開(kāi)支,而就整個(gè)游戲流程來(lái)看,10分鐘的游戲流程Gameloop的量也是1分鐘的游戲流程的10倍。Gameloop數(shù)g正比于t,note數(shù)正比于t,發(fā)問(wèn)數(shù)是二者相乘,就正比于t的二次方了。這種二次關(guān)系對(duì)于稍微長(zhǎng)一些的游戲流程會(huì)非常不利。
// 為此,有一個(gè)比較自然的想法,就是控制一下每個(gè)Gameloop的輪詢范圍。可以設(shè)想一下按時(shí)間為1-1000ms,1001-2000ms,…把所有的Note分進(jìn)若干的組,那么Gameloop時(shí)只需要將時(shí)間做一下對(duì)應(yīng),就可以做到一個(gè)Gameloop最多輪詢(完整顯示組數(shù)+2)組Note。
// 這個(gè)優(yōu)化收益還是比較可觀的,試試,沒(méi)效果也不會(huì)有啥損失。
3、變速和實(shí)時(shí)調(diào)速的實(shí)現(xiàn)
// 視覺(jué)變速系統(tǒng)(下稱SC)有一種常規(guī)實(shí)現(xiàn),就是構(gòu)建一個(gè)SC節(jié)點(diǎn)表,以這個(gè)表為依據(jù)為每個(gè)Note計(jì)算一個(gè)SC系數(shù)(下稱dtime)。
// 如何根據(jù)樂(lè)曲進(jìn)度(從樂(lè)曲開(kāi)始播放經(jīng)過(guò)的秒數(shù))t、SC節(jié)點(diǎn)表、玩家設(shè)置的速度倍率k設(shè)計(jì)一個(gè)dtime -> 畫面pos的投射呢?
// 就個(gè)人的實(shí)現(xiàn)而言,一切的前提是“讓其它形式的時(shí)間向ms時(shí)間靠攏”。這里假定我們存儲(chǔ)的所有時(shí)間都是ms時(shí)間,便沒(méi)有“其它形式的時(shí)間”。
// 接下來(lái),就可以嘗試給SC節(jié)點(diǎn)表的每一個(gè)節(jié)點(diǎn)追加一個(gè)dtime戳了。這里使用的是增量法,左邊是“處理前”,右邊是“處理后”,右邊最后一列給了一個(gè)大概的算式。
// 講解盡量通俗,就不給出具體實(shí)現(xiàn)了。

// 根據(jù)處理后的SC表,可以比較容易地算出一個(gè)Note的dtime,這里采用的是插值的思想,不展開(kāi)講解。
// 從數(shù)學(xué)上,可以這么表述從ms時(shí)間到dtime的一種映射:dtime = f(mstime)。根據(jù)插值思想,這個(gè)表達(dá)式的f是一個(gè)由若干線段構(gòu)成的分段函數(shù)。
// dtime到場(chǎng)景pos又怎么映射過(guò)去呢?可以簡(jiǎn)單的假設(shè)一個(gè)“照度”的概念:
// 考慮一個(gè)Note,出現(xiàn)在一個(gè)假想的“畫面頂端”,然后豎直地落到判定線上。“畫面頂端”的位置記為y,“判定線”的位置記為y0,那么這個(gè)游戲場(chǎng)景的“照度”就是(y-y0)。
// 記照度為i,當(dāng)前時(shí)間為t,那么一屏之內(nèi)顯示的dtime范圍就是[ f(t), f(t)+i/k?],這里引入的比例系數(shù)k便是玩家設(shè)定(或譜面規(guī)定,或兩者都有)的流速了。考慮pos = g(dtime)的形式,不難得出,時(shí)間為t時(shí):
y = g( f(t)+i/k?)
y0 = g( f(t) )
// 由于Note勻速下落,g(dtime)需要是一條直線,解這個(gè)變換的條件就這么湊齊了。
?
// 由于比例系數(shù)k有可能動(dòng)態(tài)改變,按pos進(jìn)行分組優(yōu)化不太現(xiàn)實(shí)。因此,如果實(shí)行分組優(yōu)化的話,可以考慮按dtime來(lái)分組。
// 加載一幀,輪詢哪幾組Note呢?取決于 f(t) 和 f(t)+i/k 兩個(gè)值。設(shè)分組組距為整數(shù)w,把 f(t) 和 f(t)+i/k 強(qiáng)轉(zhuǎn)為整數(shù),輪詢的第一個(gè)組為 f(t) // w(//表示整除);順次輪詢,一共輪詢幾個(gè)組?[f(t)+i/k]//w - f(t)//w + 1 組。
4、下落Note的多節(jié)點(diǎn)狀態(tài)
// 如果一個(gè)Note會(huì)在下落紙帶上相對(duì)運(yùn)動(dòng),那么根據(jù)實(shí)際需要,可以給這個(gè)Note確定一個(gè)“出現(xiàn)在紙帶上的時(shí)間”t0。但考慮到Note相對(duì)運(yùn)動(dòng)的用例沒(méi)有那么常見(jiàn),這個(gè)t0可以直接界定為0。
// 接下來(lái),準(zhǔn)備一個(gè)分段函數(shù)h,使得 t' = h(t) ,dtime計(jì)算相應(yīng)地就變?yōu)榱?dtime = f(t')。這意味著,“出現(xiàn)時(shí)間”后的每個(gè)Gameloop都要為這個(gè)Note確定一個(gè)t'。
// 因此,通常采用這樣一種處理:將附帶相對(duì)運(yùn)動(dòng)事件表的Note單獨(dú)拎出來(lái)組成一個(gè)表,每幀給表內(nèi)還存活著的Note推斷它的t',再尋找t'對(duì)應(yīng)的dtime。計(jì)算完 f(t) 和 f(t)+i/k 后,除了常規(guī)Note表之外,還在這個(gè)單獨(dú)的表“發(fā)問(wèn)”,詢問(wèn)里面的每個(gè)Note,是否要渲染出來(lái)?渲染出來(lái)的話,g(dtime)是多少?
// 最后……g(dtime)比判定線低的話,是否屬于Autoplay要播放打擊動(dòng)畫的情況?播放的話,播放多少?……