SetActive() can only be called from the main thread
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;
}
}
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 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);
}
}
}
- 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
- 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);
}
}
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);
})
}
}
}