Faizan Khan
Faizan Khan

Reputation: 207

SceneManager.LoadScene(index) doesn't work when called in an async method. unity3d

So, I am using Firebase Realtime Database for fetching a user values. I first check using DataSnapShot if a certain user exists or not. And then, if it exists, I call SceneManager.LoadScene (index). All the logs are correctly shown in the console. But SceneManager.LoadScene method won't work.

Here is the code block:

 void authWithFirebaseFacebook(string accessToken)
    {
        Firebase.Auth.Credential credential =
            Firebase.Auth.FacebookAuthProvider.GetCredential(accessToken);
        auth.SignInWithCredentialAsync(credential).ContinueWith(task => {
            if (task.IsCanceled) {
             
                return;
            }
            if (task.IsFaulted) {
              
                return;
            }

            loginSuccesful();
            
        });

    }

void loginSuccesful()
    {
     // SOME CODE HERE THEN (ONLY ASSIGNMENTS, NO TASK OR ANYTHING)
       SaveUser();       
    }
 
 public async void SaveUser()
    {
        DataSnapshot snapShot = await playerExistsAlready();

        if (snapShot.Exists)
        {
            Debug.Log("Continue Fetching the details for user");
            Debug.Log("Values fetched for new player...:" + snapShot.GetValue(true));
            Debug.Log("Convert JSON to Object");
            string json = snapShot.GetValue(true).ToString();
           
            PlayerData newPlayer = JsonUtility.FromJson<PlayerData>(json);
            
            UserManager.instance.player = newPlayer;               
            Debug.Log("User Creaated!");
            Debug.Log("New Values are:" + newPlayer.DisplayName + " .." + newPlayer.PhotoURL) ;
            Debug.Log("Load Main Menu Scene Now...");
            SceneManager.LoadScene(1);

        }
        else
        {               
            saveNewUser();
        }                     
    }



async Task<DataSnapshot> playerExistsAlready()
   {
       DataSnapshot dataSnapShot = await _database.GetReference("users/" + UserManager.instance.currentUserID ).GetValueAsync();                  
       return dataSnapShot;
   }

The output: Continue Fetching the details for user

Values fetched for new player...: (player data here)

Convert JSON to Object

User Created!

New Values are : (values here)

Load Main Menu Scene Now...

Upvotes: 0

Views: 556

Answers (2)

derHugo
derHugo

Reputation: 90739

The issue is that your code is running on a separate background thread. Your main thread will not "know" that anything was called by a thread. In general most of the Unity API can not be called from a thread .. and it makes no sense to do.

Firebase specifically for that provides an extension method ContinueWithOnMainThread which does exactly that: Makes sure that the callback is executed in the Unity main thread.

void authWithFirebaseFacebook(string accessToken)
{
    Firebase.Auth.Credential credential = Firebase.Auth.FacebookAuthProvider.GetCredential(accessToken);
    auth.SignInWithCredentialAsync(credential).ContinueWithOnMainThread(task => 
    {
        if (task.IsCanceled || task.IsFaulted) 
        {
            return;
        }

        loginSuccesful();
        
    });
}

void loginSuccesful()
{
 // SOME CODE HERE THEN (ONLY ASSIGNMENTS, NO TASK OR ANYTHING)
   SaveUser();       
}

public void SaveUser()
{
    _database.GetReference("users/" + UserManager.instance.currentUserID ).GetValueAsync().ContinueWithOnMainThread(task =>
    {
        if (task.IsCanceled || task.IsFaulted) 
        {
            return;
        }

        var snapShot = task.Result;

        if (snapShot.Exists)
        {
            Debug.Log("Continue Fetching the details for user");
            Debug.Log("Values fetched for new player...:" + snapShot.GetValue(true));
            Debug.Log("Convert JSON to Object");
            string json = snapShot.GetValue(true).ToString();
       
            PlayerData newPlayer = JsonUtility.FromJson<PlayerData>(json);
        
            UserManager.instance.player = newPlayer;               
            Debug.Log("User Creaated!");
            Debug.Log("New Values are:" + newPlayer.DisplayName + " .." + newPlayer.PhotoURL) ;
            Debug.Log("Load Main Menu Scene Now...");
            SceneManager.LoadScene(1);
        }
        else
        {               
            saveNewUser();
        } 
    }                    
}

Alternatively an often used pattern is a so called "main thread dispatcher" (I know there is an asset for that but this one requires a lot of refactoring actually ;) ).

But you can easily create one yourself. Usually it looks somewhat like

public class YourClass : MonoBehaviour
{
    private ConcurrentQueue<Action> mainThreadActions = new ConcurrentQueue<Action>();

    private void Update()
    {
        while(mainThreadActions.Count > 0 && mainThreadActions.TryDequeue(out var action))
        {
            action?.Invoke();
        }
    }
}

so any thread can just add an action to be executed on the main thread via e.g.

void authWithFirebaseFacebook(string accessToken)
{
    Firebase.Auth.Credential credential = Firebase.Auth.FacebookAuthProvider.GetCredential(accessToken);
    auth.SignInWithCredentialAsync(credential).ContinueWith(task => 
    {
        if (task.IsCanceled || task.IsFaulted) 
        {
            return;
        }

        // either as method call
        mainThreadActions.Enqueue(loginSuccesful);
    });
}

void loginSuccesful()
{
 // SOME CODE HERE THEN (ONLY ASSIGNMENTS, NO TASK OR ANYTHING)
   SaveUser();       
}

public void SaveUser()
{
    // or as lambda exopression
    database.GetReference("users/" + UserManager.instance.currentUserID ).GetValueAsync().ContinueWith(task => 
    {
        mainThreadActions.Enqueue(() => 
        {
            if (task.IsCanceled || task.IsFaulted) 
            {
                return;
            }

            var snapShot = task.Result;

            if (snapShot.Exists)
            {
                Debug.Log("Continue Fetching the details for user");
                Debug.Log("Values fetched for new player...:" + snapShot.GetValue(true));
                Debug.Log("Convert JSON to Object");
                string json = snapShot.GetValue(true).ToString();
       
                PlayerData newPlayer = JsonUtility.FromJson<PlayerData>(json);
        
                UserManager.instance.player = newPlayer;               
                Debug.Log("User Creaated!");
                Debug.Log("New Values are:" + newPlayer.DisplayName + " .." + newPlayer.PhotoURL) ;
                Debug.Log("Load Main Menu Scene Now...");
                SceneManager.LoadScene(1);
            }
            else
            {               
                saveNewUser();
            }    
        })
    });                 
}

Upvotes: 1

Faizan Khan
Faizan Khan

Reputation: 207

So the problem was the method was not being executed on Main Thread. I guess Unity Methods like SceneManager.Load() etc are only called on a Main Thread. So, I used a class named UnityMainThreadDispatcher

And then I wrote

IEnumerator loadMainMenuScene()
   {
       SceneManager.LoadScene(1);
       yield return null;
   }

And called

UnityMainThreadDispatcher.Instance().Enqueue(loadMainMenuScene());

instead of

SceneManager.LoadScene(1);

Upvotes: 0

Related Questions