優雅的模塊化-單例模式-音頻資源預加載

摘要

CocosCreator 的音頻資源本地加載有兩種辦法,其一是在腳本中聲明並拖入屬性面板,其二是利用 cc.loader 做動態加載。如何優雅的做音頻資源預加載呢?KUOKUO 通過一個小例子帶你學習。

正文

使用版本

CocosCreator 版本 2.2.2

明確目標

我們要做一個音頻資源加載模塊,與場景解耦,通過名稱獲取音頻資源。(預製體、圖片資源同理)如下圖,我們的目標是優雅的實現這些資源的加載。

單例模式

音頻資源加載模塊,全局一份即可,自然我們就想到了單例模式。實現單例很簡單,我們暴露出一個 getInstance 方法,始終返回一份實例,私有化構造函數,使得類無法被 new 即可!

/** 音樂資源管理單例 */
export class AudioClipManager {
    
    private static instance: AudioClipManager

    /** 構造函數私有化 */
    private constructor () { 
    }

    public static getInstance (): AudioClipManager {
        if (!this.instance) {
            this.instance = new AudioClipManager()
        }
        return this.instance
    }

}

預加載

單例寫好了,接下來就是預加載的方法了,cocos 中有一個 cc.loader.loadResDir 的方法能夠動態加載一個文件夾下的資源,我們分類好音頻資源後正好都在一個文件夾下即可。那我們先聲明下音頻資源的路徑,後期手動修改,或者你再寫一個修改路徑的方法都可以。

/** resources 下音樂文件夾目錄 */
private static audioClipsUrl: string = 'music'

然後讓我們寫一下加載代碼:

/** 緩存所有音頻資源 */
public preLoadAllAudioClips () {
    /** 加載代碼,參數爲 url,資源類型,進度回調,完成回調 */
    cc.loader.loadResDir(AudioClipManager.audioClipsUrl, cc.AudioClip, (completedCount, totalCount, item) => {

    }, (error, audioClips, urls) => {

    })
}

我們能夠獲取到加載的一些參數,讓我們計算下進度,豐富下代碼:

/** 緩存所有音頻資源 */
public preLoadAllAudioClips () {
    /** 加載代碼,參數爲 url,資源類型,進度回調,完成回調 */
    cc.loader.loadResDir(AudioClipManager.audioClipsUrl, cc.AudioClip, (completedCount, totalCount, item) => {
        // 計算進度
        let progress = (completedCount / totalCount) * 100
        // 打印一下
        cc.log(`緩存音頻資源中: ${completedCount}/${totalCount}`)
    }, (error, audioClips, urls) => {
        // 錯誤處理
        if (error) {
            cc.error(error)
            return
        }
        cc.log('緩存完畢!')
    })
}

新建個腳本使用一波:

import { AudioClipManager } from "./module/AudioClipManager"

const {ccclass, property} = cc._decorator

@ccclass
export default class Login extends cc.Component {

    audioClipManager: AudioClipManager

    onLoad () {
        this.audioClipManager = AudioClipManager.getInstance()
    }

    start () {
        this.audioClipManager.preLoadAllAudioClips()
    }

}

進度回調

我們已經能夠正常的使用了,但是在 Login 腳本中怎麼知道進度回調呢?簡單,寫個 callback !

/** 緩存所有音頻資源 */
public preLoadAllAudioClips (callback: (progress: number, isCompleted: boolean) => void) {
    /** 加載代碼,參數爲 url,資源類型,進度回調,完成回調 */
    cc.loader.loadResDir(AudioClipManager.audioClipsUrl, cc.AudioClip, (completedCount, totalCount, item) => {
        // 計算進度
        let progress = (completedCount / totalCount) * 100
        // 執行回調返回進度
        callback(progress, false)
        // 打印一下
        cc.log(`緩存音頻資源中: ${completedCount}/${totalCount}`)
    }, (error, audioClips, urls) => {
        // 錯誤處理
        if (error) {
            cc.error(error)
            return
        }
        // 執行回調返回進度
        callback(100, true)
        cc.log('緩存完畢!')
    })
}

在加載場景中使用:

this.audioClipManager.preLoadAllAudioClips((progress, isCompleted) => {
    if (isCompleted) {
        cc.log('預加載完成,進入遊戲')
        // 緩存完了,可以進入遊戲了
        cc.director.loadScene('game')
    } else {
        cc.log(`回調進度: ${progress}`)
    }
})

效果:

Map存儲

現在我們已經知道資源什麼時候被加載完畢了,那麼我們如何獲取這些資源呢?用Map!鍵值爲字符串資源名稱,方便獲取!

/** 存放音頻資源的 Map */
private audioClipMap: Map<string, cc.AudioClip> = new Map()

在預加載完畢的回調中有資源的數組:

// 獲取完畢後裝入 Map
audioClips.forEach(ele => {
    this.audioClipMap.set(ele.name, ele)
})

封裝一個獲取方法:

/** 獲取音頻資源 */
public getAudioClip (clipName: string): cc.AudioClip {
    if (!this.audioClipMap.has(clipName)) {
        cc.warn(`未緩存的音頻資源: ${clipName}`)
        return
    }
    return this.audioClipMap.get(clipName)
}

