I want to achieve playing the music one by one in Unity. I tried to create an audioClip array and want to play them one by one. And i also tried to use StartCoroutine to wait one song finished and then playing the next.
I tried to create an audioClip array and want to play them one by one. And i also tried to use StartCoroutine to wait one song finished and then playing the next.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
[RequireComponent(typeof(AudioSource))]
public class AudioManager : MonoBehaviour {
public AudioClip[] _audioClips;
private AudioSource _audioSource;
void Awake(){
_audioSource = GetComponent<AudioSource>();
}
// Use this for initialization
void Start () {
for (int i = 0; i < _audioClips.Length;i++){
_audioSource.PlayOneShot(_audioClips[i]);
StartCoroutine("WaitForMusicEnd");
}
}
IEnumerator WaitForMusicEnd()
{
while (_audioSource.isPlaying)
{
yield return null;
}
}
}
However, the music will play at the same time. Please help!
you are not too far off, but this is not how coroutine work - you need to be insde a coroutine to be able to wait, here's how you do it
void Start ()
{
StartCoroutine(PlayMusic());
}
IEnumerator PlayMusic()
{
for (int i = 0; i < _audioClips.Length;i++)
{
_audioSource.PlayOneShot(_audioClips[i]);
while (_audioSource.isPlaying)
yield return null;
}
}
The control flow goes like this:
void Start()
{
StartCoroutine(Foo());
StartCoroutine(Bar());
}
IEnumerator Foo()
{
Debug.Log("FOO");
yield return null;
Debug.Log("foo");
}
IEnumerator Bar()
{
Debug.Log("BAR");
yield return null;
Debug.Log("bar");
}
// FOO
// BAR
// foo
// bar
If you watch what happens: With each coroutine start, the control goes into the coroutine, up until the first yield return. At this point we rewind the instruction pointer back to Start(), and start the second coroutine. Then Start() finishes, the frame is drawn. Unity keeps track of running coroutines, and before next frame will return control to where you left within your coroutine.
Its quite clever, as it makes spreading things in time much easier.
Related
I am fairly new to Unity and I really don't know how to directly ask this question. Basicaly I am just playing around with Unitiy in general and I am trying to create script that initially has it set to active then disappear after 3 seconds but then come back as active again, can anyone explain anything specific that has to do with this problem? This is my code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScriptTwo : MonoBehaviour
{
public GameObject theBall;
// Start is called before the first frame update
void Start()
{
theBall.SetActive(true);
StartCoroutine(BallCode());
}
IEnumerator BallCode() {
yield return new WaitForSeconds(3);
theBall.SetActive(false);
Debug.Log("Set Active: False");
Debug.Log("Test Before");
yield return new WaitForSeconds(3);
theBall.SetActive(true);
}
}
What your code should be doing is:
-activating
-waiting 3 seconds
-disabling
-waiting 3 seconds
-activating
If it is doing anything other than that it is a problem with another code you wrote. If you wanted it to continue infinitly than:
IEnumerator BallCode()
{
while (true)
{
yield return new WaitForSeconds(3);
theBall.SetActive(!theBall.activeInHierarchy);
Debug.Log("Set Active: " + theBall.activeInHierarchy);
}
}
If your script is in the gameObject you are deactivating (in this instance the ball) you should move it to another gameObject.
All Unity Monobehaviour methods and by extent coroutines, are halted/stopped when the gameObject is deactivated.
This is a script I'm attaching to a button in order to load the next level when clicked
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.SceneManagement;
public class LoadNextLevel : MonoBehaviour
{
static Action<string> LoadNextLvl;
void Start()
{
//The issue isn't with the delegate because i tried running the
//code without it
LoadNextLvl += (string name) => StartCoroutine("LoadFunc" , name);
}
IEnumerator LoadFunc(string name)
{
//This is to load the next level, it has nothing to do with the bug
int levelnum = int.Parse(name[5].ToString());
levelnum++;
SceneManager.LoadScene("Loading");
//i want to wait for a certain amount of time then load the next scene
yield return new WaitForSeconds(2f);
//For some reason the code stops here and doesn't continue even after
//I wait, in fact I tried yield return null but the same issue still
//occurs.
SceneManager.LoadScene("Level" + levelnum);
}
public void Load()
{
//This function will be called using a button
LoadNextLvl(gameObject.scene.name);
}
}
As I explained in the comments, I tried different things until i narrowed down the issue to the fact that it stops at yield return even though I did exactly like all the tutorials I found.
When you do
SceneManager.LoadScene("Loading");
the current scene is unloaded and this component most probably along with it so your Coroutine is only executed to the first yield and then never continues.
Also you really don't need that delegate. Why not simply call your method directly like this:
public class LoadNextLevel : MonoBehaviour
{
private IEnumerator LoadFunc()
{
var levelnum = int.Parse(gameObject.scene.name[5].ToString());
levelnum++;
// Make sure this is not destroyed when the scene is unloaded
DontDestroyOnLoad (gameObject);
SceneManager.LoadScene("Loading");
yield return new WaitForSeconds(2f);
SceneManager.LoadScene("Level" + levelnum);
// Then once you don't need this anymore now destroy it
Destroy (gameObject);
}
public void Load()
{
StartCoroutine(LoadFunc());
}
}
Instead of going by strings you could also use
var nextIndex = SceneManager.GetActiveScene().buildIndex + 1;
and then later
SceneManager.LoadScene(nextIndex);
VIDEO < when ever I click my replay button and when the transition is about to start it loads the game before the transition even ends you can see the game being loaded and causes this snappy effect that I'm not sure why is there anyway i could fix this?
my code even though I have a yield that waits for the aimation to finish it doesn't wait at all it only delays when the animation will show and plays the game.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
using UnityEngine.SceneManagement;
public class replayscript : MonoBehaviour
{
GameObject replaybutton;
public Animator transitionERR;
public float transitiontime = 19f;
GameObject replay;
public float timeRR = 0f;
// Start is called before the first frame update
// Update is called once per frame
void Start()
{
replay.SetActive(false);
}
void Update()
{
if (CrossPlatformInputManager.GetButton("replaybutton")){
Debug.Log("candice");
StartCoroutine(LoadLevel(1));
replay.SetActive(true);
}
}
IEnumerator LoadLevel(int levelIndex)
{
//play animation
transitionERR.SetTrigger("start");
//wait for the animaton
yield return new WaitForSeconds(1.5f);
//load the scene
SceneManager.LoadScene(1);
}
}
You could try adding the transitionERR.SetTrigger("start"); before using the StartCoroutine(LoadLevel(1));
You could make an event for the animation end and then put your load scene code in there.
Something like:
//
public UnityAnimationEvent OnAnimationComplete;
public AnimationClip clip;
//
AnimationEvent animationEndEvent = new AnimationEvent();
animationEndEvent.time = clip.length;
animationEndEvent.functionName = "AnimationCompleteHandler";
animationEndEvent.stringParameter = clip.name;
clip.AddEvent(animationEndEvent);
//
public void AnimationCompleteHandler(string name)
{
OnAnimationComplete?.Invoke(ChangeSceneMethodName);
}
Also if you plan on using a coroutine to get a animation duration you could use WaitForSeconds(anim.GetCurrentAnimatorStateInfo(0).length+anim.GetCurrentAnimatorStateInfo(0).normalizedTime);
Im trying to make a basic platformer game and my level controller script isnt working, it needs to load loading scene first and then nextlevel but it constantly loads the nextlevel
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelController : MonoBehaviour
{
[SerializeField] string nextLevel;
private Gem[] gems;
void OnEnable()
{
gems = FindObjectsOfType<Gem>();
}
IEnumerator Wait()
{
yield return new WaitForSeconds(3);
}
void Update()
{
if (ReadyToNextLevel())
{
SceneManager.LoadScene("Loading");
StartCoroutine(Wait());
SceneManager.LoadScene(nextLevel);
}
}
bool ReadyToNextLevel()
{
foreach (var gem in gems)
{
if (gem.gameObject.activeSelf)
{
return false;
}
}
return true;
}
}
There are two big issues in your code.
Starting a Coroutine does not delay the method that started the routine!
If you want something happening after the Coroutine is finished you need to either move it into the routine itself or use a callback.
However, the thing is: You are loading a new scene, namely the "Loading" scene -> The current scene is unloaded -> the object this script is on gets destroyed -> the routine would no further executed.
Except your object is DontDestroyOnLoad which seems not the case from your code.
So in order to solve both you will need to make sure this object is not destroyed when another scene is loaded, at least not until it finished the loading process.
You could do this like e.g.
public class LevelController : MonoBehaviour
{
[SerializeField] string nextLevel;
private Gem[] gems;
void OnEnable()
{
gems = FindObjectsOfType<Gem>();
// Makes sure this is not destroyed when another scene is load
DontDestroyOnLoad(gameObject);
}
bool alreadyLoading;
IEnumerator Wait(Action whenDone)
{
yield return new WaitForSeconds(3);
// invoke the callback action
whenDone?.Invoke();
}
void Update()
{
if (ReadyToNextLevel() && ! alreadyLoading)
{
alreadyLoading = true;
SceneManager.LoadScene("Loading");
StartCoroutine(Wait(() =>
{
// This will be done after the routine finished
SceneManager.LoadScene(nextLevel);
// Now we don't need this object anymore
Destroy(gameObject);
}));
}
}
bool ReadyToNextLevel()
{
foreach (var gem in gems)
{
if (gem.gameObject.activeSelf)
{
return false;
}
}
return true;
}
}
EDIT: see derHugo's answer. They are more familiar with Unity than I am and point out that your script will be unloaded on scene change.
Your Wait() coroutine yields immediately, so StartCoroutine(Wait()) will return immediately and load the next scene.
If you want to wait three seconds before loading, then put the load inside the coroutine.
Like this:
private bool isLoading;
void Update()
{
if (!isLoading && ReadyToNextLevel())
StartCoroutine(LoadNextLevel());
}
IEnumerator LoadNextLevel()
{
isLoading = true;
SceneManager.LoadScene("Loading");
yield return new WaitForSeconds(3);
SceneManager.LoadScene(nextLevel);
isLoading = false;
}
See Coroutine help for more details
there are two reasons why this doesn't work, firstly when you load into the loading scene the gameObject is destroyed because you haven't told it to DontDestroyOnLoad, so the script will be sort of 'deleted' from the current scene. Second of all, when you call the coroutine, it calls the coroutine and then immediately continuous on to the next line of code, it doesn't wait for the coroutine to finish before moving on. I think derHugo covered how to fix these.
If I use one-layer yield return, StopCoroutine() can successfully stop my coroutine. See the code example below...
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestStopcoroutine : MonoBehaviour {
IEnumerator co = null;
// Use this for initialization
void Start () {
co = FunA();
StartCoroutine(co);
}
private IEnumerator FunA() {
Debug.Log("Enter FunA...");
yield return RepeatPrint();
Debug.Log("FunA end...");
}
private IEnumerator RepeatPrint() {
for (int i = 0; i < 5; i++) {
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick() {
if (co != null) {
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
}
this output is...
// Enter FunA...
// 0
// 1
// 2
// 3
// Stop Coroutine...
However, if I add one layer (i.e.FunB()), FunA() will be stopped but the inside coroutine(FunB()) will not be stopped. See the example code below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestStopcoroutine : MonoBehaviour {
IEnumerator co = null;
void Start () {
co = FunA();
StartCoroutine(co);
}
private IEnumerator FunA() {
Debug.Log("Enter FunA...");
yield return FunB();
Debug.Log("FunA end...");
}
private IEnumerator FunB () {
Debug.Log("Enter FunB...");
yield return RepeatPrint();
Debug.Log("FunB end...");
}
private IEnumerator RepeatPrint() {
for (int i = 0; i < 5; i++) {
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick() {
if (co != null) {
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
}
this output is...
// Enter FunA...
// Enter FunB...
// 0
// 1
// 2
// Stop Coroutine...
// 3
// 4
// FunB end...
Therefore, I am wondering why StopCoroutine() cannot successfully stop multi-layer yield return coroutine??
For your second code, the log should indeed end at Stop Coroutine.....
There are three possibilities to why it is showing the current output: (Very likely in order why this is happening)
1. You are setting Time.timeScale to 0. Search in your whole project and make sure that you're not doing this: Time.timeScale = 0;. This can pause or upause a coroutine function that's waiting with WaitForSeconds. If you did, temporary remove or comment it out and see if it's the issue.
2. Your project is corrupted and there is now a bug in it. Sometimes, a bug can randomly happen in a Unity project and the only way to fix that is to create a new project and manually move the resources from the old project to the new one.
Create a new project and test your modified code below.
3. A bug with Unity itself. Since you're using Unity 5.6.4p3, there is chance this is bug with Unity. If doing what's in #1 and #2 did not solve your issue then simply update Unity to the latest version (Unity 2018.xxx). This is more likely to fix your issue and don't forget to test with a new project instead of importing the old one.
Use the modified code from your question below to test #2 and #3. It uses the Invoke function to stop the coroutine after one second.
IEnumerator co = null;
void Start()
{
co = FunA();
Invoke("OnCancelButtonClick", 1f);
StartCoroutine(co);
}
private IEnumerator FunA()
{
Debug.Log("Enter FunA...");
yield return FunB();
Debug.Log("FunA end...");
}
private IEnumerator FunB()
{
Debug.Log("Enter FunB...");
yield return RepeatPrint();
Debug.Log("FunB end...");
}
private IEnumerator RepeatPrint()
{
for (int i = 0; i < 5; i++)
{
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick()
{
if (co != null)
{
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
The expected output:
Enter FunA...
Enter FunB...
0
Stop Coroutine...
To address KYL3R's statement in his answer that stopping the main Coroutine won't affect any other coroutine, that has already been started. This is partially true but totally depends on how and where the coroutine was started.
There are two ways to start a coroutine function:
1. Start a coroutine with the StartCoroutine function from any function like a function with a void return type.
void Start()
{
StartCoroutine(RepeatPrint());
}
or
IEnumerator Start()
{
yield return StartCoroutine(RepeatPrint());
}
2. Start a coroutine function without the StartCoroutine function by yielding the coroutine function you want to start. This must be done inside a coroutine function or a function with IEnumerator return type. It can't be done in a normal function like a void function.
IEnumerator Start()
{
yield return RepeatPrint();
}
When you start a coroutine with StartCoroutine then start children coroutines after with StartCoroutine but then killed the parent coroutine, the children coroutine functions will and should continue to run untouched.
Now, when you start a coroutine with StartCoroutine then start children coroutines after with yield return YourCoroutine() without using the StartCoroutine function but then killed the parent coroutine, the children coroutine functions will and should terminate or stop immediately so as the parent one.
It's not surprising that most Unity users don't know because it is not documented but it's something that's very important to know when using coroutine in Unity.
Coroutines run independent from each other. So stopping the main Coroutine won't affect any other, that has already been started.
It seems you have 2 options:
A: Stop the nested Coroutine from inside the first level
Store a Reference to RepeatPrint and use that to stop it using StopCoroutine.
edit: actually "from inside the first level" is not necessary, just use the correct Reference.
B: Stop ALL Coroutines in your MonoBehaviour
May work if you have no other Coroutines running