## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #56
290 lines
9.7 KiB
C#
290 lines
9.7 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
using AppleHills.Core;
|
|
using AppleHills.Core.Interfaces;
|
|
using System.Collections.Generic;
|
|
using AudioSourceEvents;
|
|
using System;
|
|
using Core;
|
|
using Core.Lifecycle;
|
|
|
|
public class AudioManager : ManagedBehaviour, IPausable
|
|
{
|
|
/// <summary>
|
|
/// Play all audio, just music or no audio at all when the game is paused.
|
|
/// </summary>
|
|
public enum PauseBehavior
|
|
{ PlayAllAudio, MusicOnly, NoAudio }
|
|
|
|
public PauseBehavior currentPauseBehavior;
|
|
|
|
public AudioMixer audioMixer;
|
|
private AudioListener _audioListener;
|
|
public AppleAudioSource currentlyPlayingVO;
|
|
|
|
|
|
private static AudioManager _instance;
|
|
private GameObject _player;
|
|
|
|
public List<AppleAudioSource> criticalVOSources;
|
|
public List<AppleAudioSource> VOSources;
|
|
public List<AppleAudioSource> musicSources;
|
|
public List<AppleAudioSource> ambienceSources;
|
|
public List<AppleAudioSource> SFXSources;
|
|
|
|
private IAudioEventSource _eventSource;
|
|
private bool wasInterrupted;
|
|
|
|
/// <summary>
|
|
/// Singleton instance of the AudioManager.
|
|
/// </summary>
|
|
public static AudioManager Instance => _instance;
|
|
|
|
// ManagedBehaviour configuration
|
|
public override int ManagedAwakePriority => 30; // Audio infrastructure
|
|
public override bool AutoRegisterPausable => true; // Auto-register as IPausable
|
|
|
|
internal override void OnManagedAwake()
|
|
{
|
|
// Set instance immediately (early initialization)
|
|
_instance = this;
|
|
}
|
|
|
|
internal override void OnManagedStart()
|
|
{
|
|
// Initialize lists if they were not set in inspector
|
|
criticalVOSources = criticalVOSources ?? new List<AppleAudioSource>();
|
|
VOSources = VOSources ?? new List<AppleAudioSource>();
|
|
musicSources = musicSources ?? new List<AppleAudioSource>();
|
|
ambienceSources = ambienceSources ?? new List<AppleAudioSource>();
|
|
SFXSources = SFXSources ?? new List<AppleAudioSource>();
|
|
|
|
// Perform singleton-dependent initialization here
|
|
if (QuickAccess.Instance != null)
|
|
{
|
|
_player = QuickAccess.Instance.PlayerGameObject;
|
|
if (QuickAccess.Instance.MainCamera != null)
|
|
{
|
|
_audioListener = QuickAccess.Instance.MainCamera.GetComponent<AudioListener>();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logging.Warning("[AudioManager] QuickAccess.Instance is null during OnManagedStart. Some audio references may remain unset.");
|
|
}
|
|
|
|
// Diagnostic
|
|
foreach (AppleAudioSource _audioSource in criticalVOSources)
|
|
{
|
|
Logging.Debug("Found source: " + _audioSource.name);
|
|
}
|
|
}
|
|
|
|
public void SetAudioPauseBehavior(PauseBehavior newPauseBehavior)
|
|
{
|
|
switch (newPauseBehavior)
|
|
{
|
|
case PauseBehavior.PlayAllAudio:
|
|
audioMixer.updateMode = AudioMixerUpdateMode.UnscaledTime;
|
|
AudioListener.pause = false;
|
|
break;
|
|
case PauseBehavior.MusicOnly:
|
|
audioMixer.updateMode = AudioMixerUpdateMode.UnscaledTime; break;
|
|
//TODO: Pause all audio mixers except music mixer
|
|
case PauseBehavior.NoAudio:
|
|
audioMixer.updateMode = AudioMixerUpdateMode.Normal;
|
|
AudioListener.pause = true;
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
public LevelAudioObject GetCurrentLevelAudioObject()
|
|
{
|
|
Logging.Debug("Audio objects: " + FindObjectsByType<LevelAudioObject>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length);
|
|
if (FindObjectsByType<LevelAudioObject>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length > 1)
|
|
{
|
|
Logging.Warning("Warning! More than one LevelAudioObject in the level! Using the first one found");
|
|
return FindObjectsByType<LevelAudioObject>(FindObjectsInactive.Include, FindObjectsSortMode.None)[0];
|
|
}
|
|
if (FindObjectsByType<LevelAudioObject>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length == 0)
|
|
{
|
|
Logging.Warning("Error! No LevelAudioObject found, AudioManager might not function properly!");
|
|
return null;
|
|
}
|
|
else
|
|
return FindFirstObjectByType<LevelAudioObject>();
|
|
}
|
|
|
|
|
|
|
|
public void Pause()
|
|
{
|
|
SetAudioPauseBehavior(PauseBehavior.NoAudio);
|
|
}
|
|
|
|
public void DoResume()
|
|
{
|
|
SetAudioPauseBehavior(PauseBehavior.PlayAllAudio);
|
|
}
|
|
|
|
public void RegisterNewAudioSource(AppleAudioSource newAudioSource)
|
|
{
|
|
switch (newAudioSource.audioSourceType)
|
|
{
|
|
case AppleAudioSource.AudioSourceType.CriticalVO:
|
|
criticalVOSources.Add(newAudioSource);
|
|
break;
|
|
case AppleAudioSource.AudioSourceType.VO:
|
|
VOSources.Add(newAudioSource);
|
|
break;
|
|
case AppleAudioSource.AudioSourceType.SFX:
|
|
SFXSources.Add(newAudioSource);
|
|
break;
|
|
case AppleAudioSource.AudioSourceType.Ambience:
|
|
ambienceSources.Add(newAudioSource);
|
|
break;
|
|
case AppleAudioSource.AudioSourceType.Music:
|
|
musicSources.Add(newAudioSource);
|
|
break;
|
|
}
|
|
|
|
}
|
|
/// <summary>
|
|
/// Request playing a VO line. Returns true if whatever is playing is not critical, or weight of requested VO line is lower.
|
|
/// </summary>
|
|
public bool RequestPlayVO(AppleAudioSource requestedAudioSource)
|
|
{
|
|
// If nothing is playing, let the requested audio source play
|
|
if (currentlyPlayingVO == null)
|
|
{
|
|
SetupNewAudioSource(requestedAudioSource);
|
|
return true;
|
|
}
|
|
|
|
// If the requested audio is not critical, and the currently playing audio is, tell the request to get bent
|
|
if (requestedAudioSource.audioSourceType == AppleAudioSource.AudioSourceType.VO && currentlyPlayingVO.audioSourceType == AppleAudioSource.AudioSourceType.CriticalVO)
|
|
{
|
|
return false;
|
|
}
|
|
// If the requested audio source is the same, interrupt and trigger it again
|
|
if (currentlyPlayingVO == requestedAudioSource)
|
|
{
|
|
InterruptAudioSource(requestedAudioSource);
|
|
SetupNewAudioSource(requestedAudioSource);
|
|
return true;
|
|
|
|
}
|
|
// if the currently playing audio source is not critical, interrupt it and play the requested audio source
|
|
if (currentlyPlayingVO.audioSourceType != AppleAudioSource.AudioSourceType.CriticalVO)
|
|
{
|
|
|
|
InterruptAudioSource(requestedAudioSource);
|
|
SetupNewAudioSource(requestedAudioSource);
|
|
return true;
|
|
}
|
|
// If the requested audio source has the same priority as currently playing source, check the priority of the requested clip
|
|
if (currentlyPlayingVO.audioSourceType == AppleAudioSource.AudioSourceType.CriticalVO && currentlyPlayingVO.sourcePriority == requestedAudioSource.sourcePriority)
|
|
{
|
|
if (currentlyPlayingVO.clipPriority > requestedAudioSource.clipPriority)
|
|
{
|
|
InterruptAudioSource(requestedAudioSource);
|
|
SetupNewAudioSource(requestedAudioSource);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
// If the requested audio source has higher priority than the currently playing source, interrupt the current source and let the requested one play
|
|
if (currentlyPlayingVO.audioSourceType == AppleAudioSource.AudioSourceType.CriticalVO && currentlyPlayingVO.sourcePriority > requestedAudioSource.sourcePriority)
|
|
{
|
|
currentlyPlayingVO.InterruptAudio(requestedAudioSource.name);
|
|
InterruptAudioSource(requestedAudioSource);
|
|
SetupNewAudioSource(requestedAudioSource);
|
|
return true;
|
|
}
|
|
// If the requested audio source didn't clear any of the above cases, tell it to get rekt.
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void OnApplicationQuit()
|
|
{
|
|
// TODO: Release the handles safely ReleaseAllHandles();
|
|
}
|
|
|
|
private void SetupNewAudioSource(AppleAudioSource audioSource)
|
|
{
|
|
if (audioSource.audioSource.resource == null)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
currentlyPlayingVO = audioSource;
|
|
_eventSource = audioSource.audioSource.RequestEventHandlers();
|
|
_eventSource.AudioStopped += OnAudioStopped;
|
|
_eventSource.AudioStarted += OnAudioStarted;
|
|
}
|
|
}
|
|
|
|
private void OnAudioStopped(object sender, EventArgs e)
|
|
{
|
|
if (wasInterrupted)
|
|
{
|
|
ResetAudioSource();
|
|
}
|
|
else
|
|
{
|
|
currentlyPlayingVO = null;
|
|
ResetAudioSource();
|
|
}
|
|
}
|
|
|
|
private void OnAudioStarted(object sender, EventArgs e)
|
|
{
|
|
|
|
}
|
|
|
|
private void ResetAudioSource()
|
|
{
|
|
_eventSource.AudioStopped -= OnAudioStopped;
|
|
_eventSource.AudioStarted -= OnAudioStarted;
|
|
wasInterrupted = false;
|
|
}
|
|
|
|
private void InterruptAudioSource(AppleAudioSource newAudioSource)
|
|
{
|
|
wasInterrupted = true;
|
|
//currentlyPlayingVO.InterruptAudio(newAudioSource.name);
|
|
InterruptAllVOSources();
|
|
ResetAudioSource();
|
|
currentlyPlayingVO = newAudioSource;
|
|
}
|
|
|
|
private void InterruptAllVOSources()
|
|
{
|
|
foreach (AppleAudioSource source in criticalVOSources)
|
|
{
|
|
if (source == null)
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
source.InterruptAudio("GlobalInterrupt");
|
|
}
|
|
|
|
|
|
}
|
|
foreach (AppleAudioSource source in VOSources)
|
|
{
|
|
source.InterruptAudio("GlobalInterrupt");
|
|
|
|
}
|
|
}
|
|
}
|