- Home /
Play Audio in sequence from children objects
Hi everyone, I am asking your help.. after two days of trying and digging forums and tutorials.. I have in my scene an object called "collector". This object will move around the space and if it collides (trigger enter) with an object tag "sculpts", this become a child of the collector. So far so good..
Each of this "sculpts" objects have an AudioSource with a clip. I need to do that when the collector has one or more child objects, it checks the AudioSources of each child, and play the clips of each child one after the other.. in the order of the child. This is the script I came up with, but I get an error and does not work and I have no idea how to solve this.. Please anyone out here! Help! <3
Here is the code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class audioSequencer : MonoBehaviour
{
private GameObject parentObject;
private AudioSource[] myAudioSources;
private AudioClip[] clips;
private int childCount;
private bool playNow = false;
private int cnt = 0;
private void Awake()
{
parentObject = this.gameObject;
}
void OnTriggerEnter(Collider other)
{
childCount = transform.childCount;
myAudioSources = parentObject.GetComponentsInChildren<AudioSource>();
Debug.Log(myAudioSources.Length);
foreach (AudioSource myAudioSource in myAudioSources)
{
clips = parentObject.GetComponentsInChildren<AudioClip>();
}
}
private void Update()
{
if (childCount > 0)
{
playNow = !playNow;
foreach (AudioSource myAudioSource in myAudioSources)
{
myAudioSource.Stop();
}
}
if (playNow)
{
PlaySounds();
}
}
public void PlaySounds()
{
while (true)
{
for (int i = 0; i < myAudioSources.Length; i++)
{
if (!myAudioSources[i].isPlaying && cnt < clips.Length)
{
myAudioSources[i].clip = clips[cnt];
myAudioSources[i].Play();
cnt = cnt + 1;
}
if (cnt == clips.Length)
{
cnt = 0;
}
}
}
}
}
and this is the error I get:
ArgumentException: GetComponent requires that the requested component 'AudioClip' derives from MonoBehaviour or Component or is an interface.
UnityEngine.GameObject.GetComponentsInChildren[AudioClip] (Boolean includeInactive) (at /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/GameObjectBindings.gen.cs:142)
UnityEngine.GameObject.GetComponentsInChildren[AudioClip] () (at /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/GameObjectBindings.gen.cs:159)
audioSequencer.OnTriggerEnter (UnityEngine.Collider other) (at Assets/Scripts/audioSequencer.cs:30)
Do you need to replay all audio clips of children (for example play 1,2,3... and again 1,2,3...)? Or you just want to play once every audio clip and remove it from sequence (for example play 1,2,3... and wait for new collisions and play them)? I can help you with right code if clarify this moment.
I am not sure yet, I think the script of Captain_Pineapple works great, but would be cool to know how to have the sequence played once only, just in case. But Having the sequence working at all is SUPER for now! I was so frustrated eheh :-P
Answer by Captain_Pineapple · Jun 10, 2018 at 11:17 AM
Hey there,
There are some things that you look into:
AudioClips are no Components of Objects. So there is no way to get them by calling GetComponent with AudioClip as Type. Also NEVER implement a while-true loop in a function without a way to break it. You will just loop infinetly with your programm freezing. Also you don't need it since an audiosource that is set to play will just play it clip until it finishes.
i adjusted your script to the way you probably want it to work.
public class audioSequencer : MonoBehaviour
{
private List<AudioSource> myAudioSources;
//private int childCount;
private int cnt = 0;
public void Start()
{
myAudioSources = new List<AudioSource>();
}
void OnTriggerEnter(Collider other)
{
//check if entered object has an audiosource attached:
if(other.GetComponent<AudioSource>())
{
//add to list:
myAudioSources.Add(other.GetComponent<AudioSource>());
//check if this is the first object:
if(myAudioSources.Count == 1)
{
//if yes play it, from here the update will take over playing the clips
myAudioSources[0].Play();
}
}
}
private void Update()
{
if(myAudioSources.Count > 0)
{
//check if current clip has ended:
if(!myAudioSources[cnt].isPlaying)
{
cnt += 1;
//reset count if we reched the end of the current List:
if(cnt >= myAudioSources.Count)
{
cnt = 0;
}
//play next clip:
myAudioSources[cnt].Play();
}
}
}
}
Please note that i did not test the code for functionality and it will only work as inteded when your audioSources do NOT have the loop checkbox set to true.
If something is unclear or does not work as inteded feel free to ask.
O$$anonymous$$G THAN$$anonymous$$ YOU SO $$anonymous$$UCH! I COULD NOT SLEEP LAST NIGHT BECAUSE OF THIS! I am fairly new to code, so some things are still very unclear to me (like the while-true loop) but I am trying to learn by doing.. and sometimes it gets too hard for my brain to solve.. Your script works perfectly! thank you so much!
just in case: if I want the audio sequence being repeated just once, I would need to move it to a separate void (so not update but lets say, PlaySounds()) and use PlayOneShot ins$$anonymous$$d?
Hey,
glad it helped. If you want everything to play only once implement another List (`playedSources`) to keep track of everything.
Then ins$$anonymous$$d of increasing the counter, add the zero element from the list to playedSources
and afterwards remove it from myAudioSources
using .RemoveAt(0)
Then again Play the first Clip from the AudiosourcesList BUT only of one exists. With this version the List may get empty.
Answer by bakir-omarov · Jun 10, 2018 at 12:08 PM
It is hard to understand what you mean by playing once. Because you can collect 4 triggers at once, play all of them by sequence, after it save them for next playing. And play when you will collect some new triggers also. For example:
1) you collected 3 triggers (audiosources), you begin play 1,2,3. - and stop.
2) after 10 seconds you collected 2 more triggers, so it will play : 1,2,3,4,5 - and stop.
If you want to clear your audisources after playing once, and wait for new triggers, then just uncomment playableAudioSources.Clear(); . And it will like:
1) you collected 3 triggers (audiosources), you begin play 1,2,3. - and stop, and delete them from list.
2) after 10 seconds you collected 2 more triggers, so it will play : 4,5 - and stop, and delete them from list.
This will help you :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class delete : MonoBehaviour
{
public List<AudioSource> playableAudioSources; // Use List because it is dynamic
public bool audioPlaying;
void Awake()
{
playableAudioSources = new List<AudioSource>();
}
void OnTriggerEnter(Collider other)
{
// checking if trigger has a audiosource, and if we already added it to our list. If added already then it will skip it. Because OnTriggerEnter can be called many times
if (other.GetComponent<AudioSource>() && !playableAudioSources.Contains(other.GetComponent<AudioSource>()))
{
//add to list:
playableAudioSources.Add(other.GetComponent<AudioSource>());
if (!audioPlaying)
{
audioPlaying = true;
StartCoroutine(PlaySounds());
}
}
}
private IEnumerator PlaySounds()
{
// play all audisources one by one, if you will add some new audiosource while playing, it will be added to the end of list and played also
for (int i = 0; i < playableAudioSources.Count; i++)
{
// check if current is not playing
if (!playableAudioSources[i].isPlaying)
{
playableAudioSources[i].Play();
// wait for the end of current audiosource
yield return new WaitForSeconds(playableAudioSources[i].clip.length);
}
}
//// if you want to clear your audiosources after playing, just use it.
//playableAudioSources.Clear(); // uncomment me !
// lets say to the game that we finished playing one cicle of audisources, so if we will hit one more trigger, we can play it again
audioPlaying = false;
}
}
Answer by Mijn · Jun 11, 2018 at 05:13 PM
thanks bakir-omarov! this solution of yours is very nice! I implemented on my current script,it is for SteamVR / HTC Vive. It is attached to the controller. If I press the grip button while ontrigger, I can grab objects. Each grabbed object has the sound played in order (this part all works). When I press the trigger, I release the children (set parent to null again), audio stops but somehow I need to wait (I think) the coroutine to finish the loop before I can properly use it again. Any idea why?
(for some reason it won't load the code as comment but only as answer..)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class VRControllerInput : MonoBehaviour
{
protected List<Collectable> heldObjects;
private Valve.VR.EVRButtonId gripButton = Valve.VR.EVRButtonId.k_EButton_Grip;
private Valve.VR.EVRButtonId triggerButton = Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger;
protected SteamVR_TrackedObject trackedObj;
public SteamVR_Controller.Device device { get { return SteamVR_Controller.Input((int)trackedObj.index); } }
public List<AudioSource> playableAudioSources; // Use List because it is dynamic
public bool audioPlaying;
public static bool needsToStop;
private void Awake()
{
trackedObj = GetComponent<SteamVR_TrackedObject>();
heldObjects = new List<Collectable>();
playableAudioSources = new List<AudioSource>();
}
private void OnTriggerStay(Collider other)
{
Collectable collectable = other.GetComponent<Collectable>();
if (collectable != null)
{
if (device.GetPressDown(gripButton))
{
collectable.PickUP(this);
heldObjects.Add(collectable);
needsToStop = false;
// checking if trigger has a audiosource, and if we already added it to our list. If added already then it will skip it.
if (collectable.GetComponent<AudioSource>() && !playableAudioSources.Contains(collectable.GetComponent<AudioSource>()))
{
//add to list:
playableAudioSources.Add(other.GetComponent<AudioSource>());
if (!audioPlaying)
{
audioPlaying = true;
StartCoroutine(PlaySounds());
}
}
}
}
}
private void Update()
{
if (heldObjects.Count > 0)
{
if (device.GetPressDown(triggerButton))
{
for (int i = 0; i < heldObjects.Count; i++)
{
heldObjects[i].Release(this);
}
for (int i = 0; i < playableAudioSources.Count; i++)
{
playableAudioSources[i].Stop();
}
heldObjects = new List<Collectable>();
playableAudioSources = new List<AudioSource>();
}
}
}
private IEnumerator PlaySounds()
{
for (int i = 0; i < playableAudioSources.Count; i++)
{
if (!playableAudioSources[i].isPlaying)
{
playableAudioSources[i].Play();
// wait for the end of current audiosource
yield return new WaitForSeconds(playableAudioSources[i].clip.length);
}
//// if you want to clear your audiosources after playing, just use it.
//playableAudioSources.Clear(); // uncomment me !
// lets say to the game that we finished playing one cicle of audisources, so if we will hit one more trigger, we can play it again
audioPlaying = false;
}
}
}
You don't want to stop playing all sequenced sounds after deleting them from child, you want to continue playing current loop(sequence), and then stop and wait for another action(ontriggerenter) ? Or what you want?
at the moment when I delete the child, the audio stop (I can not hear it), but somehow if I grab an object (make a new child) too quickly, the sound of this object is not played. It seems to me, that I have to wait the coroutine to "stop play the length of the sound before". Sorry if Im not super clear, I do not have yet the proper vocabulary for scripting.. your script as it was works great. I just want to break the coroutine (I guess) if children are detach (so if there are no children, after button press trigger, audio and coroutine for audio play or waiting for new audio should stop).
but somehow if I grab an object (make a new child) too quickly, the sound of this object is not played.
So you want to play it? if you released all objects , and instantly grabbed one or several of them, you want to play it again?