Advanced POC - portals, dialogue system + floating words, room loading
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user