音频文件处理

在音频文件的导入设置中,Default选项卡内,设置Load Type。

比较长的背景音,选择Stream模式。比较短的音效,选择Decompress On Load。一般情况下不选Composed In Memory。

在Decompress On Load模式下,关闭PreLoad Audio Data。

AudioSetting

在Editor文件夹中新建AudioSetting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEditor;
using UnityEngine;

public class AudioSetting : AssetPostprocessor
{
private void OnPostprocessAudio(AudioClip clip)
{
AudioImporter audioImporter = assetImporter as AudioImporter;
AudioImporterSampleSettings customSetting = new AudioImporterSampleSettings();
if (clip.length < 1)//播放时长小于1秒
{
customSetting.loadType = AudioClipLoadType.DecompressOnLoad;
}
else
{
customSetting.loadType = AudioClipLoadType.Streaming;
}
audioImporter.defaultSampleSettings = customSetting;
audioImporter.preloadAudioData = false;
}
}

Pathes

Pathes添加音频文件路径

1
2
3
4
5
6
7
8
//音频文件路径
public const string AUDIO_FOLDER = "Audio";
public const string AUDIO_UI_FOLDER = AUDIO_FOLDER + "/UI/";
public const string AUDIO_PLAYER_FOLDER = AUDIO_FOLDER + "/Player/";
public const string AUDIO_GAME_BG = AUDIO_FOLDER + "/Game_BG";
public const string AUDIO_CLICK_BUTTON = AUDIO_UI_FOLDER + "UI_ClickButton";
public const string AUDIO_LOADING = AUDIO_UI_FOLDER + "UI_Loading";
public const string AUDIO_START_GAME = AUDIO_UI_FOLDER + "UI_StartGame";

将之前的Path中命名都按照类型修改名称,比如将之前的Prefab的路径名都添加上PREFAB前缀,将之前的Config路径名都添加上CONFIG前缀,这里略。

这里的AUDIO_FOLDER路径后面没有斜杠,是为了后面的JsonDataTool配置工具使用

Enums

添加音频名的枚举

1
2
3
4
5
6
7
8
9
10
public enum BGAudio
{
Game_BG
}
public enum UIAudio
{
UI_ClickButton,
UI_Loading,
UI_StartGame
}

Player的音频名直接使用Hero枚举即可。

DataKeys

添加音频配置文件中的Key值。是为了后面读取配置文件用。详见下面JsonDataToolAudioVolume

1
2
public const string AUDIO_NAME = "Name";
public const string AUDIO_VOLUME = "Volume";

AudioMgr

