using System.Collections; using System.Collections.Generic; using UnityEngine; namespace MoreMountains.Tools { /// /// This class stores all the info related to items in a playlist /// public struct MMPlaylistPlayEvent { static private event Delegate OnEvent; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; } static public void Register(Delegate callback) { OnEvent += callback; } static public void Unregister(Delegate callback) { OnEvent -= callback; } public delegate void Delegate(int channel); static public void Trigger(int channel) { OnEvent?.Invoke(channel); } } public struct MMPlaylistStopEvent { static private event Delegate OnEvent; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; } static public void Register(Delegate callback) { OnEvent += callback; } static public void Unregister(Delegate callback) { OnEvent -= callback; } public delegate void Delegate(int channel); static public void Trigger(int channel) { OnEvent?.Invoke(channel); } } public struct MMPlaylistPauseEvent { static private event Delegate OnEvent; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; } static public void Register(Delegate callback) { OnEvent += callback; } static public void Unregister(Delegate callback) { OnEvent -= callback; } public delegate void Delegate(int channel); static public void Trigger(int channel) { OnEvent?.Invoke(channel); } } public struct MMPlaylistPlayNextEvent { static private event Delegate OnEvent; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; } static public void Register(Delegate callback) { OnEvent += callback; } static public void Unregister(Delegate callback) { OnEvent -= callback; } public delegate void Delegate(int channel); static public void Trigger(int channel) { OnEvent?.Invoke(channel); } } public struct MMPlaylistPlayPreviousEvent { static private event Delegate OnEvent; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; } static public void Register(Delegate callback) { OnEvent += callback; } static public void Unregister(Delegate callback) { OnEvent -= callback; } public delegate void Delegate(int channel); static public void Trigger(int channel) { OnEvent?.Invoke(channel); } } public struct MMPlaylistPlayIndexEvent { static private event Delegate OnEvent; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; } static public void Register(Delegate callback) { OnEvent += callback; } static public void Unregister(Delegate callback) { OnEvent -= callback; } public delegate void Delegate(int channel, int index); static public void Trigger(int channel, int index) { OnEvent?.Invoke(channel, index); } } [System.Serializable] public class MMPlaylistSong { /// the audiosource that contains the audio clip we want to play public AudioSource TargetAudioSource; /// the min (when it's off) and max (when it's playing) volume for this source [MMVector("Min", "Max")] public Vector2 Volume = new Vector2(1f, 1f); /// a random delay in seconds to apply, between its RMin and RMax [MMVector("RMin", "RMax")] public Vector2 InitialDelay = Vector2.zero; /// a random crossfade duration (in seconds) to apply when transitioning to this song, between its RMin and RMax [MMVector("RMin", "RMax")] public Vector2 CrossFadeDuration = new Vector2(2f, 2f); /// a random pitch to apply to this song, between its RMin and RMax [MMVector("RMin", "RMax")] public Vector2 Pitch = Vector2.one; /// the stereo pan for this song [Range(-1f, 1f)] public float StereoPan = 0f; /// the spatial blend for this song (0 is 2D, 1 is 3D) [Range(0f, 1f)] public float SpatialBlend = 0f; /// whether this song should loop or not public bool Loop = false; /// whether this song is playing right now or not [MMReadOnly] public bool Playing = false; /// whether this song is fading right now or not [MMReadOnly] public bool Fading = false; [MMHidden] public bool _initialized; public virtual void Initialization() { if (_initialized) { return; } this.Volume = new Vector2(1f, 1f); this.InitialDelay = Vector2.zero; this.CrossFadeDuration = new Vector2(2f, 2f); this.Pitch = Vector2.one; this.StereoPan = 0f; this.SpatialBlend = 0f; this.Loop = false; this._initialized = true; } } /// /// Use this class to play audiosources (usually background music but feel free to use that for anything) in sequence, with optional crossfade between songs /// [AddComponentMenu("More Mountains/Tools/Audio/MMPlaylist")] [MMRequiresConstantRepaint] public class MMPlaylist : MMMonoBehaviour { /// the possible states this playlist can be in public enum PlaylistStates { Idle, Playing, Paused } [MMInspectorGroup("Playlist Songs", true, 18)] /// the channel on which to broadcast orders for this playlist [Tooltip("the channel on which to broadcast orders for this playlist")] public int Channel = 0; /// the songs that this playlist will play [Tooltip("the songs that this playlist will play")] public List Songs; [MMInspectorGroup("Settings", true, 13)] /// whether this should play in random order or not [Tooltip("whether this should play in random order or not")] public bool RandomOrder = false; /// if this is true, random seed will be randomized by the system clock [Tooltip("if this is true, random seed will be randomized by the system clock")] [MMCondition("RandomOrder", true)] public bool RandomizeOrderSeed = true; /// whether this playlist should play and loop as a whole forever or not [Tooltip("whether this playlist should play and loop as a whole forever or not")] public bool Endless = true; /// whether this playlist should auto play on start or not [Tooltip("whether this playlist should auto play on start or not")] public bool PlayOnStart = true; /// a global volume multiplier to apply when playing a song [Tooltip("a global volume multiplier to apply when playing a song")] public float VolumeMultiplier = 1f; /// if this is true, this playlist will automatically pause/resume OnApplicationPause, useful if you've prevented your game from running in the background [Tooltip("if this is true, this playlist will automatically pause/resume OnApplicationPause, useful if you've prevented your game from running in the background")] public bool AutoHandleApplicationPause = true; [MMInspectorGroup("Persistence", true, 32)] /// if this is true, this playlist will persist from scene to scene [Tooltip("if this is true, this playlist will persist from scene to scene")] public bool Persistent = false; /// if this is true, this singleton will auto detach if it finds itself parented on awake [Tooltip("if this is true, this singleton will auto detach if it finds itself parented on awake")] [MMCondition("Persistent", true)] public bool AutomaticallyUnparentOnAwake = true; [MMInspectorGroup("Status", true, 14)] /// the current state of the playlist, debug display only [Tooltip("the current state of the playlist, debug display only")] [MMReadOnly] public PlaylistStates DebugCurrentState = PlaylistStates.Idle; /// the index we're currently playing [Tooltip("the index we're currently playing")] [MMReadOnly] public int CurrentlyPlayingIndex = -1; /// the name of the song that is currently playing [Tooltip("the name of the song that is currently playing")] [MMReadOnly] public string CurrentSongName; /// the current state of this playlist [MMReadOnly] public MMStateMachine PlaylistState; [MMInspectorGroup("Tests", true, 15)] /// a play test button [MMInspectorButton("Play")] public bool PlayButton; /// a pause test button [MMInspectorButton("Pause")] public bool PauseButton; /// a stop test button [MMInspectorButton("Stop")] public bool StopButton; /// a next song test button [MMInspectorButton("PlayNextSong")] public bool NextButton; /// the index of the song to play when pressing the PlayTargetSong button [Tooltip("the index of the song to play when pressing the PlayTargetSong button")] public int TargetSongIndex = 0; /// a next song test button [MMInspectorButton("PlayTargetSong")] public bool TargetSongButton; protected int _songsPlayedSoFar = 0; protected int _songsPlayedThisCycle = 0; protected Coroutine _coroutine; protected bool _shouldResumeOnApplicationPause = false; public static bool HasInstance => _instance != null; public static MMPlaylist Current => _instance; protected static MMPlaylist _instance; protected bool _enabled; /// /// Singleton design pattern /// /// The instance. public static MMPlaylist Instance { get { if (_instance == null) { _instance = FindObjectOfType (); if (_instance == null) { GameObject obj = new GameObject (); obj.name = typeof(MMPlaylist).Name + "_AutoCreated"; _instance = obj.AddComponent (); } } return _instance; } } /// /// On awake, we check if there's already a copy of the object in the scene. If there's one, we destroy it. /// protected virtual void Awake () { InitializeSingleton(); } /// /// Initializes the singleton. /// protected virtual void InitializeSingleton() { if (!Application.isPlaying) { return; } if (!Persistent) { return; } if (AutomaticallyUnparentOnAwake) { this.transform.SetParent(null); } if (_instance == null) { //If I am the first instance, make me the Singleton _instance = this; DontDestroyOnLoad (transform.gameObject); _enabled = true; } else { //If a Singleton already exists and you find //another reference in scene, destroy it! if(this != _instance) { Destroy(this.gameObject); } } } /// /// On Start we initialize our playlist /// protected virtual void Start() { Initialization(); } /// /// On init we initialize our state machine and start playing if needed /// protected virtual void Initialization() { if (RandomOrder && RandomizeOrderSeed) { Random.InitState(System.Environment.TickCount); } _songsPlayedSoFar = 0; PlaylistState = new MMStateMachine(this.gameObject, true); ChangePlaylistState(PlaylistStates.Idle); if (Songs.Count == 0) { return; } if (PlayOnStart) { PlayFirstSong(); } } protected virtual void ChangePlaylistState(PlaylistStates newState) { PlaylistState.ChangeState(newState); DebugCurrentState = newState; } /// /// Picks and plays the first song /// protected virtual void PlayFirstSong() { _songsPlayedThisCycle = 0; CurrentlyPlayingIndex = -1; int newIndex = PickNextIndex(); _coroutine = StartCoroutine(PlaySong(newIndex)); } /// /// Plays a new song in the playlist, and stops / fades the previous one /// /// /// protected virtual IEnumerator PlaySong(int index) { // if we don't have a song, we stop if (Songs.Count == 0) { yield break; } // if we've played all our songs, we stop if (!Endless && (_songsPlayedThisCycle > Songs.Count)) { yield break; } if (_coroutine != null) { StopCoroutine(_coroutine); } // we stop our current song if ((PlaylistState.CurrentState == PlaylistStates.Playing) && (index >= 0 && index < Songs.Count) && !Songs[index].TargetAudioSource.isPlaying) { StartCoroutine(Fade(CurrentlyPlayingIndex, Random.Range(Songs[index].CrossFadeDuration.x, Songs[index].CrossFadeDuration.y), Songs[CurrentlyPlayingIndex].Volume.y * VolumeMultiplier, Songs[CurrentlyPlayingIndex].Volume.x * VolumeMultiplier, true)); } // we stop all other coroutines if ((CurrentlyPlayingIndex >= 0) && (Songs.Count > CurrentlyPlayingIndex)) { foreach (MMPlaylistSong song in Songs) { if (song != Songs[CurrentlyPlayingIndex]) { song.Fading = false; } } } if (index < 0 || index >= Songs.Count) { yield break; } // initial delay yield return MMCoroutine.WaitFor(Random.Range(Songs[index].InitialDelay.x, Songs[index].InitialDelay.y)); if (Songs[index].TargetAudioSource == null) { Debug.LogError(this.name + " : the playlist song you're trying to play is null"); yield break; } Songs[index].TargetAudioSource.pitch = Random.Range(Songs[index].Pitch.x, Songs[index].Pitch.y); Songs[index].TargetAudioSource.panStereo = Songs[index].StereoPan; Songs[index].TargetAudioSource.spatialBlend = Songs[index].SpatialBlend; Songs[index].TargetAudioSource.loop = Songs[index].Loop; // fades the new song's volume StartCoroutine(Fade(index, Random.Range(Songs[index].CrossFadeDuration.x, Songs[index].CrossFadeDuration.y), Songs[index].Volume.x * VolumeMultiplier, Songs[index].Volume.y * VolumeMultiplier, false)); // starts the new song Songs[index].TargetAudioSource.Play(); // updates our state CurrentSongName = Songs[index].TargetAudioSource.clip.name; ChangePlaylistState(PlaylistStates.Playing); Songs[index].Playing = true; CurrentlyPlayingIndex = index; _songsPlayedSoFar++; _songsPlayedThisCycle++; while (Songs[index].TargetAudioSource.isPlaying || (PlaylistState.CurrentState == PlaylistStates.Paused) || _shouldResumeOnApplicationPause) { yield return null; } if (PlaylistState.CurrentState != PlaylistStates.Playing) { yield break; } if (_songsPlayedSoFar < Songs.Count) { _coroutine = StartCoroutine(PlaySong(PickNextIndex())); } else { if (Endless) { _coroutine = StartCoroutine(PlaySong(PickNextIndex())); } else { ChangePlaylistState(PlaylistStates.Idle); } } } /// /// Fades an audiosource in or out, optionnally stopping it at the end /// /// /// /// /// /// /// protected virtual IEnumerator Fade(int index, float duration, float initialVolume, float endVolume, bool stopAtTheEnd) { if (index < 0 || index >= Songs.Count) { yield break; } float startTimestamp = Time.time; float progress = 0f; Songs[index].Fading = true; while ((Time.time - startTimestamp < duration) && (Songs[index].Fading)) { progress = MMMaths.Remap(Time.time - startTimestamp, 0f, duration, 0f, 1f); Songs[index].TargetAudioSource.volume = Mathf.Lerp(initialVolume, endVolume, progress); yield return null; } Songs[index].TargetAudioSource.volume = endVolume; if (stopAtTheEnd) { Songs[index].TargetAudioSource.Stop(); Songs[index].Playing = false; Songs[index].Fading = false; } } /// /// Picks the next song to play /// /// protected virtual int PickNextIndex() { if (Songs.Count == 0) { return -1; } int newIndex = CurrentlyPlayingIndex; if (RandomOrder) { while (newIndex == CurrentlyPlayingIndex) { newIndex = Random.Range(0, Songs.Count); } } else { newIndex = (CurrentlyPlayingIndex + 1) % Songs.Count; } return newIndex; } /// /// Picks the previous song to play /// /// protected virtual int PickPreviousIndex() { if (Songs.Count == 0) { return -1; } int newIndex = CurrentlyPlayingIndex; if (RandomOrder) { while (newIndex == CurrentlyPlayingIndex) { newIndex = Random.Range(0, Songs.Count); } } else { newIndex = (CurrentlyPlayingIndex - 1); if (newIndex < 0) { newIndex = Songs.Count - 1; } } return newIndex; } /// /// Plays either the first song or resumes playing a paused one /// public virtual void Play() { switch (PlaylistState.CurrentState) { case PlaylistStates.Idle: PlayFirstSong(); break; case PlaylistStates.Paused: Songs[CurrentlyPlayingIndex].TargetAudioSource.UnPause(); ChangePlaylistState(PlaylistStates.Playing); break; case PlaylistStates.Playing: // do nothing break; } } public virtual void PlayAtIndex(int songIndex) { _coroutine = StartCoroutine(PlaySong(songIndex)); } /// /// Pauses the current song /// public virtual void Pause() { if (PlaylistState.CurrentState != PlaylistStates.Playing) { return; } Songs[CurrentlyPlayingIndex].TargetAudioSource.Pause(); ChangePlaylistState(PlaylistStates.Paused); } /// /// Stops the playlist /// public virtual void Stop() { if (PlaylistState.CurrentState != PlaylistStates.Playing) { return; } Songs[CurrentlyPlayingIndex].TargetAudioSource.Stop(); Songs[CurrentlyPlayingIndex].Playing = false; Songs[CurrentlyPlayingIndex].Fading = false; CurrentlyPlayingIndex = -1; ChangePlaylistState(PlaylistStates.Idle); } /// /// Plays the next song in the playlist /// public virtual void PlayNextSong() { int newIndex = PickNextIndex(); _coroutine = StartCoroutine(PlaySong(newIndex)); } /// /// Plays the previous song in the playlist /// public virtual void PlayPreviousSong() { int newIndex = PickPreviousIndex(); _coroutine = StartCoroutine(PlaySong(newIndex)); } protected virtual void PlayTargetSong() { int newIndex = Mathf.Clamp(TargetSongIndex, 0, Songs.Count - 1); PlayAtIndex(newIndex); } protected virtual void OnPlayEvent(int channel) { if (channel != Channel) { return; } Play(); } protected virtual void OnPauseEvent(int channel) { if (channel != Channel) { return; } Pause(); } protected virtual void OnStopEvent(int channel) { if (channel != Channel) { return; } Stop(); } protected virtual void OnPlayNextEvent(int channel) { if (channel != Channel) { return; } PlayNextSong(); } protected virtual void OnPlayPreviousEvent(int channel) { if (channel != Channel) { return; } PlayPreviousSong(); } protected virtual void OnPlayIndexEvent(int channel, int index) { if (channel != Channel) { return; } _coroutine = StartCoroutine(PlaySong(index)); } /// /// On enable, starts listening for playlist events /// protected virtual void OnEnable() { MMPlaylistPauseEvent.Register(OnPauseEvent); MMPlaylistPlayEvent.Register(OnPlayEvent); MMPlaylistPlayNextEvent.Register(OnPlayNextEvent); MMPlaylistPlayPreviousEvent.Register(OnPlayPreviousEvent); MMPlaylistStopEvent.Register(OnStopEvent); MMPlaylistPlayIndexEvent.Register(OnPlayIndexEvent); } /// /// On disable, stops listening for playlist events /// protected virtual void OnDisable() { MMPlaylistPauseEvent.Unregister(OnPauseEvent); MMPlaylistPlayEvent.Unregister(OnPlayEvent); MMPlaylistPlayNextEvent.Unregister(OnPlayNextEvent); MMPlaylistPlayPreviousEvent.Unregister(OnPlayPreviousEvent); MMPlaylistStopEvent.Unregister(OnStopEvent); MMPlaylistPlayIndexEvent.Unregister(OnPlayIndexEvent); } protected bool _firstDeserialization = true; protected int _listCount = 0; /// /// On Validate, we check if our array has changed and if yes we initialize our new elements /// protected virtual void OnValidate() { if (_firstDeserialization) { if (Songs == null) { _listCount = 0; _firstDeserialization = false; } else { _listCount = Songs.Count; _firstDeserialization = false; } } else { if (Songs.Count != _listCount) { if (Songs.Count > _listCount) { foreach(MMPlaylistSong song in Songs) { song.Initialization(); } } _listCount = Songs.Count; } } } /// /// On ApplicationPause, we pause the playlist and resume it afterwards /// /// protected virtual void OnApplicationPause(bool pauseStatus) { if (!AutoHandleApplicationPause) { return; } if (pauseStatus && PlaylistState.CurrentState == PlaylistStates.Playing) { Pause(); _shouldResumeOnApplicationPause = true; } if (!pauseStatus && _shouldResumeOnApplicationPause) { _shouldResumeOnApplicationPause = false; Play(); } } } }