枚舉名稱

直接用音頻資源的名稱也是可以,但是不好維護,我們建個腳本,寫個枚舉列表。

/** 音樂的資源名稱枚舉 */
export enum MusicType {
    /** 背景音樂 */
    BGM = 'bgm',
    /** 點擊音效 */
    CLICK = 'click',
    /** 動作音效 */
    ACTION = 'action',
    /** 金幣音效 */
    COIN = 'getcoin',
    /** 遊戲結束音效 */
    GAME_OVER = 'gameover',
}

遊戲場景中試試效果(demo 裏一個 login 場景,一個 game 場景):

import { MusicType } from "./enum"
import { AudioClipManager } from "./module/AudioClipManager"

const {ccclass, property} = cc._decorator

@ccclass
export default class Game extends cc.Component {

    audioClipManager: AudioClipManager

    onLoad () {
        this.audioClipManager = AudioClipManager.getInstance()
    }

    start () {
        const bgmAudioClip = this.audioClipManager.getAudioClip(MusicType.BGM)
        cc.audioEngine.playMusic(bgmAudioClip, true)
    }

}

加一點細節

我們豐富一下方法,給出所有代碼:

/** 音樂資源管理單例 */
export class AudioClipManager {
    
    private static instance: AudioClipManager

    /** resources 下音樂文件夾目錄 */
    private static audioClipsUrl: string = 'music'

    /** 存放音頻資源的 Map */
    private audioClipMap: Map<string, cc.AudioClip> = new Map()

    /** 構造函數私有化 */
    private constructor () { 
    }

    public static getInstance (): AudioClipManager {
        if (!this.instance) {
            this.instance = new AudioClipManager()
        }
        return this.instance
    }

    /** 獲取音頻資源 */
    public getAudioClip (clipName: string): cc.AudioClip {
        if (!this.audioClipMap.has(clipName)) {
            cc.warn(`未緩存的音頻資源: ${clipName}`)
            return
        }
        return this.audioClipMap.get(clipName)
    }

    /** 獲取一部分音頻資源 */
    public getAudioClipsByArray (clipNames: string[]): cc.AudioClip[] {
        const audioClips: cc.AudioClip[] = []
        clipNames.forEach(clipName => {
            if (!this.audioClipMap.has(clipName)) {
                cc.warn(`未緩存的音頻資源: ${clipName}`)
                return
            }
            audioClips.push(this.audioClipMap.get(clipName))
        })
        return audioClips
    }

    /** 緩存所有音頻資源 */
    public preLoadAllAudioClips (callback: (progress: number, isCompleted: boolean) => void) {
        /** 加載代碼,參數爲 url,資源類型,進度回調,完成回調 */
        cc.loader.loadResDir(AudioClipManager.audioClipsUrl, cc.AudioClip, (completedCount, totalCount, item) => {
            // 計算進度
            let progress = (completedCount / totalCount) * 100
            // 執行回調返回進度
            callback(progress, false)
            // 打印一下
            cc.log(`緩存音頻資源中: ${completedCount}/${totalCount}`)
        }, (error, audioClips, urls) => {
            // 錯誤處理
            if (error) {
                cc.error(error)
                return
            }
            // 獲取完畢後裝入 Map
            audioClips.forEach(ele => {
                this.audioClipMap.set(ele.name, ele)
            })
            // 執行回調返回進度
            callback(100, true)
            cc.log('緩存完畢!')
        })
    }

    /** 單獨緩存一部分音頻 */
    public preloadAudioClipsByArray (clipNames: string[], callback: (progress: number, isCompleted: boolean) => void) {
        const urls = clipNames.map(clipName => `${AudioClipManager.audioClipsUrl}/${clipName}`)
        cc.loader.loadResArray(urls, cc.AudioClip, (completedCount, totalCount, item) => {
            let progress = completedCount / totalCount * 100
            cc.log(`緩存音頻資源中: ${completedCount}/${totalCount}`)
            callback(Math.floor(progress), false)
        }, (error, audioClips: cc.AudioClip[]) => {
            if (error) {
                cc.error(error)
                return
            }
            // 將預加載的所有音頻存入map
            audioClips.forEach(ele => {
                this.audioClipMap.set(ele.name, ele)
            })
            cc.log('緩存完畢!')
            callback(100, true)
        })
    }

    /** 釋放音頻資源 */
    public releaseAudioClipsByArray (clipNames: string[]) {
        clipNames.forEach(clipName => {
            if (!this.audioClipMap.has(clipName)) {
                cc.warn(`未緩存的音頻: ${clipName}`)
                return
            }
            cc.log(`釋放了音頻資源: ${clipName}`)
            cc.loader.releaseRes(`${AudioClipManager.audioClipsUrl}/${clipName}`, cc.AudioClip)
            this.audioClipMap.delete(clipName)
        })
    }

    /** 釋放所有音頻資源 */
    public releaseAllAudioClips () {
        cc.log('釋放了所有音頻資源')
        cc.loader.releaseResDir(AudioClipManager.audioClipsUrl, cc.AudioClip)
        this.audioClipMap.clear()
    }

}

結語

文章有沒有帶給你收穫呢!O(∩_∩)O~~

微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章