iT邦幫忙

2022 iThome 鐵人賽

DAY 25
4
Modern Web

30個遊戲程設的錦囊妙計系列 第 25

Trick 24: 重覆播放的環境音同時有三百個會怎樣

  • 分享至 

  • xImage
  •  

遊戲中總有某些音效需要循環播放,像是迴旋鏢在飛行時產生的咻咻聲,小火堆的辟啪聲,牛群經過時的咚咚聲。如果遊戲能確保聲音來源的數量,那可能沒什麼問題。但像是Minecraft或是其他支援玩家自製模組的遊戲,那麼一次上來三百個咻咻聲,或是上千隻牛迎面走來咚咚咚,別說對遊戲效能的影響了,就是人耳可能第一個就受不了。
too many boomerangs

我們需要一個方法管理遊戲中數百個環境音,將這些環境音動態地濃縮成適當的數量,又不失原味。

環境音中控系統

這個系統的邏輯很簡單,每個想要發聲的音源(如迴旋鏢)都要在中控系統中登記想要發聲的音量大小,然後統一由中控系統來發聲。

/** 寫一個音源的類別,包含音源ID和音量
 * 這個類別不是遊戲中的物件,
 * 而是中控系統內部用來識別音源用的物件代理。
 */
class SoundSource {
    // 建構子
    constructor(
        public id: string,    // 音源ID
        public volume: number // 音量
    ) { }
}

假設我們定義中控系統最多能同時播放三個聲道,那麼在每一幀的更新函式裏,中控系統會選出最大聲的三個聲源,並以這三個聲源登記的音量去調整正在發聲的聲道。

/** 環境音中控系統 */
class SoundCentralController {
    // 聲道的陣列
    channels: PIXI.sound.IMediaInstance[] = [];
    // 音源Map
    sourceMap: { [key: string]: SoundSource } = {};

    // 建構子,給一個最大聲道數量的參數
    constructor(public maxChannels: number) {

    }
    // 登記音源函式(音源ID,音量)
    registerSoundSource(id: string, volume: number): void {
        // 若登記時,音量為正
        if (volume > 0) {
            // 從soundMap取出這個id的音源(有可能是空的)
            let source = this.sourceMap[id];
            if (!source) {
                // 若原本沒有這個音源,就建立一個新的
                this.sourceMap[id] = source = new SoundSource(id, volume);
            } else {
                // 否則只要調整音源的音量就行
                source.volume = volume;
            }
        } else if (this.sourceMap[id]) {
            // 登記音量為零的時候,表示可以刪掉這個音源
            // delete 是刪除物件某個屬性的關鍵字
            delete this.sourceMap[id];
        }
    }
    // 每幀更新函式(其實每10幀更新一次也行,看遊戲對這個音效的更新頻率需求)
    update(): void {
        // 用Object.values()把this.sourceMap裏的東西拿出來變成一個陣列
        let sources: SoundSource[] = Object.values(this.sourceMap);
        // 利用ArrayUtil提供的排序工具,將sources依volume屬性
        // 從大排到小(最後一個參數)
        ArrayUtil.sortNumericOn(sources, 'volume', false);
        // 將排序好的音源,取最前面的幾個(maxChannels個)
        sources = sources.slice(0, this.maxChannels);
        // 放一個迴圈,跑過每個音源
        for (let i = 0; i < sources.length; i++) {
            let source = sources[i];
            // 從目前的聲道找出對應i的聲道
            let channel = this.channels[i];
            if (!channel) {
                // 如果目前聲道沒那麼多,那就新建一個播放聲道
                // 並設定循環播放(loop)
                this.channels[i]
                    = channel
                    = playSound("ironman2022_trick24.shu", { loop: true });
            }
            // 依音源更新聲道的音量
            channel.volume = source.volume;
        }
        // 放一個迴圈,跑過每個沒用到的聲道
        for (let i = sources.length; i < this.channels.length; i++) {
            // 將沒用到的聲道滅了
            this.channels[i].destroy();
        }
        // 只取有用的聲道,跟後面沒用到的說byebye。
        // 直接把陣列的長度改短,
        // 是最快可以把長度後面的元素刪掉的方法。
        this.channels.length = sources.length;
    }
}

有了上面寫好的中控系統,就可以寫個小範例來測試效果了。

CG示範程式
示範程式會放一個主角在畫面中間,然後丟出數十把迴旋鏢,迴旋鏢發出的音量與主角的距離成反比。主角外圍的圓圈是主角可以聽到聲音的範圍。
env sound controller

在示範程式中可以實際體驗這個系統帶來的好處,即使同時有數十把迴旋鏢進入聽力範圍,系統仍然只會選出三個最有影響力的聲源來發聲,不但維護了遊戲的效能,而且也沒有失去鏢聲雜亂的效果。

同學們還可以更進一步,將這個系統改成能夠支援立體音的播放,把左右聲道分開管理,這應該會是個非常有趣的練習題目。


上一篇
Trick 23: 大型垃圾不要丟,資源回收再利用
下一篇
Trick 25: 路徑搜尋的鼻祖-戴克斯特拉
系列文
30個遊戲程設的錦囊妙計32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言