Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SetActive() can only be called from the main thread

I am stuck with this problem for 3 days, I did a lot of research, but couldn't find any answer, Here is a brief explanation of what is happening, trying to work with Firebase Database and Authentication with Unity3D, Here are the steps:

First user signs in and if it's successful, it will fetch user's data from Database and then Authentication panel should be deactivated and User's panel activated.

It Gives me this error when trying to SetActive panel.

SetActive can only be called from the main thread. Constructors and field initializers will be executed from the loading thread when loading a scene. Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function. UnityEngine.GameObject:SetActive(Boolean)

public void SignInWithEmail()
{
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
      DatabaseReference.GetValueAsync().ContinueWith(task => {

        //here after successful signing in, it gets the data from the Database
        //and after that it should activate the user panel
        //and deactivate the authentication panel

        //HERE IS THE PROBLEM 
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    }
  }
}

I'm not trying to load another scene or anything else.

If needed I can provide more information

like image 993
Orif Avatar asked Dec 24 '18 18:12

Orif


3 Answers

So my answer is very similar to the accepted answer from Milod's, but a little different, as it took me a while to wrap my head around his, even though his still works.

  1. The Issue: Normally, all your code runs on a single thread in Unity, since Unity is single-threaded, however when working with APIs like Firebase, which require callbacks, the callback functions will be handled by a new thread. This can lead to race-conditions, especially on a single-threaded engine like Unity.

  2. The solution (from Unity): Starting from Unity 2017.X, unity now requires changes to UI components to be run on the Main thread (i.e. the first thread that was started with Unity).

  3. What is impacted ?: Mainly calls that modify the UI like...

    gameObject.SetActive(true);  // (or false)
    textObject.Text = "some string" // (from UnityEngine.UI)
    
  4. How this relates to your code:

public void SignInWithEmail() {
    // auth.SignInWithEmailAndPasswordAsyn() is run on the local thread, 
    // ...so no issues here
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {

     // .ContinueWith() is an asynchronous call 
     // ...to the lambda function defined within the  task=> { }
     // and most importantly, it will be run on a different thread, hence the issue
      DatabaseReference.GetValueAsync().ContinueWith(task => {

        //HERE IS THE PROBLEM 
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    }
  }
}

  1. Suggested Solution: For those calls which require callback functions, like...
DatabaseReference.GetValueAsync()

...you can...

  • send them to a function which is set up to run on that initial thread.
  • ...and which uses a queue to ensure that they will be run in the order that they were added.
  • ...and using the singleton pattern, in the way advised by the Unity team.

Actual solution

  1. Place the code below into your scene on a gameObject that will always be enabled, so that you have a worker that...
    • always runs on the local thread
    • can be sent those callback functions to be run on the local thread.
using System;
using System.Collections.Generic;
using UnityEngine;

internal class UnityMainThread : MonoBehaviour
{
    internal static UnityMainThread wkr;
    Queue<Action> jobs = new Queue<Action>();

    void Awake() {
        wkr = this;
    }

    void Update() {
        while (jobs.Count > 0) 
            jobs.Dequeue().Invoke();
    }

    internal void AddJob(Action newJob) {
        jobs.Enqueue(newJob);
    }
}

  1. Now from your code, you can simply call...

     UnityMainThread.wkr.AddJob();
    

...so that your code remains easy to read (and manage), as shown below...

public void SignInWithEmail() {
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {

      DatabaseReference.GetValueAsync().ContinueWith(task => {
        UnityMainThread.wkr.AddJob(() => {
            // Will run on main thread, hence issue is solved
            userPanel.SetActive(true);
            authPanel.SetActive(false);            
        })

    }
  }
}
like image 181
olonge Avatar answered Nov 11 '22 17:11

olonge


Note that Firebase has now a nice ContinueWithOnMainThread extension method that solves this problem more elegantly than the other suggested answers:

using Firebase.Extensions;
public void SignInWithEmail() {
    // This code runs on the caller's thread.
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
      // This code runs on an arbitrary thread.
      DatabaseReference.GetValueAsync().ContinueWithOnMainThread(task => {
        // This code runs on the Main thread. No problem.
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    }
  }
}```
like image 26
hk1ll3r Avatar answered Nov 11 '22 17:11

hk1ll3r


So basically UI elements need to be modified in Main thread, and I found this script and it will execute your function in Main thread, just put your function in a Coroutine and Enqueue it to the script(UnityMainThreadDispatcher). (You need an object in the scene and add the MainThreadDispathcer script to it)

Here's how my Function looked:

public void SignInWithEmail()
{
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
     DatabaseReference.GetValueAsync().ContinueWith(task => {
         //Here's the fix
         UnityMainThreadDispatcher.Instance().Enqueue(ShowUserPanel());
    }
  }
}


public IEnumerator ShowUserPanel()
{
    uiController.userPanel.panel.SetActive(true);
    uiController.authPanel.SetActive(false);
    yield return null;
}

This is the script to run it in Main Thead

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;


public class UnityMainThreadDispatcher : MonoBehaviour {

private static readonly Queue<Action> _executionQueue = new Queue<Action>();


/// <summary>
/// Locks the queue and adds the IEnumerator to the queue
/// </summary>
/// <param name="action">IEnumerator function that will be executed from the main thread.</param>
public void Enqueue(IEnumerator action) {
    lock (_executionQueue) {
        _executionQueue.Enqueue (() => {
            StartCoroutine (action);
        });
    }
}

/// <summary>
/// Locks the queue and adds the Action to the queue
/// </summary>
/// <param name="action">function that will be executed from the main thread.</param>
public void Enqueue(Action action)
{
    Enqueue(ActionWrapper(action));
}
IEnumerator ActionWrapper(Action a)
{
    a();
    yield return null;
}


private static UnityMainThreadDispatcher _instance = null;

public static bool Exists() {
    return _instance != null;
}

public static UnityMainThreadDispatcher Instance() {
    if (!Exists ()) {
        throw new Exception ("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene.");
    }
    return _instance;
}

void Awake() {
    if (_instance == null) {
        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    }
}

public void Update() {
    lock(_executionQueue) {
        while (_executionQueue.Count > 0) {
            _executionQueue.Dequeue().Invoke();
        }
    }
}

void OnDestroy() {
        _instance = null;
}

}
like image 33
Orif Avatar answered Nov 11 '22 19:11

Orif