334 lines
11 KiB
C#
334 lines
11 KiB
C#
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
using Unheimlich.Gameplay.Portals;
|
|
|
|
namespace Unheimlich.Audio
|
|
{
|
|
/// <summary>
|
|
/// Creates virtual audio proxies for sounds coming from the other side of a portal.
|
|
/// When a portal links to a room, sounds from that room are projected through the portal
|
|
/// so the player hears them spatially from the portal's location. This creates seamless
|
|
/// audio transitions when walking through portals.
|
|
/// </summary>
|
|
public class PortalAudioProxy : MonoBehaviour
|
|
{
|
|
[Header("Settings")]
|
|
[SerializeField] private float _maxDistance = 20f;
|
|
[SerializeField] private float _fadeStartDistance = 15f;
|
|
[SerializeField] private float _transitionDistance = 2f;
|
|
[SerializeField] private bool _useLowPassFilter = true;
|
|
[SerializeField] private float _lowPassCutoff = 5000f;
|
|
[SerializeField] private float _timeSyncThreshold = 0.1f;
|
|
[SerializeField] private float _minSpread = 60f;
|
|
[SerializeField] private float _maxSpread = 180f;
|
|
[SerializeField] private float _volumeRolloffPower = 2f;
|
|
|
|
[Header("Debug")]
|
|
[SerializeField] private List<AudioSource> _remoteSources = new List<AudioSource>();
|
|
|
|
private Portal _portal;
|
|
private Dictionary<AudioSource, AudioSource> _proxyMapping = new Dictionary<AudioSource, AudioSource>();
|
|
private Dictionary<AudioSource, AudioLowPassFilter> _filterMapping = new Dictionary<AudioSource, AudioLowPassFilter>();
|
|
|
|
private GameObject _lastLoadedRoomInstance;
|
|
private bool _needsRefresh;
|
|
|
|
private void Awake()
|
|
{
|
|
_portal = GetComponent<Portal>();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (_portal != null)
|
|
{
|
|
_portal.RoomLoaded += OnRoomLoaded;
|
|
_portal.RoomUnloaded += OnRoomUnloaded;
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (_portal != null)
|
|
{
|
|
_portal.RoomLoaded -= OnRoomLoaded;
|
|
_portal.RoomUnloaded -= OnRoomUnloaded;
|
|
}
|
|
}
|
|
|
|
private void OnRoomLoaded()
|
|
{
|
|
_needsRefresh = true;
|
|
}
|
|
|
|
private void OnRoomUnloaded()
|
|
{
|
|
_needsRefresh = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Main update loop: refreshes audio sources when the room changes,
|
|
/// then updates all proxy positions and volumes each frame.
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
if (_portal == null || _portal.LinkedPortal == null)
|
|
return;
|
|
|
|
GameObject currentLoadedRoom = _portal.LoadedRoomInstance;
|
|
|
|
if (currentLoadedRoom != _lastLoadedRoomInstance)
|
|
{
|
|
_lastLoadedRoomInstance = currentLoadedRoom;
|
|
_needsRefresh = true;
|
|
}
|
|
|
|
if (_needsRefresh)
|
|
{
|
|
RefreshRemoteSources();
|
|
_needsRefresh = false;
|
|
}
|
|
|
|
UpdateProxies();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans the loaded room for all AudioSource components and caches them.
|
|
/// Called whenever a new room is loaded through the portal.
|
|
/// </summary>
|
|
private void RefreshRemoteSources()
|
|
{
|
|
GameObject linkedRoom = _portal.LoadedRoomInstance;
|
|
if (linkedRoom == null)
|
|
{
|
|
_remoteSources.Clear();
|
|
return;
|
|
}
|
|
|
|
AudioSource[] sources = linkedRoom.GetComponentsInChildren<AudioSource>();
|
|
|
|
_remoteSources.Clear();
|
|
_remoteSources.AddRange(sources);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates all audio proxies each frame:
|
|
/// - Syncs playback state with remote sources
|
|
/// - Positions proxies at the portal location
|
|
/// - Adjusts volume based on distance and player orientation
|
|
/// - Applies low-pass filtering for sounds through the portal
|
|
/// This creates the illusion that sounds are coming from the portal opening.
|
|
/// </summary>
|
|
private void UpdateProxies()
|
|
{
|
|
Camera playerCam = Camera.main;
|
|
if (playerCam == null)
|
|
return;
|
|
|
|
Portal linkedPortal = _portal.LinkedPortal;
|
|
if (linkedPortal == null)
|
|
return;
|
|
|
|
Vector3 playerPos = playerCam.transform.position;
|
|
Vector3 portalAPos = transform.position;
|
|
|
|
float distPlayerToPortalA = Vector3.Distance(playerPos, portalAPos);
|
|
|
|
Vector3 offsetFromPortalA = playerPos - portalAPos;
|
|
float dotToPortalA = Vector3.Dot(offsetFromPortalA.normalized, transform.forward);
|
|
bool playerFacingPortal = dotToPortalA > 0;
|
|
|
|
float transitionBlend = 0f;
|
|
if (playerFacingPortal && distPlayerToPortalA < _transitionDistance)
|
|
{
|
|
transitionBlend = 1f - (distPlayerToPortalA / _transitionDistance);
|
|
}
|
|
|
|
List<AudioSource> toRemove = new List<AudioSource>();
|
|
|
|
foreach (var remoteSource in _remoteSources)
|
|
{
|
|
if (remoteSource == null)
|
|
{
|
|
toRemove.Add(remoteSource);
|
|
continue;
|
|
}
|
|
|
|
if (!remoteSource.isPlaying)
|
|
{
|
|
if (_proxyMapping.ContainsKey(remoteSource))
|
|
{
|
|
AudioSource proxy = _proxyMapping[remoteSource];
|
|
if (proxy != null && proxy.isPlaying)
|
|
proxy.Stop();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
AudioSource localProxy = GetOrCreateProxy(remoteSource);
|
|
|
|
if (localProxy.clip != remoteSource.clip)
|
|
{
|
|
localProxy.clip = remoteSource.clip;
|
|
localProxy.loop = remoteSource.loop;
|
|
localProxy.time = remoteSource.time;
|
|
localProxy.Play();
|
|
}
|
|
else if (!localProxy.isPlaying)
|
|
{
|
|
localProxy.time = remoteSource.time;
|
|
localProxy.Play();
|
|
}
|
|
else
|
|
{
|
|
float timeDifference = Mathf.Abs(localProxy.time - remoteSource.time);
|
|
if (timeDifference > _timeSyncThreshold)
|
|
localProxy.time = remoteSource.time;
|
|
}
|
|
|
|
localProxy.pitch = remoteSource.pitch;
|
|
|
|
if (_useLowPassFilter && _filterMapping.ContainsKey(remoteSource))
|
|
{
|
|
AudioLowPassFilter filter = _filterMapping[remoteSource];
|
|
if (filter != null)
|
|
filter.enabled = !_portal.IsDoorOpen;
|
|
}
|
|
|
|
Vector3 virtualPlayerPos = linkedPortal.transform.TransformPoint(transform.InverseTransformPoint(playerPos));
|
|
|
|
Vector3 proxyWorldPos = transform.TransformPoint(linkedPortal.transform.InverseTransformPoint(remoteSource.transform.position));
|
|
localProxy.transform.position = proxyWorldPos;
|
|
|
|
float baseVolume = remoteSource.volume;
|
|
AudioSourceDemo demo = remoteSource.GetComponent<AudioSourceDemo>();
|
|
if (demo != null)
|
|
baseVolume = demo.OriginalVolume;
|
|
|
|
localProxy.volume = baseVolume * (1f - transitionBlend);
|
|
|
|
if (demo != null)
|
|
{
|
|
float sourceFade = demo.GetDistanceFadeMultiplier(virtualPlayerPos);
|
|
localProxy.volume *= sourceFade;
|
|
}
|
|
|
|
float distanceToPortal = Vector3.Distance(playerPos, portalAPos);
|
|
if (distanceToPortal > _fadeStartDistance)
|
|
{
|
|
float fadeRange = _maxDistance - _fadeStartDistance;
|
|
float fadeAmount = (distanceToPortal - _fadeStartDistance) / fadeRange;
|
|
fadeAmount = Mathf.Clamp01(fadeAmount);
|
|
localProxy.volume *= (1f - fadeAmount);
|
|
}
|
|
|
|
if (localProxy.volume <= PortalConstants.MIN_VOLUME_THRESHOLD && localProxy.isPlaying)
|
|
{
|
|
localProxy.Stop();
|
|
continue;
|
|
}
|
|
|
|
localProxy.spread = Mathf.Lerp(_minSpread, _maxSpread, transitionBlend);
|
|
}
|
|
|
|
foreach (var source in toRemove)
|
|
{
|
|
if (_proxyMapping.ContainsKey(source))
|
|
{
|
|
AudioSource proxy = _proxyMapping[source];
|
|
if (proxy != null)
|
|
Destroy(proxy.gameObject);
|
|
|
|
_proxyMapping.Remove(source);
|
|
}
|
|
|
|
if (_filterMapping.ContainsKey(source))
|
|
_filterMapping.Remove(source);
|
|
|
|
_remoteSources.Remove(source);
|
|
}
|
|
}
|
|
|
|
private float CalculateVolumeFromDistance(float distance, AudioSource source)
|
|
{
|
|
float baseVolume = source.volume;
|
|
|
|
AudioSourceDemo demo = source.GetComponent<AudioSourceDemo>();
|
|
if (demo != null)
|
|
baseVolume = demo.OriginalVolume;
|
|
|
|
if (distance > source.maxDistance)
|
|
return 0f;
|
|
|
|
if (distance < source.minDistance)
|
|
return baseVolume;
|
|
|
|
switch (source.rolloffMode)
|
|
{
|
|
case AudioRolloffMode.Logarithmic:
|
|
float normalizedDist = (distance - source.minDistance) / (source.maxDistance - source.minDistance);
|
|
float logVolume = baseVolume * (1f / (1f + normalizedDist * 10f));
|
|
return logVolume * Mathf.Pow(1f - normalizedDist, _volumeRolloffPower);
|
|
|
|
case AudioRolloffMode.Linear:
|
|
float linearFactor = 1f - ((distance - source.minDistance) / (source.maxDistance - source.minDistance));
|
|
linearFactor = Mathf.Pow(linearFactor, _volumeRolloffPower);
|
|
return baseVolume * Mathf.Max(0f, linearFactor);
|
|
|
|
default:
|
|
return baseVolume;
|
|
}
|
|
}
|
|
|
|
private AudioSource GetOrCreateProxy(AudioSource remoteSource)
|
|
{
|
|
if (_proxyMapping.ContainsKey(remoteSource) && _proxyMapping[remoteSource] != null)
|
|
return _proxyMapping[remoteSource];
|
|
|
|
GameObject proxyObj = new GameObject($"AudioProxy_{remoteSource.name}");
|
|
proxyObj.transform.SetParent(transform);
|
|
proxyObj.transform.localPosition = Vector3.zero;
|
|
|
|
AudioSource proxy = proxyObj.AddComponent<AudioSource>();
|
|
proxy.spatialBlend = 1f;
|
|
proxy.spatialize = true;
|
|
proxy.spatializePostEffects = true;
|
|
proxy.reverbZoneMix = 1f;
|
|
proxy.spread = 60f;
|
|
proxy.rolloffMode = remoteSource.rolloffMode;
|
|
proxy.minDistance = remoteSource.minDistance;
|
|
proxy.maxDistance = remoteSource.maxDistance;
|
|
proxy.playOnAwake = false;
|
|
|
|
if (_useLowPassFilter)
|
|
{
|
|
AudioLowPassFilter filter = proxyObj.AddComponent<AudioLowPassFilter>();
|
|
filter.cutoffFrequency = _lowPassCutoff;
|
|
filter.enabled = false;
|
|
_filterMapping[remoteSource] = filter;
|
|
}
|
|
|
|
_proxyMapping[remoteSource] = proxy;
|
|
|
|
return proxy;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
foreach (var kvp in _proxyMapping)
|
|
{
|
|
if (kvp.Value != null)
|
|
Destroy(kvp.Value.gameObject);
|
|
}
|
|
_proxyMapping.Clear();
|
|
_filterMapping.Clear();
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = Color.cyan;
|
|
Gizmos.DrawWireSphere(transform.position, _maxDistance);
|
|
}
|
|
}
|
|
}
|