com.alicizax.unity.framework/Runtime/Audio/Components/AudioEmitter.cs
陈思海 46194ddee8 重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
重构音频模块
1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优
2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历
3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点
4. 支持3D环境音并具备距离衰减、遮挡等空间属性
5. 新增音频类型(BGM/SFX/Voice/Ambient)
6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
2026-04-23 17:21:36 +08:00

287 lines
8.4 KiB
C#

using AlicizaX;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
[DisallowMultipleComponent]
[AddComponentMenu("Game Framework/Audio/Audio Emitter")]
public sealed class AudioEmitter : MonoBehaviour
{
private enum AudioEmitterClipMode
{
Address = 0,
Clip = 1
}
[Header("Playback")]
[SerializeField] private AudioType m_AudioType = AudioType.Ambient;
[SerializeField] private AudioEmitterClipMode m_ClipMode = AudioEmitterClipMode.Address;
[SerializeField] private string m_Address = string.Empty;
[SerializeField] private AudioClip m_Clip;
[SerializeField] private bool m_PlayOnEnable = true;
[SerializeField] private bool m_Loop = true;
[SerializeField, Range(0f, 1f)] private float m_Volume = 1f;
[SerializeField] private bool m_Async = true;
[SerializeField] private bool m_CacheClip = true;
[SerializeField] private bool m_StopWithFadeout = true;
[Header("Spatial")]
[SerializeField] private bool m_FollowSelf = true;
[SerializeField] private Vector3 m_FollowOffset = Vector3.zero;
[SerializeField, Range(0f, 1f)] private float m_SpatialBlend = 1f;
[SerializeField] private AudioRolloffMode m_RolloffMode = AudioRolloffMode.Logarithmic;
[SerializeField, Min(0f)] private float m_MinDistance = 2f;
[SerializeField, Min(0f)] private float m_MaxDistance = 30f;
[Header("Trigger")]
[SerializeField] private bool m_UseTriggerRange = false;
[SerializeField, Min(0f)] private float m_TriggerRange = 10f;
[SerializeField, Min(0f)] private float m_TriggerHysteresis = 0.5f;
[Header("Gizmos")]
[SerializeField] private bool m_DrawGizmos = true;
[SerializeField] private bool m_DrawOnlyWhenSelected = true;
[SerializeField] private Color m_TriggerColor = new Color(0.2f, 0.9f, 1f, 0.9f);
[SerializeField] private Color m_MinDistanceColor = new Color(1f, 0.9f, 0.2f, 0.9f);
[SerializeField] private Color m_MaxDistanceColor = new Color(1f, 0.45f, 0.05f, 0.9f);
private IAudioService _audioService;
private ulong _handle;
private bool _isPlaying;
private bool _insideTriggerRange;
public ulong Handle => _handle;
public bool IsPlaying => _isPlaying;
private void OnEnable()
{
TryBindService();
if (m_PlayOnEnable && !m_UseTriggerRange)
{
StartPlayback();
}
}
private void Update()
{
if (!m_UseTriggerRange)
{
RefreshPlaybackState();
return;
}
if (_audioService == null && !TryBindService())
{
return;
}
Transform listener = _audioService.ListenerTransform;
if (listener == null)
{
_insideTriggerRange = false;
StopPlayback();
return;
}
Vector3 offset = listener.position - transform.position;
float range = _isPlaying ? m_TriggerRange + m_TriggerHysteresis : m_TriggerRange;
float sqrRange = range * range;
if (offset.sqrMagnitude <= sqrRange)
{
RefreshPlaybackState();
if (!_insideTriggerRange || (m_Loop && !_isPlaying))
{
_insideTriggerRange = true;
StartPlayback();
}
}
else
{
_insideTriggerRange = false;
StopPlayback();
}
}
private void OnDisable()
{
StopPlayback();
_audioService = null;
_insideTriggerRange = false;
}
public void Play()
{
if (_audioService == null)
{
TryBindService();
}
StartPlayback();
}
public void Stop()
{
StopPlayback();
}
private bool TryBindService()
{
return AppServices.TryGet(out _audioService);
}
private void StartPlayback()
{
if (_audioService == null || _isPlaying || !HasPlayableAsset())
{
return;
}
float maxDistance = m_MaxDistance >= m_MinDistance ? m_MaxDistance : m_MinDistance;
if (m_FollowSelf)
{
_handle = m_ClipMode == AudioEmitterClipMode.Clip
? _audioService.PlayFollow(
m_AudioType,
m_Clip,
transform,
m_FollowOffset,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume)
: _audioService.PlayFollow(
m_AudioType,
m_Address,
transform,
m_FollowOffset,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume,
m_Async,
m_CacheClip);
}
else
{
Vector3 position = transform.position;
_handle = m_ClipMode == AudioEmitterClipMode.Clip
? _audioService.Play3D(
m_AudioType,
m_Clip,
position,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume)
: _audioService.Play3D(
m_AudioType,
m_Address,
position,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume,
m_Async,
m_CacheClip);
}
_isPlaying = _handle != 0UL;
}
private void StopPlayback()
{
if (!_isPlaying)
{
_handle = 0UL;
return;
}
if (_audioService != null && _handle != 0UL)
{
_audioService.Stop(_handle, m_StopWithFadeout);
}
_handle = 0UL;
_isPlaying = false;
}
private bool HasPlayableAsset()
{
return m_ClipMode == AudioEmitterClipMode.Clip
? m_Clip != null
: !string.IsNullOrEmpty(m_Address);
}
private void RefreshPlaybackState()
{
if (!_isPlaying || _audioService == null || _handle == 0UL)
{
return;
}
if (_audioService.IsPlaying(_handle))
{
return;
}
_handle = 0UL;
_isPlaying = false;
}
private void OnValidate()
{
if (m_MaxDistance < m_MinDistance)
{
m_MaxDistance = m_MinDistance;
}
}
private void OnDrawGizmos()
{
if (!m_DrawOnlyWhenSelected)
{
DrawEmitterGizmos();
}
}
private void OnDrawGizmosSelected()
{
if (m_DrawOnlyWhenSelected)
{
DrawEmitterGizmos();
}
}
private void DrawEmitterGizmos()
{
if (!m_DrawGizmos)
{
return;
}
Vector3 position = transform.position;
if (m_UseTriggerRange)
{
Gizmos.color = m_TriggerColor;
Gizmos.DrawWireSphere(position, m_TriggerRange);
}
Gizmos.color = m_MinDistanceColor;
Gizmos.DrawWireSphere(position, m_MinDistance);
Gizmos.color = m_MaxDistanceColor;
Gizmos.DrawWireSphere(position, m_MaxDistance);
}
}
}