AudioSource组件是可以重复挂载的。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AudioMgr : LazyMonoSing<AudioMgr>, IInit
{
private readonly Dictionary<string,AudioClip> _clips = new();
private AudioSource _audioSource;//用于播放背景音乐的AudioSource
private List<AudioSource> _activeSources = new();//激活状态的AudioSource池
private List<AudioSource> _inactiveSources = new();//未激活状态的AudioSource池
private readonly Dictionary<string,AudioSource> _clipAndSourceMap = new();//放在AudioSource池中循环播放的clip的对应的Source的Map
private readonly Dictionary<string,float> _volumes = new();//AudioVolume.json中每个音频和对应的volume的缓存。
private Action _changeVolume;//所有需要获取音量的操作的回调
private readonly float _defaultVolume = 0.5f;
private int _cyclingId;//正在回收的source的ID
public void Init()
{
_changeVolume = null;
_cyclingId = -1;
AudioClip[] clips = LoadMgr.Instance.LoadAll<AudioClip>(Pathes.AUDIO_FOLDER);
foreach (var clip in clips)
{
_clips.Add(clip.name, clip);
}
_audioSource = gameObject.AddComponent<AudioSource>();
IReader reader = ReaderMgr.Instance.GetReader(Pathes.AUDIO_VOLUME_CONFIG);
reader.Count(count =>
{
for (int i = 0; i < count; i++)
{
TaskQueueMgr.Instance.AddReaderQueue<string>(() => reader[i][DataKeys.AUDIO_NAME]);
TaskQueueMgr.Instance.AddReaderQueue<float>(() => reader[i][DataKeys.AUDIO_VOLUME]);
TaskQueueMgr.Instance.Execute(datas =>
{
_volumes.Add((string)datas[0], (float)datas[1]);
});
}
_changeVolume?.Invoke();//执行所有设置音量的方法
_changeVolume = null;
});
}
private void SetVolume(string name, AudioSource source)
{
if (_volumes.Count == 0)//如果当前还没读取完音量配置,全部放在_changeVolume回调中缓存
{
_changeVolume += () => source.volume = GetVolumeValue(name);
}
else
{
source.volume = GetVolumeValue(name);
}
}
private float GetVolumeValue(string name)
{
if (_volumes.TryGetValue(name, out var volume))
return volume;
else
{
Debug.LogError("配置中没有对应的音量信息:name: " + name);
return _defaultVolume;
}
}
public AudioClip GetClip(string name)
{
if(_clips.TryGetValue(name, out AudioClip clip)) { return clip; }
else
{
Debug.LogError("无法找到当前音频文件:" + name);
return null;
}
}
public void PlayBG(BGAudio audio)
{

_audioSource.clip = GetClip(audio.ToString());
SetVolume(audio.ToString(), _audioSource);
_audioSource.loop = true;
_audioSource.Play();

}
public void PlayOnce(string name)
{
var clip = GetClip(name);
_audioSource.PlayOneShot(clip, GetVolumeValue(name));//TODO: 音效音量没有在回调中设置
}
public void Play(string name, bool isLoop = false)
{
AudioSource source = GetSource();
AudioClip clip = GetClip(name);
source.clip = clip;
SetVolume(name, source);
source.loop = isLoop;
_clipAndSourceMap.Add(name, source);
source.Play();

_cyclingId = CoroutineMgr.Instance.RegisterCoroutine(WaitToRecycle(name));
}
private IEnumerator WaitToRecycle(string name)
{
var clip = GetClip(name);
yield return new WaitForSeconds(clip.length);
Stop(name);
}
public void Stop(string name)
{
if (_clipAndSourceMap.TryGetValue(name, out AudioSource source))
{
source.Stop();
if (_cyclingId >= 0)
{
CoroutineMgr.Instance.StopCo(_cyclingId);
_cyclingId = -1;
}
source.clip = null;
_activeSources.Remove(source);
_inactiveSources.Add(source);
_clipAndSourceMap.Remove(name);
}
}
private AudioSource GetSource()
{
AudioSource source;
if (_inactiveSources.Count > 0)
{
source = _inactiveSources[0];
_inactiveSources.RemoveAt(0);

}
else
{
source = gameObject.AddComponent<AudioSource>();
}
_activeSources.Add(source);
return source;
}
}

AudioSource.PlayOneShot方法不会影响一个AudioSource正在播放的音乐,适合播放临时音效。

使用两个List<AudioSource>来用一种类似对象池的方式来缓存所有需要重复播放的音效。

加载音量配置的操作在下一节介绍

这里的AudioMgr并不完善,后面会修改

LifeCycleAddConfig

LifeCycleAddConfig中添加AudioMgr

1
2
3
4
5
6
private void Add()
{
//...
//优先级2
Objects.Add(AudioMgr.Instance);
}

StartController

在其中播放游戏开始时的背景音乐

1
2
3
4
5
protected override void InitChild()
{
//...
AudioMgr.Instance.PlayBG(BGAudio.Game_BG);
}

音频音量配置

我们要为每一个音频的音量生成配置,在配置里指定单个音频的音量大小。

Pathes中添加生成的音频音量json配置文件的路径,同时修改AUDIO_FOLDER

1
public static readonly string AUDIO_VOLUME_CONFIG = CONFIG_FOLDER + "/AudioVolume.json";

Editor文件夹内新建JsonDataTool

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using LitJson;
using System.Linq;

