Advertisement
mvaganov

Noisy.cs

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