音频文件处理
在音频文件的导入设置中,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) { 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值。是为了后面读取配置文件用。详见下面JsonDataTool
的AudioVolume
类
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; private List<AudioSource> _activeSources = new(); private List<AudioSource> _inactiveSources = new(); private readonly Dictionary<string,AudioSource> _clipAndSourceMap = new(); private readonly Dictionary<string,float> _volumes = new(); private Action _changeVolume; private readonly float _defaultVolume = 0.5f; private int _cyclingId; 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 += () => 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)); } 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() { 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; } }
|
为了防止配置新的音频文件后,旧的音频配置会被修改,所以我们需要每次都读取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()); } } }
|
使用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); }
|