public class JsonDataTool
{
[MenuItem("Assets/CreateAudioCfgJson")]
private static void CreateJson()
{
string[] guids = Selection.assetGUIDs;
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
AudioJson(path);
}
private static void AudioJson(string selectedPath)
{
if (!selectedPath.EndsWith(Pathes.AUDIO_FOLDER))
{
return;
}

DirectoryInfo directoryInfo = new(selectedPath);
FileInfo[] fileInfos = directoryInfo.GetFiles("*", SearchOption.AllDirectories);

string path = Pathes.AUDIO_VOLUME_CONFIG;
List<AudioVolume> volumes = new();
if (File.Exists(path))
{
AudioVolume[] volumeCfg = JsonMapper.ToObject<AudioVolume[]>(File.ReadAllText(path));

foreach (var file in fileInfos)
{
if (file.Name.EndsWith(".meta"))
{
continue;
}
string fileName = Path.GetFileNameWithoutExtension(file.Name);
AudioVolume temp = new()
{
Name = fileName,
Volume = GetVolume(volumeCfg, fileName)
};
volumes.Add(temp);
}
}
else
{
foreach(var file in fileInfos)
{
if (file.Name.EndsWith(".meta"))
{
continue;
}
string fileName = Path.GetFileNameWithoutExtension(file.Name);
AudioVolume temp = new()
{
Name = fileName,
Volume = 0.5
};
volumes.Add(temp);
}
}

string json = JsonMapper.ToJson(volumes);


File.WriteAllText(path, json);
Debug.Log("成功生成Volume配置文件");

AssetDatabase.Refresh();
}
private static double GetVolume(AudioVolume[] audioVolumes, string key)
{
AudioVolume targetVolume = audioVolumes.Where(u => u.Name == key).FirstOrDefault();
if (targetVolume == null)
{
return 0.5f;
}
return targetVolume.Volume;
}
}

public class AudioVolume
{
public string Name { get; set; }
public double Volume { get; set; }//LitJson只支持Double
}

为了防止配置新的音频文件后,旧的音频配置会被修改,所以我们需要每次都读取AudioVolume.json文件(如果有的话),把旧的配置保留。

然后选中Assets——Resources——Audio文件夹——右键——CreateJson,就可以生成音频配置文件了。

界面和UI音效实现

开始界面

开始界面除了播放背景音,还要添加点击开始按钮时的音效,修改StartController

1
2
3
4
5
6
7
8
9
protected override void InitChild()
{
transform.ButtonAction("Start",() =>
{
UIMgr.Instance.Show(Pathes.SELECT_HERO_VIEW);
AudioMgr.Instance.PlayOnce(UIAudio.UI_StartGame.ToString());
});
AudioMgr.Instance.PlayBG(BGAudio.Game_BG);
}

选择英雄界面

选中每个英雄时,会播放一段语音,此时选中其他英雄,会中断语音的播放,修改HeroItemController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HeroItemController : ControllerBase
{
//...
private void Selected()
{
if (GameStateModel.Instance.SelectedHero != _hero)
{
GameStateModel.Instance.SelectedHero = _hero;
AudioMgr.Instance.Play(_hero.ToString());
}
}
public override void UpdateFun()
{
base.UpdateFun();
if(_hero != GameStateModel.Instance.SelectedHero)
{
AudioMgr.Instance.Stop(_hero.ToString());
}
}
}

默认Button音效

使用ExtendUtil来添加默认音效,修改ButtonAction方法并添加useDefaultAudio参数

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
public static void ButtonAction(this Transform target, string path, Action action, bool useDefaultAudio = true)
{
var go = target.Find(path);
if (go != null)
{
if (go.TryGetComponent<Button>(out var button))
{
button.onClick.AddListener(()=> action());
if (useDefaultAudio)//+++
{
button.onClick.AddListener(AddButtonAudio);
}
}
else
{
Debug.LogError("获取Button组件失败:" + path);
}
}
else
{
Debug.LogError("查找transform失败:" + path);
}
}
private static void AddButtonAudio()//+++
{
AudioMgr.Instance.PlayOnce(UIAudio.UI_ClickButton.ToString());
}

此时修改StartController,它的按钮不需要默认音效

1
2
3
4
5
6
7
8
9
protected override void InitChild()
{
transform.ButtonAction("Start",() =>
{
UIMgr.Instance.Show(Pathes.SELECT_HERO_VIEW);
AudioMgr.Instance.PlayOnce(UIAudio.UI_StartGame.ToString());
}, false);//+++
AudioMgr.Instance.PlayBG(BGAudio.Game_BG);
}