From 0e9656b3b82714bac4133244c9d3871a56955b53 Mon Sep 17 00:00:00 2001 From: Machillka Date: Tue, 4 Nov 2025 19:16:31 +0800 Subject: [PATCH] Add: Audio Manager - play, stop audio clip - set volume --- .vscode/settings.json | 140 +++++----- .../Framework/Sample/Scenes/SampleScene.unity | 2 +- .../Scripts/Utilities/Audio Manager.meta | 8 + .../Utilities/Audio Manager/AudioEntry.cs | 60 +++++ .../Audio Manager/AudioEntry.cs.meta | 2 + .../Utilities/Audio Manager/AudioHandler.cs | 42 +++ .../Audio Manager/AudioHandler.cs.meta | 2 + .../Utilities/Audio Manager/AudioManager.cs | 251 ++++++++++++++++++ .../Audio Manager/AudioManager.cs.meta | 2 + .../Utilities/Audio Manager/IAudioManager.cs | 14 + .../Audio Manager/IAudioManager.cs.meta | 2 + 11 files changed, 454 insertions(+), 71 deletions(-) create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager.meta create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs.meta create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs.meta create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs.meta create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs create mode 100644 Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs.meta diff --git a/.vscode/settings.json b/.vscode/settings.json index a999920..f69c966 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,71 +1,71 @@ { - "files.exclude": { - "**/.DS_Store": true, - "**/.git": true, - "**/.vs": true, - "**/.gitmodules": true, - "**/.vsconfig": true, - "**/*.booproj": true, - "**/*.pidb": true, - "**/*.suo": true, - "**/*.user": true, - "**/*.userprefs": true, - "**/*.unityproj": true, - "**/*.dll": true, - "**/*.exe": true, - "**/*.pdf": true, - "**/*.mid": true, - "**/*.midi": true, - "**/*.wav": true, - "**/*.gif": true, - "**/*.ico": true, - "**/*.jpg": true, - "**/*.jpeg": true, - "**/*.png": true, - "**/*.psd": true, - "**/*.tga": true, - "**/*.tif": true, - "**/*.tiff": true, - "**/*.3ds": true, - "**/*.3DS": true, - "**/*.fbx": true, - "**/*.FBX": true, - "**/*.lxo": true, - "**/*.LXO": true, - "**/*.ma": true, - "**/*.MA": true, - "**/*.obj": true, - "**/*.OBJ": true, - "**/*.asset": true, - "**/*.cubemap": true, - "**/*.flare": true, - "**/*.mat": true, - "**/*.meta": true, - "**/*.prefab": true, - "**/*.unity": true, - "build/": true, - "Build/": true, - "Library/": true, - "library/": true, - "obj/": true, - "Obj/": true, - "Logs/": true, - "logs/": true, - "ProjectSettings/": true, - "UserSettings/": true, - "temp/": true, - "Temp/": true - }, - "files.associations": { - "*.asset": "yaml", - "*.meta": "yaml", - "*.prefab": "yaml", - "*.unity": "yaml" - }, - "explorer.fileNesting.enabled": true, - "explorer.fileNesting.patterns": { - "*.sln": "*.csproj" - }, - "dotnet.defaultSolution": "Framework.sln", - "git.ignoreLimitWarning": true -} + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.vs": true, + "**/.gitmodules": true, + "**/.vsconfig": true, + "**/*.booproj": true, + "**/*.pidb": true, + "**/*.suo": true, + "**/*.user": true, + "**/*.userprefs": true, + "**/*.unityproj": true, + "**/*.dll": true, + "**/*.exe": true, + "**/*.pdf": true, + "**/*.mid": true, + "**/*.midi": true, + "**/*.wav": true, + "**/*.gif": true, + "**/*.ico": true, + "**/*.jpg": true, + "**/*.jpeg": true, + "**/*.png": true, + "**/*.psd": true, + "**/*.tga": true, + "**/*.tif": true, + "**/*.tiff": true, + "**/*.3ds": true, + "**/*.3DS": true, + "**/*.fbx": true, + "**/*.FBX": true, + "**/*.lxo": true, + "**/*.LXO": true, + "**/*.ma": true, + "**/*.MA": true, + "**/*.obj": true, + "**/*.OBJ": true, + "**/*.asset": true, + "**/*.cubemap": true, + "**/*.flare": true, + "**/*.mat": true, + "**/*.meta": true, + "**/*.prefab": true, + "**/*.unity": true, + "build/": true, + "Build/": true, + "Library/": true, + "library/": true, + "obj/": true, + "Obj/": true, + "Logs/": true, + "logs/": true, + "ProjectSettings/": true, + "UserSettings/": true, + "temp/": true, + "Temp/": true + }, + "files.associations": { + "*.asset": "yaml", + "*.meta": "yaml", + "*.prefab": "yaml", + "*.unity": "yaml" + }, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.sln": "*.csproj" + }, + "dotnet.defaultSolution": "Unity-Framework.sln", + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/Assets/Framework/Sample/Scenes/SampleScene.unity b/Assets/Framework/Sample/Scenes/SampleScene.unity index f054956..06b482e 100644 --- a/Assets/Framework/Sample/Scenes/SampleScene.unity +++ b/Assets/Framework/Sample/Scenes/SampleScene.unity @@ -130,7 +130,7 @@ GameObject: - component: {fileID: 365617369} - component: {fileID: 365617368} m_Layer: 0 - m_Name: GameObject + m_Name: Bootstrap m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager.meta b/Assets/Framework/Scripts/Utilities/Audio Manager.meta new file mode 100644 index 0000000..8bc5280 --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0d36f19515c6e4aeeab082a4998a3348 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs new file mode 100644 index 0000000..37a508f --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Framework.Utilities.AudioManager +{ + [CreateAssetMenu(fileName = "AudioCatalog", menuName = "Framework/Audio/Audio Catalog")] + public class AudioCatalogSO : ScriptableObject + { + public List entries = new(); + public Dictionary entriesDict = new(); + + // NOTE: 在编辑器中保持同步,以及统计数量一样不代表内容一致 + private void EnsureMap() + { + if (entries.Count != entriesDict.Count) + { + entriesDict.Clear(); + foreach (var e in entries) + { + if (!entriesDict.ContainsKey(e.id)) + { + entriesDict.Add(e.id, e); + } + } + } + } + + public SoundEntry GetSoundEntry(string id) + { + EnsureMap(); + return entriesDict.TryGetValue(id, out var entry) ? entry : null; + } + + public AudioClip GetAudioClip(string id) + { + EnsureMap(); + return entriesDict.TryGetValue(id, out var entry) ? entry.clip : null; + } + +#if UNITY_EDITOR + public void EditorRebuild() + { + entriesDict.Clear(); + EnsureMap(); + UnityEditor.EditorUtility.SetDirty(this); + } +#endif + } + + [System.Serializable] + public class SoundEntry + { + public string id; + public AudioClip clip; + [Range(0f, 1f)] public float defaultVolume = 1f; + [Range(0.5f, 2f)] public float defaultPitch = 1f; + public bool spatial = false; + public bool loop = false; + } +} diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs.meta b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs.meta new file mode 100644 index 0000000..b813e57 --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioEntry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 470cff01f689548b589775f497f0719c \ No newline at end of file diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs new file mode 100644 index 0000000..3ba1908 --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs @@ -0,0 +1,42 @@ +using UnityEngine; +using System; + +namespace Framework.Utilities.AudioManager +{ + public class AudioHandle + { + AudioSource _src; + Action _release; + + internal AudioHandle(AudioSource src, Action release) + { + _src = src; + _release = release; + } + + public bool IsPlaying => _src != null && _src.isPlaying; + + // 关闭音效,并且调用回调函数 + public void Stop() + { + if (_src == null) return; + _src.Stop(); + _release?.Invoke(_src); + _src = null; + _release = null; + } + + // Internal: used by manager to return when finished + internal void ReturnIfFinished() + { + if (_src == null) return; + if (!_src.isPlaying) + { + _release?.Invoke(_src); + _src = null; + _release = null; + } + } + } + +} \ No newline at end of file diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs.meta b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs.meta new file mode 100644 index 0000000..4846a5f --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 89384e596bf5e44838985b0899c6e4ca \ No newline at end of file diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs new file mode 100644 index 0000000..43c1ebd --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs @@ -0,0 +1,251 @@ +using System.Collections; +using Codice.CM.SEIDInfo; +using UnityEngine; +using UnityEngine.Audio; +using UnityEngine.Pool; + +namespace Framework.Utilities.AudioManager +{ + public class AudioManager : MonoBehaviour, IAudioManager + { + [SerializeField] + private AudioCatalogSO _audioCatalog; + + private ObjectPool _sourcePool; + + public AudioMixerGroup BGMGroup; + public AudioMixerGroup SFXGroup; // 短时播放,使用pool管理,减少资源开销 + + public GameObject audioSourcePrefab; + private Transform _sourceRootTransform; + + [Header("Pool Settings")] + public int defaultPoolSize = 10; + public int maxPoolSize = 20; + + AudioSource _bgm; + Coroutine _bgmFade; + Transform _root; + + [Header("Volumes (linear 0..1)")] + [Range(0f, 1f)] public float masterVolume = 1f; + [Range(0f, 1f)] public float bgmVolume = 1f; + [Range(0f, 1f)] public float sfxVolume = 1f; + + private void Awake() + { + _sourcePool = new ObjectPool( + createFunc: () => CreatePoolSource(), + // 得到的时候只是激活,具体播放逻辑留给 Play 方法 + actionOnGet: s => { s.gameObject.SetActive(true); s.Stop(); s.clip = null; }, + actionOnRelease: s => { s.Stop(); s.clip = null; s.gameObject.SetActive(false); }, + actionOnDestroy: s => { if (s != null) Object.Destroy(s.gameObject); }, + collectionCheck: false, + defaultCapacity: defaultPoolSize, + maxSize: maxPoolSize + ); + + // 预热 pool + for (int i = 0; i < defaultPoolSize; i++) + { + _sourcePool.Release(_sourcePool.Get()); + } + + CreateBgmSource(); + ApplyVolumes(); + } + + private void CreateBgmSource() + { + var go = new GameObject("BGM_Source"); + go.transform.SetParent(_root, false); + _bgm = go.AddComponent(); + _bgm.loop = true; + _bgm.playOnAwake = false; + if (BGMGroup != null) + _bgm.outputAudioMixerGroup = BGMGroup; + } + + private AudioSource CreatePoolSource() + { + GameObject go; + if (audioSourcePrefab != null) + { + go = Instantiate(audioSourcePrefab, _sourceRootTransform); + } + else + { + go = new GameObject("PoolAudioSource", typeof(AudioSource)); + } + var src = go.GetComponent() ?? go.AddComponent(); + src.playOnAwake = false; + if (SFXGroup != null) + src.outputAudioMixerGroup = SFXGroup; + go.SetActive(false); + return src; + } + + // TODO: + public void PlayBGM(string clipName, float duration, bool isLoop) + { + if (_audioCatalog == null) + return; + + var entry = _audioCatalog.GetSoundEntry(clipName); + if (entry == null || entry.clip == null) + return; + if (_bgmFade != null) + { + StopCoroutine(_bgmFade); + } + + _bgm.clip = entry.clip; + _bgm.loop = isLoop; + _bgm.Play(); + _bgmFade = StartCoroutine(FadeVolumeTo(_bgm, masterVolume * bgmVolume, duration)); + } + + public void StopBGM(float duration) + { + if (_bgm == null) + return; + if (_bgmFade != null) + StopCoroutine(_bgmFade); + _bgmFade = StartCoroutine(FadeOutAndStop(_bgm, duration)); + } + + public void SetBGMVolume(float volume, float duration) + { + if (_bgm == null) + return; + if (_bgmFade != null) + StopCoroutine(_bgmFade); + _bgmFade = StartCoroutine(FadeVolumeTo(_bgm, volume, duration)); + } + + public void PauseBGM() + { + if (_bgm == null) + return; + _bgm.Pause(); + } + + public void ResumeBGM() + { + if (_bgm == null) + return; + _bgm.UnPause(); + } + + public AudioHandle PlaySFX(string clipName, Vector3? pos = null, float? volume = null, float? pitch = null) + { + if (_audioCatalog == null) + return null; + + var entry = _audioCatalog.GetSoundEntry(clipName); + if (entry == null || entry.clip == null) + return null; + AudioSource src; + + try + { + src = _sourcePool.Get(); + } + catch + { + Debug.LogWarning("AudioSource pool exhausted!"); + src = CreatePoolSource(); + src.gameObject.SetActive(true); + } + + src.clip = entry.clip; + src.volume = (volume ?? entry.defaultVolume) * masterVolume * sfxVolume; + src.pitch = pitch ?? entry.defaultPitch; + src.spatialBlend = entry.spatial ? 1f : 0f; + src.loop = entry.loop; + src.Play(); + + var handle = new AudioHandle(src, ReleaseSource); + if (!src.loop) + { + StartCoroutine(AutoReleaseWhenFinished(handle)); + } + return handle; + } + + private void ReleaseSource(AudioSource src) + { + if (_sourcePool != null && _sourcePool.CountInactive < maxPoolSize) + { + _sourcePool.Release(src); + } + else + { + Destroy(src.gameObject); + } + } + + public void SetMasterVolume(float v) { masterVolume = Mathf.Clamp01(v); ApplyVolumes(); } + public void SetBGMVolume(float v) { bgmVolume = Mathf.Clamp01(v); ApplyVolumes(); } + public void SetSFXVolume(float v) { sfxVolume = Mathf.Clamp01(v); /* active sfx keep their set volume; new ones use updated value */ } + + private void ApplyVolumes() + { + if (_bgm != null) _bgm.volume = masterVolume * bgmVolume; + } + + /// + /// 在给定时间内把音量淡入或淡出到目标值 + /// + /// + /// + /// + /// + IEnumerator FadeVolumeTo(AudioSource source, float targetVolume, float duration) + { + float t = 0f; + float startVolume = source.volume; + while (t < duration) + { + t += Time.deltaTime; + source.volume = Mathf.Lerp(startVolume, targetVolume, t / duration); + yield return null; + } + + source.volume = targetVolume; + _bgmFade = null; + } + + /// + /// 淡出到声音为0 并且停止播放 + /// + /// + /// + /// + IEnumerator FadeOutAndStop(AudioSource source, float duration) + { + float t = 0f; + float startVolume = source.volume; + while (t < duration) + { + t += Time.deltaTime; + source.volume = Mathf.Lerp(startVolume, 0f, t / duration); + yield return null; + } + + source.volume = 0f; + source.Stop(); + source.clip = null; + _bgmFade = null; + } + + IEnumerator AutoReleaseWhenFinished(AudioHandle handle) + { + while (handle.IsPlaying) + { + yield return null; + } + handle.Stop(); + } + } +} diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs.meta b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs.meta new file mode 100644 index 0000000..db618a1 --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/AudioManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ec9fd183ec13a48478d534295fef567f \ No newline at end of file diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs b/Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs new file mode 100644 index 0000000..1c783e0 --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs @@ -0,0 +1,14 @@ +using UnityEngine; +namespace Framework.Utilities.AudioManager +{ + public interface IAudioManager + { + void PlayBGM(string clipName, float duration, bool isLoop); + void StopBGM(float duration); + void PauseBGM(); + void ResumeBGM(); + void SetBGMVolume(float volume, float duration); + + AudioHandle PlaySFX(string clipName, Vector3? pos = null, float? volume = null, float? pitch = null); + } +} \ No newline at end of file diff --git a/Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs.meta b/Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs.meta new file mode 100644 index 0000000..9e5c438 --- /dev/null +++ b/Assets/Framework/Scripts/Utilities/Audio Manager/IAudioManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0ffd4e75e97904c2b959557d49d3ce69 \ No newline at end of file