本文前面说说问题和解决思路,有点长,可以直接跳到后面的代码部分。
文档中是使用 AudioSource 的 playOneShot 播放音效,在 web 调试时一切正常,但是打包成小游戏后就发现了两个问题:
1、音频播放存在延迟,真机环境上,iOS 的延迟不明显,并不影响体验;但是安卓的延迟就严重到不可容忍的程度了;
2、游戏进行一段时间后,会出现 “Too many InnerAudioContext instance.” 的 warning。
如果音效播放不是很频繁,这两个问题并不突出,但是需要频繁的实时音效时这个问题就无法忍受了,游戏月初上线,至今将近一个月这个问题都困扰着我,同时我也发现,其他使用 cocoscreator 开发的,有实时音效的微信小游戏也有类似音效延迟甚至丢失的问题,看来这不是一个孤例。
今天我决定一鼓作气彻底修复这个问题。先分析问题,微信小游戏的代码已经经过压缩,很难被人读懂,先从已知的信息入手吧。
从问题 2 的 warning 提示来看,问题点在于 cocoscreator 打包成小游戏代码以后,每一次播放音频都会重新创建音频播放上下文实例,而在音效播放完毕后并没有回收,导致过度占用内存;同时结合微信小游戏文档中的说明,上下文实例创建过程中需要重新加载音频文件,这也就是问题 1 音效延迟的原因。
这么梳理下来问题就很清晰了,问题是出在 playOneShot 方法上,打包成小游戏以后这个方法每次调用都会重新创建一个音频播放上下文实例,并且实例播放完毕并没有释放。
解决思路也逐渐成行:使用 AudioSource 组件,clip 固定设置为要播放的音效,关闭 loop,只需要要在播放某个音效的时候找到存放该音效的 AudioSource 组件并调用 play 方法就好了。
照着这个思路我重新写了一个 AudioManager 类,代码如下:
import { AudioClip, AudioSource, Node, resources } from "cc";
export class AudioManager {
private container: Node = null;
private audioSources: {[key: string]: AudioSource[]} = {};
public constructor(containerNode: Node) {
this.container = containerNode;
}
public async playSound(name: string) {
const audioSource: AudioSource = await this.findIdleAudioSource(name);
if (audioSource) {
audioSource.play();
}
}
private async findIdleAudioSource(name: string): Promise<AudioSource> {
if (!this.audioSources[name]) {
this.audioSources[name] = [];
await this.creatAudioSource(name);
}
for (let i = 0; i < this.audioSources[name].length; i++) {
const audioSource: AudioSource = this.audioSources[name][i];
if (!audioSource.playing) {
return audioSource;
}
}
if (this.audioSources[name].length < 10) {
return await this.creatAudioSource(name);
}
return null;
}
private creatAudioSource(name: string): Promise<AudioSource> {
return new Promise((resolve, reject) => {
resources.load('audios/' + name, AudioClip, (err, clip: AudioClip) => {
const audioSource: AudioSource = this.container.addComponent(AudioSource);
audioSource.playOnAwake = false;
audioSource.loop = false;
audioSource.volume = 1;
audioSource.clip = clip;
this.audioSources[name].push(audioSource);
resolve(audioSource);
});
});
}
}
我没有使用单例,因为我的音效只在游戏场景中使用,所以直接用了一普通类供音频管理组件调用。大家也可以根据实际情况具体问题具体分析。
该类里我维护了一个 audioSources 属性,用于存放不同的 AudioSource 实例。当 playSound 方法被调用,如果是新的音效,则会创建一个新的 AudioSource,同时我也允许在当前音效没有空闲的 AudioSource 时创建新的 AudioSource,最高限制十个,用于避免因为上一个音效未播放完导致下一个音效无法正常播放的情况。
问题得到了完美解决,再也不存在音效延迟的问题了(当然首次播放依然有少许延迟,但是在可忍受范围内,如果这个问题无法忍受的话,也可以考虑增加预加载机制),内存占用也被降低了。
还是希望官方能够尽早优化 playOneShot 方法,末了打个小广告,我做的两个小游戏,欢迎各位大佬提点建议:
2048Q弹数字球

郝小贱的穿越历险:



