制作游戏音效
创建声音数据结构,在不同场景出现不同音源
在MainCamera
中有玩家能听到的音源,因此只需要保证在对应场景能正常切换不同音源即可。
因此需要一个专门管理音源的控制器AudioManager
,由于会出现环境音和背景音乐,因此在AudioManager
下增加两个音源的控制器Game Music
和Ambient Music
,在两个音源控制器上需要一个音源组件Audio Source
。
由于不同动作触发的音效不同,且音调等内容都需要修改,因此需要一个特别的数据结构来存放所有音源SoundDetails
除了管理音源的数据,还需要一个数据结构SceneSound
来处理不同情况下使用的音源(包括背景音乐和环境音)。
然后在切换场景的时候获得背景音等,然后调用播放函数
声音数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
[CreateAssetMenu(fileName = "SoundDetailsList_SO", menuName = "Sound/SoundDetailsList")]
public class SoundDetailsList_SO : ScriptableObject
{
public List<SoundDetails> soundDetailsList;
public SoundDetails GetSoundDetails(SoundName soundName)
{
return soundDetailsList.Find(s => s.soundName == soundName);
}
}
[System.Serializable]
public class SoundDetails
{
public SoundName soundName;
public AudioClip soundClip;
/* 音源的特殊设定 */
[UnityEngine.Range(0.1f, 1.5f)]
public float soundPitchMin;
[UnityEngine.Range(0.1f, 1.5f)]
public float soundPitchMax;
[UnityEngine.Range(0.1f, 1f)]
public float soundVolume;
}
|
以上数据结构用于存放每个音乐名对应的音乐信息,即声音片段
场景音乐管理数据结构
该数据结构用于管理每个场景需要的所有音乐信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
[CreateAssetMenu(fileName = "SceneSoundList_SO", menuName = "Sound/SceneSoundList")]
public class SceneSoundList_SO : ScriptableObject
{
public List<SceneSoundItem> sceneSoundItemList;
public SceneSoundItem GetSceneSoundItem(string sceneName)
{
return sceneSoundItemList.Find(s => s.sceneName == sceneName);
}
}
[System.Serializable]
public class SceneSoundItem
{
[SceneName]public string sceneName;
public SoundName ambientMusic;
public SoundName sceneMusic;
}
|
音乐管理控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
public class AudioManager : MonoBehaviour
{
[Header("音乐库")] public SoundDetailsList_SO soundDetailsData;
public SceneSoundList_SO sceneSoundData;
public AudioSource ambientSource;
public AudioSource gameSource;
private float MusicStartSecond => Random.Range(3f, 5f); /* 播放音乐的等待时间 */
private Coroutine _soundRoutine;
private void OnEnable()
{
EventHandler.AfterSceneLoadEvent += OnAfterSceneLoadEvent;
}
private void OnDisable()
{
EventHandler.AfterSceneLoadEvent -= OnAfterSceneLoadEvent;
}
#region 注册事件
private void OnAfterSceneLoadEvent()
{
/* 切换场景并获得当前场景的对应音乐 */
string sceneName = SceneManager.GetActiveScene().name;
SceneSoundItem sceneSound = sceneSoundData.GetSceneSoundItem(sceneName);
if (sceneSound == null) return;
SoundDetails ambient = soundDetailsData.GetSoundDetails(sceneSound.ambientMusic);
SoundDetails music = soundDetailsData.GetSoundDetails(sceneSound.sceneMusic);
if (_soundRoutine != null)
StopCoroutine(_soundRoutine);
_soundRoutine = StartCoroutine(PlaySoundRoutine(music, ambient));
}
#endregion
#region 播放音乐
private IEnumerator PlaySoundRoutine(SoundDetails music, SoundDetails ambient)
{
if (music != null && ambient != null)
{
PlayAmbientClip(ambient);
yield return new WaitForSeconds(MusicStartSecond);
PlayMusicClip(music);
}
}
private void PlayMusicClip(SoundDetails soundDetails)
{
gameSource.clip = soundDetails.soundClip;
if (gameSource.isActiveAndEnabled)
gameSource.Play();
}
private void PlayAmbientClip(SoundDetails soundDetails)
{
ambientSource.clip = soundDetails.soundClip;
if (ambientSource.isActiveAndEnabled)
ambientSource.Play();
}
#endregion
|
在该管理代码中,主要是需要通过获得当前的场景信息来获取场景需要的音乐文件,然后将这些音乐文件赋值给音源管理组件,然后执行播放功能。
在该部分使用了协程来保证切换场景后声音的等待时间出现。在此处如果要关闭一个特定的协程,需要先定义一个协程变量,然后关闭该协程,而生成则是直接赋值。
1
2
3
|
if (_soundRoutine != null)
StopCoroutine(_soundRoutine);
_soundRoutine = StartCoroutine(PlaySoundRoutine(music, ambient));
|
后续将通过额外的功能来无缝衔接音乐
使用AudioMixer实现音效控制和转换
打开AudioMixer面板
Window栏->AudioMixer
创建AudioMixer
在项目窗口中右键创建
AudioMixer内容
Snapshots
可以作为某组音量内容的整体方案
Group
用于管理音轨,子音轨收到父音轨管理
如果需要通过代码控制音轨,则需要在音轨的Inspector
中将音轨暴露出来,然后再音源中选择指定的音轨输出。
然后在AudioManager.cs
中使用AudioMixer
组件来控制具体的音轨的数值
代码修改部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private void PlayMusicClip(SoundDetails soundDetails, float transitionTime)
{
gameAudioMixer.SetFloat("MusicVolume", ConvertSoundVolume(soundDetails.soundVolume));
gameSource.clip = soundDetails.soundClip;
if (gameSource.isActiveAndEnabled)
gameSource.Play();
/* 切换到当前快照需要的时间 */
normalSnapshot.TransitionTo(transitionTime);
}
private void PlayAmbientClip(SoundDetails soundDetails, float transitionTime)
{
gameAudioMixer.SetFloat("AmbientVolume", ConvertSoundVolume(soundDetails.soundVolume));
ambientSource.clip = soundDetails.soundClip;
if (ambientSource.isActiveAndEnabled)
ambientSource.Play();
ambientSnapshot.TransitionTo(transitionTime);
}
|
单个音效的增加
为了增加单个音效的,不能为每一个prefab
添加音源,因此建立一个能够存放音源GO的线程池,每当得到一个GO的时候播放特定的音源。
由于目前Unity自带对象池get音源prefab
的方式有BUG,必须第二次get才能播放音效,因此需要使用传统的创建对象池的方法,即自己编写代码获取线程
创建音源预制体
创建音源预制体后,为其增加初始化控制代码
1
2
3
4
5
6
7
8
9
10
11
12
|
[RequireComponent(typeof(AudioSource))]
public class Sound : MonoBehaviour
{
[SerializeField] private AudioSource audioSource;
public void SetAudio(SoundDetails soundDetails)
{
audioSource.clip = soundDetails.soundClip;
audioSource.volume = AudioManager.Instance.ConvertSoundVolume(soundDetails.soundVolume);
audioSource.pitch = Random.Range(soundDetails.soundPitchMin, soundDetails.soundPitchMax);
}
}
|
此时的audioSource
是当前预制体自己的音源组件
然后将该GO作为预制体存入PoolManager
中,然后使用一个新的事件当触发播放音效的时候就获得线程池中的GO,并初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// PoolManager.cs
/* 有BUG的方法 */
private void InitSoundEffect(ParticleEffectType effectType, SoundDetails soundDetails)
{
if (_poolEffectDic.ContainsKey(effectType.ToString()))
{
ObjectPool<GameObject> soundPool = _poolEffectDic[effectType.ToString()];
var soundObj = soundPool.Get();
soundObj.GetComponent<Sound>().SetAudio(soundDetails);
StartCoroutine(ReleaseSoundRoutine(soundPool, soundObj, soundDetails));
}
}
private IEnumerator ReleaseSoundRoutine(ObjectPool<GameObject> pool,GameObject soundObj, SoundDetails soundDetails)
{
yield return new WaitForSeconds(soundDetails.soundClip.length); // 音乐长度
pool.Release(soundObj);
}
|
设置事件触发
建立事件
1
2
3
4
5
6
7
8
|
// EventHandle.cs
public static event Action<ParticleEffectType, SoundDetails> InitSoundEffectEvent;
public static void CallInitSoundEffectEvent(ParticleEffectType particleEffectType, SoundDetails soundDetails)
{
InitSoundEffectEvent?.Invoke(particleEffectType, soundDetails);
}
|
为Cropdetails
设置音效名称,然后通过SoundManager.cs
获得音效具体数据
1
2
|
// dataCollection.cs
[Space] [Header("音效名称")] public SoundName soundName;
|
为每一种作物添加音效(由于作物,石头,杂草的处理是分开的,因此添加音效也要在不同地方
1
2
3
4
5
6
7
8
|
// Crop.cs
/*音效*/
if (currentCropDetails.soundName != SoundName.None)
{
// 更新后的内容 EventHandler.CallPlaySoundEvent(currentCropDetails.soundName);
var soundDetails = AudioManager.Instance.soundDetailsData.GetSoundDetails(currentCropDetails.soundName);
EventHandler.CallInitSoundEffectEvent(ParticleEffectType.Sound, soundDetails);
}
|
代码完成创建线程池的工作(解决Unity自带线程池问题)
使用队列的方式,预先建造一个含有20个音源对象的队列,每次都从该队列中获取音源预制体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
/* 使用队列自建线程池 */
/*初始化对象池*/
private void CreatSoundPool()
{
var soundParent = new GameObject(ParticleEffectType.Sound.ToString()).transform;
soundParent.SetParent(transform);
for (int i = 0; i < 20; i++)
{
GameObject obj = Instantiate(prefabList.Find(p => p.name == ParticleEffectType.Sound.ToString()), soundParent);
obj.SetActive(false);
_soundQueue.Enqueue(obj);
}
}
/*对象元素获取方法*/
private GameObject GetSoundPoolObj()
{
if (_soundQueue.Count < 2)
CreatSoundPool();
return _soundQueue.Dequeue();
}
private void InitSoundEffect(ParticleEffectType effectType, SoundDetails soundDetails)
{
if (_poolEffectDic.ContainsKey(effectType.ToString()))
{
var soundObj = GetSoundPoolObj();
soundObj.GetComponent<Sound>().SetAudio(soundDetails);
soundObj.SetActive(true);
StartCoroutine(ReleaseSoundObj(soundObj, soundDetails.soundClip.length));
}
}
private IEnumerator ReleaseSoundObj(GameObject obj, float duration)
{
yield return new WaitForSeconds(duration);
obj.SetActive(false);
_soundQueue.Enqueue(obj);
}
|
为人物动画帧添加事件,然后该事件调用一个单独播放音效的事件,该事件由AudioManager
注册,在该注册函数中会获取当前单独播放音效事件声音名称,然后获取音效内容,并呼叫真正的音效初始化事件(PoolManager注册
)
人物的动画事件
1
2
3
4
5
6
7
|
public class AnimationEvent : MonoBehaviour
{
public void FootStepSound()
{
EventHandler.CallPlaySoundEvent(SoundName.FootStepSoft);
}
}
|
直接播放音效事件的注册,获得音效名称后,调用CallInitSoundEffectEvent
事件,让PoolManager
创建音效事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// AudioManager.cs
/// <summary>
/// 直接播放音效事件
/// </summary>
/// <param name="soundName">音效名称</param>
private void OnPlaySoundEvent(SoundName soundName)
{
SoundDetails soundDetails = soundDetailsData.GetSoundDetails(soundName);
if (soundDetails is null)
return;
EventHandler.CallInitSoundEffectEvent(ParticleEffectType.Sound, soundDetails);
}
|