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.
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.
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.
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).
What is impacted ?: Mainly calls that modify the UI like...
gameObject.SetActive(true); // (or false) textObject.Text = "some string" // (from UnityEngine.UI)
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);
}
}
}
DatabaseReference.GetValueAsync()
...you can...
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);
}
}
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);
})
}
}
}
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);
}
}
}```
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With