在实际游戏开发中,音效既是一个相对独立的部分,又与其他游戏逻辑密切关联。也就是说,与音效相关的代码会插入很多细节代码中。
而且在音效非常丰富的情况下,如果每一个游戏模块都单独播放音效,那么可能会带来一些问题。例如,Audio Source组件很多,但大部分限制,同时播放的音效太多会显得混乱。
成熟的技术开发思路是:如果音效不多、没有造成问题,则完全可以简单处理;如果音效已经引起了代码的混乱和性能问题,就有必要统一管理所有的音源和音效,也就是设计一个易用的音频管理器。有了音频管理器,所有的Audio Source组件都会统一创建,而所有音效播放的需求都要通过调用音频管理器的方法简介实现。
音频管理器有很多设计思路,其中一种比较简洁的思路是,事先指定游戏中最多同时播放多少个音频,然后创建若干个音源。例如,最多播放8个音频,那么就创建8个Audio Source组件,这8个组件可以看作8个频道。需要播放音频时,只要找到任意一个空闲的频道播放即可;而如果8个频道都正在播放,那么就可以用某种策略替换音频(如将播放时间最早的音频替换成新的音频)。用这种简单的思路创建音频管理器的代码如下
using System.Collections; using System.Collections.Generic; using UnityEngine; //音频管理器 public class AudioManager : MonoBehaviour { // 整个游戏中,总的音源数量 private const int AUDIO_CHANNEL_NUM = 8; private struct CHANNEL { public AudioSource channel; public float keyOnTime; //记录最近一次播放音乐的时刻 }; private CHANNEL[] m_channels; void Awake() { m_channels = new CHANNEL[AUDIO_CHANNEL_NUM]; for (int i = 0; i < AUDIO_CHANNEL_NUM; i++) { //每个频道对应一个音源 m_channels[i].channel = gameObject.AddComponent<AudioSource>(); m_channels[i].keyOnTime = 0; } } //公开方法:播放一次,参数为音频片段、音量、左右声道、速度 //这个方法主要用于音效,因此考虑了音效顶替的逻辑 public int PlayOneShot(AudioClip clip, float volume, float pan, float pitch = 1.0f) { for (int i = 0; i < m_channels.Length; i++) { //如果正在播放同一个片段,而且刚刚才开始,则直接退出函数 if (m_channels[i].channel.isPlaying && m_channels[i].channel.clip == clip && m_channels[i].keyOnTime >= Time.time - 0.03f) return -1; } //遍历所有频道,如果有频道空闲直接播放新音频,并退出 //如果没有空闲频道,先找到最开始播放的频道(oldest),稍后使用 int oldest = -1; float time = 10000000.0f; for (int i = 0; i < m_channels.Length; i++) { if (m_channels[i].channel.loop==false && m_channels[i].channel.isPlaying && m_channels[i].keyOnTime < time) { oldest = i; time=m_channels[i].keyOnTime; } if (!m_channels[i].channel.isPlaying) { m_channels[i].channel.clip=clip; m_channels[i].channel.volume=volume; m_channels[i].channel.pitch=pitch; m_channels[i].channel.panStereo=pan; m_channels[i].channel.loop = false; m_channels[i].channel.Play(); m_channels[i].keyOnTime = Time.time; return i; } } //运行到这里说明没有空闲频道。让新的音频顶替最早播出的音频 if(oldest>=0) { m_channels[oldest].channel.clip = clip; m_channels[oldest].channel.volume = volume; m_channels[oldest].channel.pitch = pitch; m_channels[oldest].channel.panStereo = pan; m_channels[oldest].channel.loop = false; m_channels[oldest].channel.Play(); m_channels[oldest].keyOnTime = Time.time; return oldest; } return -1; } //公开方法:循环播放,用于播放长时间的背景音乐,处理方式相对简单一些 public int PlayLoop(AudioClip clip, float volume, float pan, float pitch = 1.0f) { for(int i = 0; i < m_channels.Length; i++) { if (!m_channels[i].channel.isPlaying) { m_channels[i].channel.clip = clip; m_channels[i].channel.volume = volume; m_channels[i].channel.pitch = pitch; m_channels[i].channel.panStereo = pan; m_channels[i].channel.loop = true; m_channels[i].channel.Play(); m_channels[i].keyOnTime = Time.time; return i; } } return -1; } //公开方法:停止所有音频 public void StopAll() { foreach(CHANNEL channel in m_channels) channel.channel.Stop(); } //公开方法:根据频道ID停止音频 public void Stop(int id) { if (id>= 0&& id < m_channels.Length){ m_channels[id].channel.Stop(); } } }
以上代码可以作为创建音频管理器的一种思路参考。
实际上,根据游戏类型的不同,音频管理器的创建思路也有区别。例如,在很多3D游戏中,需要考虑音效播放的空间位置(目的是营造真实感),这是统一创建音源就不是很合适了