Advanced POC - portals, dialogue system + floating words, room loading

This commit is contained in:
2026-01-26 20:31:27 +01:00
parent b5ca4763b3
commit 203b00578a
426 changed files with 123482 additions and 29 deletions
@@ -0,0 +1,127 @@
using UnityEngine;
using Unheimlich.Gameplay.Player;
namespace Unheimlich.Audio
{
public class AudioSourceDemo : MonoBehaviour
{
[Header("Demo Settings")]
[SerializeField] private AudioClip _clip;
[SerializeField] private bool _playOnStart = true;
[SerializeField] private bool _loop = true;
[SerializeField] private float _volume = 0.5f;
[SerializeField] private float _sourceMinDistance = 1f;
[SerializeField] private float _sourceMaxDistance = 20;
[Header("Distance Culling")]
[SerializeField] private bool _enableDistanceCulling = true;
[SerializeField] private float _cullingDistance = 50f;
[SerializeField] private float _fadeStartDistance = 40f;
private AudioSource _source;
private bool _hasStarted = false;
private float _savedTime = 0f;
private bool _wasPlaying = false;
private bool _isCulled = false;
private Transform _player;
public float OriginalVolume => _volume;
public float GetDistanceFadeMultiplier(Vector3 listenerPosition)
{
if (!_enableDistanceCulling)
return 1f;
float distance = Vector3.Distance(transform.position, listenerPosition);
if (distance > _cullingDistance)
return 0f;
if (distance > _fadeStartDistance)
{
float fadeRange = _cullingDistance - _fadeStartDistance;
float fadeAmount = (distance - _fadeStartDistance) / fadeRange;
return 1f - Mathf.Clamp01(fadeAmount);
}
return 1f;
}
private void Awake()
{
_source = GetComponent<AudioSource>();
if (_source == null)
_source = gameObject.AddComponent<AudioSource>();
_source.clip = _clip;
_source.loop = _loop;
_source.volume = _volume;
_source.playOnAwake = false;
_source.spatialBlend = 1f;
_source.rolloffMode = AudioRolloffMode.Logarithmic;
_source.minDistance = _sourceMinDistance;
_source.maxDistance = _sourceMaxDistance;
}
private void OnEnable()
{
if (_wasPlaying && _savedTime > 0f)
{
_source.time = _savedTime;
_source.Play();
}
}
private void Start()
{
if (_playOnStart && _clip != null && !_hasStarted && !_source.isPlaying)
{
_source.Play();
_hasStarted = true;
}
Player playerObj = Player.Instance;
if (playerObj != null)
_player = playerObj.transform;
}
private void Update()
{
if (_enableDistanceCulling && _player != null && _source != null)
{
float distance = Vector3.Distance(transform.position, _player.position);
if (distance > _cullingDistance)
{
if (!_isCulled)
{
_source.volume = 0f;
_isCulled = true;
}
}
else if (distance > _fadeStartDistance)
{
float fadeRange = _cullingDistance - _fadeStartDistance;
float fadeAmount = (distance - _fadeStartDistance) / fadeRange;
fadeAmount = Mathf.Clamp01(fadeAmount);
_source.volume = _volume * (1f - fadeAmount);
_isCulled = false;
}
else
{
if (_isCulled || _source.volume != _volume)
{
_source.volume = _volume;
_isCulled = false;
}
}
}
}
private void OnDisable()
{
_wasPlaying = _source.isPlaying;
_savedTime = _source.time;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f399329860533c14fba15187d7e340db
@@ -0,0 +1,63 @@
using UnityEngine;
namespace Unheimlich.Audio
{
public class AudioSourceInspector : MonoBehaviour
{
[Header("Debug Settings")]
[SerializeField] private float _logInterval = 1f;
private AudioSource _source;
private float _lastLogTime;
private void Awake()
{
_source = GetComponent<AudioSource>();
}
private void Update()
{
if (_source == null) return;
if (Time.time - _lastLogTime >= _logInterval)
{
LogAudioSourceState();
_lastLogTime = Time.time;
}
}
private void OnEnable()
{
Debug.Log($"<color=cyan>[AudioInspector] OnEnable called on {gameObject.name}</color>");
if (_source != null)
LogAudioSourceState();
}
private void OnDisable()
{
Debug.Log($"<color=magenta>[AudioInspector] OnDisable called on {gameObject.name}</color>");
if (_source != null)
LogAudioSourceState();
}
private void LogAudioSourceState()
{
if (_source == null)
{
Debug.LogWarning($"[AudioInspector] No AudioSource on {gameObject.name}");
return;
}
string clipName = _source.clip != null ? _source.clip.name : "NULL";
Debug.Log($"<color=yellow>[AudioInspector] {gameObject.name} | " +
$"Clip: {clipName} | " +
$"Time: {_source.time:F2}s | " +
$"isPlaying: {_source.isPlaying} | " +
$"enabled: {_source.enabled} | " +
$"volume: {_source.volume:F2} | " +
$"mute: {_source.mute} | " +
$"loop: {_source.loop} | " +
$"Active: {gameObject.activeInHierarchy}</color>");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 65fea0a39ddb811419ec546ea98e5917
@@ -0,0 +1,52 @@
using UnityEngine;
using Unheimlich.Gameplay.Portals;
namespace Unheimlich.Audio
{
public class PortalAudioDebugger : MonoBehaviour
{
private Portal _portal;
private void Awake()
{
_portal = GetComponent<Portal>();
}
private void OnEnable()
{
if (_portal != null)
{
_portal.RoomLoaded += OnRoomLoadedDebug;
_portal.RoomUnloaded += OnRoomUnloadedDebug;
}
}
private void OnDisable()
{
if (_portal != null)
{
_portal.RoomLoaded -= OnRoomLoadedDebug;
_portal.RoomUnloaded -= OnRoomUnloadedDebug;
}
}
private void OnRoomLoadedDebug()
{
Debug.Log($"<color=green>[PortalDebug] RoomLoaded event fired on {gameObject.name}</color>");
if (_portal.LoadedRoomInstance != null)
{
AudioSource[] sources = _portal.LoadedRoomInstance.GetComponentsInChildren<AudioSource>();
Debug.Log($"<color=green>[PortalDebug] Found {sources.Length} AudioSources in loaded room:</color>");
foreach (var source in sources)
Debug.Log($" - {source.name} | Playing: {source.isPlaying} | Clip: {(source.clip != null ? source.clip.name : "NULL")}");
}
}
private void OnRoomUnloadedDebug()
{
Debug.Log($"<color=red>[PortalDebug] RoomUnloaded event fired on {gameObject.name}</color>");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 897784c22112f5f48af4313022397d8e
@@ -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);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1ef7477556cb1fa45ab6cf4a9422c405