C# Coroutine doesnt works in Unity - c#

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.

Related

I'm using a coroutine and the code is stopping at yield return without resuming after the given time

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);

Coroutine wont load new scene after wait for 3 seconds

I got this script. The idea is to trigger LoadNextScene() after finishing the game. This will load a scene called "Well done". This scene shall be open for about three seconds and after that load the scene called "Start menu".
This works half the way. When the game is finished "Well done" scene is loaded but the "Start menu" isnt loaded and the debug message in MyCoroutine() isnt printed.
What can be wrong?
Im also thinking about if I shall stop my coroutine as good practise the way I do in the code?
Here is my code:
public void LoadNextScene()
{
Debug.Log("NEXT SCENE WAIT 3 sec");
SceneManager.LoadScene("Well done");
Debug.Log("Start waiting");
StartCoroutine(MyCoroutine());
StopCoroutine(MyCoroutine());
}
private IEnumerator MyCoroutine()
{
yield return new WaitForSeconds(3);
Debug.Log("Finish waiting and load start menu");
SceneManager.LoadScene("Start Menu");
}
Well after your very first SceneManager.Load the current Scene this script instance belongs to is unloaded → this GameObject destroyed → Nobody is running your Coroutine anymore.
For some reason you additional used StopCoroutine right after starting it so it wouldn't run anyway → No this makes no sense ;)
Alternatively you could simply have a separate dedicated script in your Well Done scene which automatically goes back to the main menu after 3 seconds.
Actually to make it more flexible and reusable you could have a component like
public class SwitchSceneDelayed : MonoBehaviour
{
// Configure these in the Inspector
[SerializeField] bool autoSwitch = true;
[SerializeField] float _delay = 3f;
[SerializeField] string targetScene = "Start menu";
private void Start()
{
if(autoSwitch) StartCoroutine(SwitchDelayedRoutine(_delay));
}
public void SwitchDelayed()
{
StartCoroutine(SwitchDelayedRoutine(targetScene, _delay));
}
public void SwitchDelayed(float delay)
{
StartCoroutine(SwitchDelayedRoutine(targetScene, delay));
}
public void SwitchDelayed(string scene)
{
StartCoroutine(SwitchDelayedRoutine(scene, _delay));
}
public void SwitchDelayed(string scene, float delay)
{
StartCoroutine(SwitchDelayedRoutine(scene, delay));
}
private IEnumerator SwitchDelayedRoutine(string scene, float delay)
{
Debug.Log($"Started waiting for {delay} seconds ...");
yield return new WaitForSeconds (delay);
SceneManager.LoadScene(scene);
}
}
So this script allows you to switch delayed to another scene configured in targetScene after delay seconds. If you enable autoSwitch the routine will start right away, otherwise you can trigger it at any time via calling SwitchDelayed.
Put this in your Well Done scene and then simply only do
public void LoadNextScene()
{
SceneManager.LoadScene("Well done");
}
in your original script.

How to play the audio one by one in unity

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.

How can I call IEnumerator when a boolean is true?

I'm using the IEnumerator function and have had some issues with my if statement working:
IEnumerator Spawning()
{
Debug.Log("Spawning being called...");
if (GameObject.Find("FPSController").GetComponent<BoxCollide>().hitTrigger == true)
{
Debug.Log("CUSTOMER SHOULD SPAWN!");
while (countStop == false) {
yield return new WaitForSeconds(2);
Debug.Log("Fisher Spawned!");
counter++;
spawnNewCharacter.SpawnCharacter();
if (counter >= 3){
countStop = true;
}
}
}
}
After some debugging, it turns out that my if statement actually works. This issue is actually the IEnumerator function being called as soon as I run my game. I need a way to call this IEnumerator function only when my hitTrigger == true, but I can't seem to get anything to work.
I've tried this on top of the IEnumerator function:
void Update()
{
if (GameObject.Find("FPSController").GetComponent<BoxCollide>().hitTrigger == true)
{
Debug.Log("Spawning now...");
StartCoroutine(Spawning());
}
}
But still can't even get any of the Debug.Log's to come through. Would appreciate some help on this!
Side Information
Find() and GetComponent()
You don't want to use GameObject.Find(...) in the Update method, as it's an expensive call. The Update method is called each frame, so you'd call GameObject.Find(...) 60 times in 1 second at 60fps.
So when you use GetComponent() or Find() you want to save a reference to these objects like shown in the snippets below.
Better locations to use methods like GetComponent() or GameObject.Find() are the Awake() and Start() methods.
Awake
Awake is used to initialize any variables or game state before the
game starts. Awake is called only once during the lifetime of the
script instance. Awake is called after all objects are initialized so
you can safely speak to other objects or query them using eg.
GameObject.FindWithTag. [...]
Explanation is taken from the linked documentation.
Start
Start is called on the frame when a script is enabled just before any
of the Update methods is called the first time. Like the Awake
function, Start is called exactly once in the lifetime of the script.
However, Awake is called when the script object is initialised,
regardless of whether or not the script is enabled. Start may not be
called on the same frame as Awake if the script is not enabled at
initialisation time.
Explanation is also taken from the linked documentation
Possible Solution
Add the first Component (FPSControllerCollission) onto the object that holds your FPSController.
It makes use of unities OnTriggerEnter & OnTriggerExit methods.
This script is gonna set the IsTriggered bool to true, when a trigger entered the space of the box collider.
Note: A collider acts as a trigger, when the "Is Trigger" checkbox on
the component is checked.
You can do similar with OnCollisionEnter/Exit to recognize, when a Collider enters the space of the box collider.
Note that the following is only an example and you'll have to tweak /
integrate it into your code
[RequireComponent(typeof(BoxCollider))]
public class FPSControllerCollission : MonoBehaviour {
public bool IsTriggered;
private void OnTriggerEnter(Collider other) {
this.IsTriggered = true;
}
private void OnTriggerExit(Collider other) {
//Maybe check the tag/type of the other object here
this.IsTriggered = false;
}
}
The following SpawnController class could be integrated in the class you allready have.
public class SpawnController : MonoBehaviour {
private FPSControllerCollission _fpsControllerCollission;
private void Awake() {
this._fpsControllerCollission = FindObjectOfType<FPSControllerCollission>();
}
private void Update() {
if (this._fpsControllerCollission.IsTriggered) {
StartCoroutine(nameof(Spawning));
}
}
IEnumerator Spawning() {
Debug.Log("Spawning being called...");
if (this._fpsControllerCollission == true) {
Debug.Log("CUSTOMER SHOULD SPAWN!");
bool countStop = false;
int counter;
while (countStop == false) {
yield return new WaitForSeconds(2);
Debug.Log("Fisher Spawned!");
counter++;
spawnNewCharacter.SpawnCharacter();
if (counter >= 3) {
countStop = true;
}
}
}
}
}

WaitForSeconds() Coroutine causes Game to freeze Indefinitely (Edited)

I have a game with four scenes, a menu scene, a loading scene, and two game scenes. All is well, when I am transitioning from my menu scene to my game scenes, but whenever I transition from my game scenes back to my menu scene or reload the game scene, the loading scene simply stops responding. I get a warning message that says "NetworkManager detected a script reload in the editor. This has caused the network to be shut down" only when I try to reload the currently active game scene. This issue is also present when I play in the build! I used print statements to trace down where my code stopped running, and I figured out that it was the Yield Return New WaitForSeconds() which caused the game to freeze. Why is that?
I have two scripts that controls transitioning. One simple script called on UIButtons for telling the second more complex script called in the preload scene to load the scene it's supposed to load and create animations. I have made sure that I was loading on to the correct scene, and that all of the scenes were added into my build settings.
The following pictures show the loading scene not responding. The first picture shows what happens when I try to reload the current game scene, and the second picture shows what happens when I try to load the menu scene:
My Loading Scene Script:
public class LoadingScreenManager : MonoBehaviour {
[Header("Loading Visuals")]
public Image loadingIcon;
public Image loadingDoneIcon;
public Text loadingText;
public Image progressBar;
public Image fadeOverlay;
[Header("Timing Settings")]
public float waitOnLoadEnd = 0.25f;
public float fadeDuration = 0.25f;
[Header("Loading Settings")]
public LoadSceneMode loadSceneMode = LoadSceneMode.Single;
public ThreadPriority loadThreadPriority;
[Header("Other")]
// If loading additive, link to the cameras audio listener, to avoid multiple active audio listeners
public AudioListener audioListener;
AsyncOperation operation;
Scene currentScene;
public static int sceneToLoad = -1;
// IMPORTANT! This is the build index of your loading scene. You need to change this to match your actual scene index
static int loadingSceneIndex = 1;
public static void LoadScene(int levelNum) {
Application.backgroundLoadingPriority = ThreadPriority.High;
sceneToLoad = levelNum;
SceneManager.LoadScene(loadingSceneIndex);
}
void Start() {
if (sceneToLoad < 0)
return;
fadeOverlay.gameObject.SetActive(true); // Making sure it's on so that we can crossfade Alpha
currentScene = SceneManager.GetActiveScene();
StartCoroutine(LoadAsync(sceneToLoad));
}
private IEnumerator LoadAsync(int levelNum) {
ShowLoadingVisuals();
yield return null;
FadeIn();
StartOperation(levelNum);
float lastProgress = 0f;
// operation does not auto-activate scene, so it's stuck at 0.9
while (DoneLoading() == false) {
yield return null;
if (Mathf.Approximately(operation.progress, lastProgress) == false) {
progressBar.fillAmount = operation.progress;
lastProgress = operation.progress;
}
}
if (loadSceneMode == LoadSceneMode.Additive)
audioListener.enabled = false;
ShowCompletionVisuals();
//THE PRINT STATEMENT WORKS FINE RIGHT HERE! The value of waitOnLoadEnd is only 1
yield return new WaitForSeconds(waitOnLoadEnd);
//THE PRINT STATEMENT STOPS RUNNING RIGHT HERE!
FadeOut();
yield return new WaitForSeconds(fadeDuration);
if (loadSceneMode == LoadSceneMode.Additive)
SceneManager.UnloadScene(currentScene.name);
else
operation.allowSceneActivation = true;
}
private void StartOperation(int levelNum) {
Application.backgroundLoadingPriority = loadThreadPriority;
operation = SceneManager.LoadSceneAsync(levelNum, loadSceneMode);
if (loadSceneMode == LoadSceneMode.Single)
operation.allowSceneActivation = false;
}
private bool DoneLoading() {
return (loadSceneMode == LoadSceneMode.Additive && operation.isDone) || (loadSceneMode == LoadSceneMode.Single && operation.progress >= 0.9f);
}
void FadeIn() {
fadeOverlay.CrossFadeAlpha(0, fadeDuration, true);
}
void FadeOut() {
fadeOverlay.CrossFadeAlpha(1, fadeDuration, true);
}
void ShowLoadingVisuals() {
loadingIcon.gameObject.SetActive(true);
loadingDoneIcon.gameObject.SetActive(false);
progressBar.fillAmount = 0f;
loadingText.text = "LOADING...";
}
void ShowCompletionVisuals() {
loadingIcon.gameObject.SetActive(false);
loadingDoneIcon.gameObject.SetActive(true);
progressBar.fillAmount = 1f;
loadingText.text = "LOADING DONE";
}
}
Script on UIButtons that call the above script:
public class LoadingSceneButton : MonoBehaviour {
public void LoadSceneWithLoadingScreen(int sceneNumber){
if (sceneNumber < 0 || sceneNumber >= SceneManager.sceneCountInBuildSettings) {
Debug.LogWarning ("Can't Load Scene, because It Doesn't Exist!");
}
LoadingScreenManager.LoadScene (sceneNumber);
}
}
(1) Don't use "print", please use this:
Debug.Log("fadeDuration is ....... " , fadeDuration.ToString("f4");
add that line of code just before you call FadeOut and also please add it inside FadeOut
(2) problems with CrossFadeAlpha
Please note that CrossFadeAlpha is extremely difficult to use! It's a real pain! It only works on UnityEngine.UI.Graphic, and it's tricky when used with coroutines.
public static void FadeOut(this Graphic g)
{
g.GetComponent<CanvasRenderer>().SetAlpha(1f);
g.CrossFadeAlpha(0f,.15f,false);
}
(3) problems with loading a scene in Unity5 !!!
Yes there is a
known issue
where it gets stuck on 0.9. Maybe this is the main problem at hand.
check out ... http://answers.unity3d.com/answers/1146173/view.html
and ... http://answers.unity3d.com/answers/1073667/view.html
Some basic working code example....
public void LaunchSoundboard()
{
StartCoroutine(_soundboard());
}
private IEnumerator _soundboard()
{
Grid.music.Duck();
yield return new WaitForSeconds(.2f);
AsyncOperation ao;
ao = UnityEngine.SceneManagement
.SceneManager.LoadSceneAsync("YourSceneName");
while (!ao.isDone)
{
yield return null;
}
// here, the new scene IS LOADED
SoundBoard soundBoard = Object.FindObjectOfType<SoundBoard>();
if(soundBoard==null) Debug.Log("WOE!");
soundBoard.SomeFunctionInSoundboardScript();
}
Note that you wait on ".isDone", rather than watch the float.
(4) #You actually have to have a scene called "preload", which ONLY preloads.
unfortunately the menu scene can not be your preload scene.
You have to actually have a separate preload scene which does nothing but that, "preload".
Note that any "game managers" you have must be on the preload scene. It's a bit annoying but that's how it is.
The purpose of the "preload" scene is to hold any game managers you have.
The only scene that you mark "don't destroy on load" must be only the "preload" scene. It's that simple.
It's a bit of a nuisance but very simple and reliable.

Categories