Search Unity

Script not waiting for coroutine to finish

Discussion in 'Scripting' started by OldRod, Jun 16, 2018.

  1. OldRod

    OldRod

    Joined:
    Mar 2, 2009
    Posts:
    136
    I am trying to make a login script, and I am new to using coroutines, so I'm sure there's something obvious that I just can't see, but when I run this code, the calling class completes execution before the WWW request finishes, so it shows that login failed, even though the WWW request returned a successful login.

    In my main class, I have this code:

    Code (CSharp):
    1. public Database db;
    2. ....
    3. ...
    4. bool tryLogin = db.LoginPlayer(usernameField.text, hashedPassword);
    5.  
    6. if(tryLogin)
    7. {
    8.     Debug.Log("Login Success!");
    9. }
    10. else
    11. {
    12.     Debug.Log("Login Failure!");
    13. }
    In Database.cs, I have this code:
    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. public class Database : MonoBehaviour
    5. {
    6.     public string loginURL = "http://localhost:8080/unitylogin.php?";
    7.     public string postURL = "";
    8.     private bool LoginResult;
    9.     public bool LoginPlayer(string userName, string hashedPassword)
    10.     {
    11.         postURL = loginURL + "acctname=" + WWW.EscapeURL(userName) + "&password=" + hashedPassword;
    12.         StartCoroutine(TryLogin());
    13.         return LoginResult;
    14.     }
    15.  
    16.     IEnumerator TryLogin()
    17.     {
    18.         WWW result = new WWW(postURL);
    19.         yield return result;
    20.         Debug.Log("Result.text = " + result.text);
    21.         if (result.text == "Success")
    22.             LoginResult = true;
    23.         else
    24.             LoginResult = false;
    25.         Debug.Log("LoginResult = " + LoginResult);
    26.     }
    27. }
    28.  
    The PHP script returns "Success" if the login is successful

    Here is the output from the console when I run this code:

    Login Failure!
    Result.text = Success
    LoginResult = True

    As you can see, the Debug.Log statement from my original class ("Login Failure!") is executing before the WWW request has actually completed. But in TryLogin(), doesn't the "yield return result" tell Unity to wait?

    Or am I using it wrong?

    Thanks to anyone who can help :)
     
  2. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    StartCorutine is not a blocking operation, it will return directly, your login method will return directly with false
     
  3. OldRod

    OldRod

    Joined:
    Mar 2, 2009
    Posts:
    136
    So how do I make it wait until the coroutine finishes? I thought that's what the "yield return" did?
     
  4. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    You should make LoginPlayer return the IEnumerator instead and move the StartCoroutine to your main class
     
  5. OldRod

    OldRod

    Joined:
    Mar 2, 2009
    Posts:
    136
    Well, before I saw your last reply, I tried it with taking the coroutine completely out and just having a while loop to wait until .isDone was true on the WWW request and it works now.

    Is there an issue with doing it that way? I suppose it would be wise to put a timeout check in there to avoid it waiting forever, but is there any other issue that I am not seeing?
     
  6. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    You did what? A blocking while loop? That would not block the render thread and the game will freeze
     
  7. OldRod

    OldRod

    Joined:
    Mar 2, 2009
    Posts:
    136
    But if they can't log in, they can't play the game anyway... I am putting a timeout check of a few seconds on the while loop in case of web failure, otherwise they get an immediate success or failure message from the login.
     
  8. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That particular line does indeed tell Unity to "wait", but only in the context of that coroutine (it'll poll internally every frame and advance to the next "step" when the yield instruction allows it).

    As mentioned by @AndersMalmgren, your LoginPlayer method will return directly after the call to StartCoroutine (which will register the coroutine and runs it until it hits the first yield instruction).

    Though, you can solve this in various ways:
    1) You could pass a callback function to the coroutine that executes further logic once the response has been received.
    2) You could add events - such as LoginFailed, LoginSucceeded - which can be raised at the end of the coroutine. Other scripts can then subscribe for these events and run their logic whenever the event is raised.
    3) You could poll for the result. But that wouldn't work well with a plain boolean. An enumeration or a state object is the most suitable.
     
  9. OldRod

    OldRod

    Joined:
    Mar 2, 2009
    Posts:
    136
    Thanks Suddoha, I'll try some of those suggestions. The while loop is working for now, but I know it's klutzy, so I need to understand coroutines better before I can fix it.
     
  10. OldRod

    OldRod

    Joined:
    Mar 2, 2009
    Posts:
    136
    I finally got it sorted out. I was trying to split it up into different classes and it was making it more complex than it needed to be. I simplified it and now it's working fine with the coroutine and without the while loop :)

    Thanks again for the help guys!
     
  11. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    Splitting it up in different classes is not wrong and usually is something you want, separation of concerns, does not stop you from using coroutines too.

    Example from my game

    Code (CSharp):
    1.     public abstract class TutorialStep : ScriptableObject
    2.     {
    3.         public abstract IEnumerator Execute();
    4.     }
    Implemented like
    Code (CSharp):
    1.     [CreateAssetMenu(menuName = "Tutorial/ChangeFireModeStep")]
    2.     public class ChangeFireModeStep : TutorialStep
    3.     {
    4.  
    5.         public FireModes Firemode;
    6.  
    7.         public override IEnumerator Execute()
    8.         {
    9.             var firearm = Get<FullAutomaticRifle>();
    10.  
    11.             ShowPopup(firearm.FireSelector.transform, string.Format("Press {0} to change fire mode to {1} auto.", GetCommandCaption(Command.ChangeFireMode), Firemode.ToString().ToLower()));
    12.  
    13.             while (firearm.FireSelector.Mode != Firemode)
    14.                 yield return null;
    15.  
    16.         }
    17.     }
    Then its executed like
    Code (CSharp):
    1.     public class Tutorial : MonoBehaviour, ITutorial
    2.     {
    3.         public TutorialStep[] Steps;
    4.  
    5.         public IEnumerator Execute()
    6.         {
    7.             foreach (var prefab in Steps)
    8.             {
    9.                 var step = Object.Instantiate(prefab);
    10.                 yield return step.Execute();
    11.             }
    12.         }
    13.     }
    As you can see its not even here the coroutine is being fired, its actually a second layer up
    Code (CSharp):
    1. public class TutorialGameManager : GameManager
    2.     {
    3.         public Tutorial[] Tutorials;
    4.    
    5.         protected IEnumerator Start()
    6.         {
    7.             foreach (var tutorial in Tutorials)            
    8.                 yield return tutorial.Execute();            
    9.         }
    10.     }