Advertisement
mvaganov

Noisy.cs

Feb 8th, 2018 (edited)
554
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 18.96 KB | Source Code | 0 0
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4.  
  5. // author: mvaganov@hotmail.com
  6. // license: Copyfree, Unlicense, public domain.
  7.  
  8. /// <summary>
  9. /// puts sounds into a globally accessible space, accessible with <see cref="Noisy.PlaySound(string)"/>.
  10. /// the most recent noise groups of a given name get priority.
  11. /// removing an object with a Noisy removes it from the global space, which may reveal a previously added named noise.
  12. /// enabled workflow: default noises can load early, be overriden with new noisy, and revert if the noisy leaves.
  13. /// </summary>
  14. public class Noisy : MonoBehaviour {
  15.     public Noise[] noises = new Noise[1];
  16.     public bool removeNoisesWhenDestroyed = true;
  17.  
  18.     /// <summary>used for getting a random noise from a list of noises, without getting the one that was just played</summary>
  19.     /// <param name="minInclusive"></param>
  20.     /// <param name="maxExclusive"></param>
  21.     /// <param name="andNot"></param>
  22.     /// <returns></returns>
  23.     public static int RandomNumberThatIsnt(int minInclusive, int maxExclusive, int andNot = -1) {
  24.         int index = minInclusive;
  25.         if (maxExclusive - minInclusive > 1) {
  26.             if (andNot >= minInclusive && andNot < maxExclusive) {
  27.                 index = Random.Range(minInclusive, maxExclusive - 1);
  28.                 if (index >= andNot) { index++; }
  29.             } else {
  30.                 index = Random.Range(minInclusive, maxExclusive);
  31.             }
  32.         }
  33.         return index;
  34.     }
  35.  
  36.     [System.Serializable] public class Noise {
  37.         [Tooltip("global name for a sound. if sound is in another noisy, a duplicate name will reference it")]
  38.         public string name;
  39.         [Tooltip("sounds will be randomized from this list at runtime. can be empty if other noisy's have it")]
  40.         public AudioClip[] sounds = new AudioClip[1];
  41.         [Range(0, 1), Tooltip("to keep max volume, leave this at 0 (changes during runtime are fine, OnValidate)")]
  42.         public float volume = 1;
  43.         [Tooltip("set the background music to this? (if this is set, none of the other checkboxes below matter)")]
  44.         public bool backgroundMusic = false;
  45.         [ContextMenuItem("advanced keypress settings... (create component)", nameof(CreateOnKeyPress)),
  46.          Tooltip("play as soon as object starts? (eg: ambient sound/music or instantiated objects with 'birth' sounds)")]
  47.         public bool playOnStart = false;
  48.         [Tooltip("play at max volume and consistent pitch, regardless of distance? (eg: volume changes by distance, doppler effect)")]
  49.         public bool is2D = false;
  50.         [Tooltip("sound should attach to this object and follow it as it moves? (eg: ambient sound/music/dialog that follows a moving agent)")]
  51.         public bool followsObject = false;
  52.         [Tooltip("start the sound again right after it ends? (eg: ambient sound/music)")]
  53.         public bool loop = false;
  54.         [Tooltip("stop the previous-Noisy-with-the-same-Name before playing another one? (eg: character sound effects, UI feedback sounds, background music)")]
  55.         public bool onePlayAtATime = false;
  56.         [ContextMenuItem("advanced collision settings... (create component)", nameof(CreateOnCollision)),
  57.          Tooltip("play when this object collides with something? (eg: adding audio output to Rigidbody collision)")]
  58.         public bool onCollision = false;
  59.         [ContextMenuItem("advanced trigger settings... (create component)", nameof(CreateOnTrigger)),
  60.          Tooltip("play when an object enters this trigger? (eg: ambient noise, reactions to movement through space)")]
  61.         public bool onTrigger = false;
  62.         public PlayFromSoundbankBehavior playFromSoundbankBehavior = PlayFromSoundbankBehavior.RandomNoRepeat;
  63.         [HideInInspector]
  64.         /// The last noise played, used to prevent duplicate repetition
  65.         public int lastNoisePlayed = -1;
  66.         [HideInInspector] public AudioSource activeAudioSource;
  67.  
  68.         public enum PlayFromSoundbankBehavior { RandomNoRepeat, RandomAllowRepeat, InOrder }
  69.  
  70.         /// Plays the sound. Cannot have the sound follow the object because a position is given, not a transform
  71.         /// <returns>The sound.</returns>
  72.         /// <param name="p">where the noise is</param>
  73.         public AudioSource PlaySound(Vector3 p) {
  74.             if (!backgroundMusic) {
  75.                 activeAudioSource = Noisy.PlaySound(GetSoundToPlay(), p, !is2D, loop, onePlayAtATime ? name : null, volume);
  76.             } else {
  77.                 activeAudioSource = Noisy.PlayBackgroundMusic(GetSoundToPlay(), volume);
  78.             }
  79.             return activeAudioSource;
  80.         }
  81.  
  82.         public AudioSource PlaySound(Transform t) {
  83.             activeAudioSource = PlaySound(t.position);
  84.             if (followsObject) { activeAudioSource.transform.SetParent(t); }
  85.             return activeAudioSource;
  86.         }
  87.  
  88.         public AudioClip GetSoundToPlay() { return GetSoundToPlay(ref lastNoisePlayed); }
  89.  
  90.         public AudioClip GetSoundToPlay(ref int indexNotToPlayNext) {
  91.             if (sounds != null && sounds.Length > 0) {
  92.                 switch (playFromSoundbankBehavior) {
  93.                     case PlayFromSoundbankBehavior.RandomNoRepeat:
  94.                         indexNotToPlayNext = RandomNumberThatIsnt(0, sounds.Length, indexNotToPlayNext);
  95.                         break;
  96.                     case PlayFromSoundbankBehavior.RandomAllowRepeat:
  97.                         indexNotToPlayNext = RandomNumberThatIsnt(0, sounds.Length);
  98.                         break;
  99.                     case PlayFromSoundbankBehavior.InOrder:
  100.                         if (++indexNotToPlayNext >= sounds.Length) { indexNotToPlayNext = 0; }
  101.                         break;
  102.                 }
  103.                 return sounds[indexNotToPlayNext];
  104.             }
  105.             return null;
  106.         }
  107.  
  108.         /// <summary>comparer, used to sort Noise objects into the list</summary>
  109.         public class Comparer : IComparer<Noise> {
  110.             public int Compare(Noise x, Noise y) { return x.name.CompareTo(y.name); }
  111.         }
  112.         public static Comparer compare = new Comparer();
  113.     }
  114.  
  115.     void Awake() {
  116.         // sort noises for faster access later
  117.         System.Array.Sort(noises, Noise.compare);
  118.         EnsureNoisesInGlobalSpace();
  119.     }
  120.  
  121.     public void EnsureNoisesInGlobalSpace() {
  122.         // add all named noises to a single static (global) listing, for easy scripted access later
  123.         for (int i = 0; i < noises.Length; ++i) {
  124.             if (noises[i].name == null || noises[i].name.Length == 0) { continue; }
  125.             EnsureNoiseInGlobalSpace(noises[i], this);
  126.         }
  127.     }
  128.  
  129.     public void RemoveNoisesFromGlobalSpace() {
  130.         for (int i = 0; i < noises.Length; ++i) {
  131.             bool hasSoundsFilledOut = noises[i].sounds != null && noises[i].sounds.Length > 0;
  132.             if (!hasSoundsFilledOut) { continue; }
  133.             RemoveNoiseFromGlobalSpace(noises[i], this);
  134.         }
  135.     }
  136.  
  137.     public static void EnsureNoiseInGlobalSpace(Noise noise, Noisy src) {
  138.         Global.NoiseSet whatToFind = new Global.NoiseSet(noise.name);
  139.         int index = Global.allNoises.BinarySearch(whatToFind, Global.NoiseSet.compare);
  140.         bool isAlreadyKnown = index >= 0;
  141.         bool hasSoundsFilledOut = noise.sounds != null && noise.sounds.Length > 0;
  142.         if (!hasSoundsFilledOut) { return; }
  143.         // if this is the first time a noise has been added
  144.         if (!isAlreadyKnown) {
  145.             whatToFind.Add(noise, src);
  146.             Global.allNoises.Insert(~index, whatToFind);
  147.         } else {
  148.             Global.allNoises[index].ExistingInsertLogic(noise, src);
  149.         }
  150.     }
  151.  
  152.     public static void RemoveNoiseFromGlobalSpace(Noise noise, Noisy src) {
  153.         Global.NoiseSet noiseSet = new Global.NoiseSet(noise.name);
  154.         int index = Global.allNoises.BinarySearch(noiseSet, Global.NoiseSet.compare);
  155.         if (index < 0) { return; }
  156.         noiseSet = Global.allNoises[index];
  157.         noiseSet.RemoveNoisesFrom(src);
  158.         if (noiseSet.entries.Count != 0) { return; }
  159.         Global.allNoises.RemoveAt(index);
  160.         bool found = s_soundPlayersByCategory.TryGetValue(noise.name, out AudioSource asrc);
  161.         if (!found) { return; }
  162.         if (asrc == null || !asrc.isPlaying) {
  163.             s_soundPlayersByCategory.Remove(noise.name);
  164.         } else {
  165.             WhenDonePlaying whenDone = src.gameObject.AddComponent<WhenDonePlaying>();
  166.             whenDone.asrc = asrc;
  167.             whenDone.whatElseToDoWhenFinished = () => { s_soundPlayersByCategory.Remove(noise.name); };
  168.         }
  169.     }
  170.  
  171.     public class WhenDonePlaying : MonoBehaviour {
  172.         public AudioSource asrc;
  173.         public System.Action whatElseToDoWhenFinished;
  174.         private void Update() {
  175.             if (asrc != null && asrc.isPlaying) { return; }
  176.             whatElseToDoWhenFinished.Invoke();
  177.             Destroy(this);
  178.         }
  179.     }
  180.  
  181.     /// plays the first sound in the noises list
  182.     public void DoActivateTrigger() {
  183.         if (noises.Length == 0 || noises[0] == null) { return; }
  184.         noises[0].PlaySound(transform.position);
  185.     }
  186.  
  187.     /// plays the first sound in the noises list
  188.     public void DoDeactivateTrigger() {
  189.         if (noises.Length == 0 || noises[0] == null || noises[0].activeAudioSource == null) { return; }
  190.         noises[0].activeAudioSource.Stop();
  191.     }
  192.  
  193.     /// returns the Noise that was created someplace in the scene with the given name
  194.     /// <returns>The sound.</returns>
  195.     /// <param name="name">Name.</param>
  196.     public static Noise GetSound(string name) {
  197.         Global.NoiseSet searched = new Global.NoiseSet(name);
  198.         int i = Global.allNoises.BinarySearch(searched, Global.NoiseSet.compare);
  199.         if (i >= 0) { return Global.allNoises[i].entries[0].noise; }
  200.         Debug.LogError("Could not find noise named \"" + name + "\". Valid names include:\n\""
  201.             + string.Join("\", \"", Global.AllNoiseNames) + "\"");
  202.         return null;
  203.     }
  204.  
  205.     void Start() {
  206.         System.Array.ForEach(noises, InitialProcessFor);
  207.     }
  208.  
  209.     private void InitialProcessFor(Noise n) {
  210.         if (n.sounds == null || n.sounds.Length == 0) {
  211.             Noise existing = GetSound(n.name);
  212.             if (existing != null) { n.sounds = existing.sounds; }
  213.         }
  214.         if (n.playOnStart || n.backgroundMusic) {
  215.             n.PlaySound(transform.position);
  216.         }
  217.         if (n.onCollision) {
  218.             Noisy.OnCollisionAdvancedSettings oc = CreateHandler<OnCollisionAdvancedSettings>(n.name);
  219.             oc.noise = n;
  220.         }
  221.         if (n.onTrigger) {
  222.             Noisy.OnTriggerAdvancedSettings oc = CreateHandler<OnTriggerAdvancedSettings>(n.name);
  223.             oc.noise = n;
  224.         }
  225.     }
  226.  
  227.     private void OnDestroy() {
  228.         if (!removeNoisesWhenDestroyed) { return; }
  229.         RemoveNoisesFromGlobalSpace();
  230.     }
  231.  
  232.     /// Plays the named sound (as a 2D sound, full volume)
  233.     public static AudioSource PlaySound(string name) {
  234.         Noise n = GetSound(name);
  235.         return (n != null) ? n.PlaySound(Vector3.zero) : null;
  236.     }
  237.  
  238.     public static AudioSource PlaySound(string name, float volume) {
  239.         return PlaySound(name, default, false, false, null, volume);
  240.     }
  241.  
  242.     public static AudioSource PlaySound(string name, Vector3 p, bool is3D, bool isLooped, string soundCategory, float volume) {
  243.         Noise n = GetSound(name);
  244.         return (n != null) ? PlaySound(n.GetSoundToPlay(), p, is3D, isLooped, soundCategory, volume) : null;
  245.     }
  246.  
  247.     public static AudioSource PlaySound(string name, Vector3 p) {
  248.         Noise n = GetSound(name);
  249.         return (n != null) ? n.PlaySound(p) : null;
  250.     }
  251.  
  252.     /// <param name="name"></param>
  253.     /// <param name="t">where to parent the noise (important for 3D sounds)</param>
  254.     /// <returns></returns>
  255.     public static AudioSource PlaySound(string name, Transform t) {
  256.         Noise n = GetSound(name);
  257.         return (n != null) ? n.PlaySound(t) : null;
  258.     }
  259.  
  260.     /// <summary>
  261.     /// every sound has it's own AudioSource. this allows overlapping sounds but only of different types.
  262.     /// </summary>
  263.     private static Dictionary<string, AudioSource> s_soundPlayersByCategory = new Dictionary<string, AudioSource>();
  264.  
  265.     /// Plays the sound.
  266.     /// <returns>Component where the sound is playing from.</returns>
  267.     /// <param name="noise">Noise. returns early if <c>null</c></param>
  268.     /// <param name="p">P. the location to play from. If is3D is false, this parameter is pretty useless.</param>
  269.     /// <param name="is3D">If set to false, sound plays without considering 3D-ness (full volume from anywhere).</param>
  270.     /// <param name="isLooped">If set to <c>true</c> is looped.</param>
  271.     /// <param name="soundCategory">If non-null, prevents multiple sounds with the same soundCategory from playing simultaneously. If null, each instance of the sound will be independent.</param>
  272.     /// <param name="volume"></param>
  273.     public static AudioSource PlaySound(AudioClip noise, Vector3 p, bool is3D, bool isLooped, string soundCategory, float volume) {
  274.         if (noise == null) return null;
  275.         AudioSource asrc = null;
  276.         if (soundCategory != null && soundCategory.Length > 0) {
  277.             s_soundPlayersByCategory.TryGetValue(soundCategory, out asrc);
  278.         }
  279.         if (asrc == null) {
  280.             string noiseName = (soundCategory != null) ? "(" + soundCategory + ")" : "<Noise: " + noise.name + ">";
  281.             GameObject go = new GameObject(noiseName);
  282.             asrc = go.AddComponent<AudioSource>();
  283.             if (soundCategory != null) {
  284.                 s_soundPlayersByCategory[soundCategory] = asrc;
  285.             }
  286.             asrc.transform.SetParent(Global.Instance().transform);
  287.         } else {
  288.             asrc.Stop();
  289.         }
  290.         asrc.clip = noise;
  291.         asrc.spatialBlend = is3D ? 1 : 0;
  292.         asrc.transform.position = p;
  293.         if (soundCategory == null && !isLooped) {
  294.             Destroy(asrc.gameObject, noise.length); // destroy the noise after it is done playing if not looped
  295.         }
  296.         asrc.loop = isLooped;
  297.         if (volume != asrc.volume) { asrc.volume = volume; }
  298.         asrc.Play();
  299.         return asrc;
  300.     }
  301.  
  302.     /// convenience method to play background music
  303.     /// <returns>The background music's AudioSource.</returns>
  304.     /// <param name="song">Song.</param>
  305.     /// <param name="volume">Volume.</param>
  306.     public static AudioSource PlayBackgroundMusic(AudioClip song, float volume)
  307.     {
  308.         AudioSource bgMusicPlayer = PlaySound(song, Vector3.zero, false, true, "{background music}", volume);
  309.         return bgMusicPlayer;
  310.     }
  311.  
  312.     /// creates an accessible listing to all sounds being used by Noisy, visible in the hierarchy & inspector. also handles some static logic.
  313.     public class Global : MonoBehaviour {
  314.         [System.Serializable] public class NoiseSet {
  315.             public string name;
  316.  
  317.             public List<NoiseSource> entries;
  318.  
  319.             [System.Serializable] public struct NoiseSource {
  320.                 public Noise noise; public Noisy src;
  321.                 public NoiseSource(Noise noise, Noisy src) { this.noise = noise; this.src = src; }
  322.             }
  323.  
  324.             public void Add(Noise noise, Noisy src) => Insert(-1, noise, src);
  325.  
  326.             public void Insert(int index, Noise noise, Noisy src) {
  327.                 if (entries == null) { entries = new List<NoiseSource>(); }
  328.                 if (index == -1) {
  329.                     index = entries.Count;
  330.                 }
  331.                 entries.Insert(index, new NoiseSource(noise, src));
  332.             }
  333.  
  334.             public void ExistingInsertLogic(Noise noise, Noisy noisy) {
  335.                 int inSet = entries.FindIndex(e => e.src == noisy);
  336.                 if (inSet != -1) {
  337.                     Insert(0, noise, noisy);
  338.                 } else {
  339.                     if (entries[inSet].noise == noise) { return; } // ignore duplicate calls
  340.                     Debug.LogError($"{this} already has an entry for {noise.name}, " +
  341.                         $"replacing old entry {entries[inSet].noise.name} with new entry {noise.name}");
  342.                     entries[inSet] = new NoiseSource(noise, noisy);
  343.                 }
  344.             }
  345.  
  346.             public int RemoveNoisesFrom(Noisy src) {
  347.                 int removed = -1;
  348.                 for (int i = entries.Count - 1; i >= 0; --i) {
  349.                     NoiseSource noiseSource = entries[i];
  350.                     if (noiseSource.src == src) {
  351.                         entries.RemoveAt(i);
  352.                         removed = i;
  353.                     }
  354.                 }
  355.                 return removed;
  356.             }
  357.  
  358.             public NoiseSet(string name) {
  359.                 this.name = name;
  360.                 entries = new List<NoiseSource>();
  361.             }
  362.             /// <summary>comparer, used to sort Noise objects into the list</summary>
  363.             public class Comparer : IComparer<NoiseSet> {
  364.                 public int Compare(NoiseSet x, NoiseSet y) { return x.name.CompareTo(y.name); }
  365.             }
  366.             public static Comparer compare = new Comparer();
  367.         }
  368.         /// All Noise objects with unique names and actual data in the 'sounds' array.
  369.         public static List<NoiseSet> allNoises = new List<NoiseSet>();
  370.  
  371.         private static Noisy.Global instance;
  372.         public static Noisy.Global Instance() {
  373.             if (instance == null) {
  374.                 if ((instance = FindObjectOfType(typeof(Noisy.Global)) as Noisy.Global) == null) {
  375.                     GameObject g = new GameObject("<" + typeof(Noisy.Global).Name + ">");
  376.                     instance = g.AddComponent<Noisy.Global>();
  377.                 }
  378.             }
  379.             return instance;
  380.         }
  381.         /// <summary>local members alias showing all noises (can be seen in the inspector, static members cannot)</summary>
  382.         [SerializeField] private List<NoiseSet> _allTheNoises;
  383.  
  384.         /// <summary>the names of all of the noises in a List</summary>
  385.         public static List<string> AllNoiseNames { get { return allNoises.ConvertAll(set => set.name); } }
  386.  
  387.         void Start() { _allTheNoises = allNoises; }
  388.     }
  389.  
  390.     TYPE CreateHandler<TYPE>(string nameOfNoise) where TYPE : NoisyHandler {
  391.         if (name != null) {
  392.             TYPE[] triggers = GetComponents<TYPE>();
  393.             for (int i = 0; i < triggers.Length; ++i) {
  394.                 if (triggers[i].advancedNoiseOverride == nameOfNoise)
  395.                     return triggers[i];
  396.             }
  397.         }
  398.         return gameObject.AddComponent<TYPE>();
  399.     }
  400.  
  401.     void CreateOnTrigger() { CreateHandler<OnTriggerAdvancedSettings>(null); }
  402.     void CreateOnCollision() { CreateHandler<OnCollisionAdvancedSettings>(null); }
  403.     void CreateOnKeyPress() { CreateHandler<OnKeyPressAdvancedSettings>(null); }
  404.  
  405.     public class NoisyHandler : MonoBehaviour {
  406.         [Tooltip("if this is set, Noise (below) will be overwritten at runtime by a Noise with this name")]
  407.         public string advancedNoiseOverride;
  408.         public Noise noise;
  409.         [Tooltip("remove this handler after playing the sound once. (eg: one-time acknowledgement)")]
  410.         public bool justOnce;
  411.         protected void NoisyHandlerStart() {
  412.             if (advancedNoiseOverride != null && advancedNoiseOverride.Length > 0) {
  413.                 noise = Noisy.GetSound(advancedNoiseOverride);
  414.             }
  415.         }
  416.         void Start() { NoisyHandlerStart(); }
  417.     }
  418.  
  419.     public class NoisyObjectInteractHandler : NoisyHandler {
  420.         [Tooltip("identify which GameObjects can trigger this. (eg: only player, only certain item)")]
  421.         public string triggersOnlyByTag;
  422.         public bool IsValidTrigger(GameObject go) {
  423.             return triggersOnlyByTag == null || triggersOnlyByTag.Length == 0 || go.tag == triggersOnlyByTag;
  424.         }
  425.     }
  426.  
  427.     public class OnTriggerAdvancedSettings : NoisyObjectInteractHandler {
  428.         void OnTriggerEnter(Collider c) {
  429.             if (!IsValidTrigger(c.gameObject)) return;
  430.             if (noise.followsObject) {
  431.                 noise.PlaySound(transform);
  432.                 if (justOnce) Destroy(this);
  433.             } else {
  434.                 noise.PlaySound(c.transform.position);
  435.             }
  436.         }
  437.     }
  438.  
  439.     public class OnCollisionAdvancedSettings : NoisyObjectInteractHandler {
  440.         void OnCollisionEnter(Collision c) {
  441.             if (!IsValidTrigger(c.gameObject)) return;
  442.             if (noise.followsObject) {
  443.                 noise.PlaySound(transform);
  444.                 if (justOnce) Destroy(this);
  445.             } else {
  446.                 noise.PlaySound(c.contacts[0].point);
  447.             }
  448.         }
  449.     }
  450.  
  451.     public class OnKeyPressAdvancedSettings : NoisyHandler {
  452.         public KeyCode key = KeyCode.None;
  453.         public enum KeyEvent { press, release, hold };
  454.         public KeyEvent eventType = KeyEvent.press;
  455.         public bool IsTriggered() {
  456.             switch (eventType) {
  457.                 case KeyEvent.press: return Input.GetKeyDown(key);
  458.                 case KeyEvent.release: return Input.GetKeyUp(key);
  459.                 case KeyEvent.hold: return Input.GetKey(key);
  460.             }
  461.             return false;
  462.         }
  463.         void Start() {
  464.             NoisyHandlerStart();
  465.             if (noise.sounds == null || noise.sounds.Length == 0) {
  466.                 Noisy n = GetComponent<Noisy>();
  467.                 if (n != null && n.noises != null && n.noises.Length > 0) {
  468.                     this.noise = n.noises[0];
  469.                 }
  470.             }
  471.         }
  472.         void Update() {
  473.             if (IsTriggered()) {
  474.                 noise.PlaySound(transform);
  475.                 if (justOnce) { enabled = false; }
  476.             }
  477.         }
  478.     }
  479.  
  480. #if UNITY_EDITOR
  481.     private void OnValidate()
  482.     {
  483.         if (noises == null) return;
  484.         for (int i = 0; i < noises.Length; ++i)
  485.         {
  486.             Noise n = noises[i];
  487.             if (n != null && n.activeAudioSource != null) { n.activeAudioSource.volume = n.volume; }
  488.         }
  489.     }
  490. #endif
  491. }
  492.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement