【问题标题】:SetActive() can only be called from the main threadSetActive() 只能从主线程调用
【发布时间】:2018-12-24 18:06:39
【问题描述】:

我被这个问题困扰了 3 天,我做了很多研究,但找不到任何答案,这里是正在发生的事情的简要说明,尝试使用 Firebase 数据库和使用 Unity3D 进行身份验证,这里步骤是:

第一个用户登录,如果成功,它将从数据库中获取用户的数据,然后应该停用身份验证面板并激活用户面板。

尝试 SetActive 面板时出现此错误。

SetActive 只能从主线程调用。 加载场景时,构造函数和字段初始化程序将从加载线程中执行。 不要在构造函数或字段初始化程序中使用此函数,而是将初始化代码移至 Awake 或 Start 函数。 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);
    }
  }
}

我没有尝试加载另一个场景或其他任何内容。

如果需要,我可以提供更多信息

【问题讨论】:

  • UI 元素只能在 UI 线程中修改,你是在另一个线程中调用 SetActive。
  • @Luis 那么如何在主线程中调用它呢? (我没有使用任何线程)

标签: c# firebase unity3d


【解决方案1】:

所以我的答案与 Milod 的 接受的答案非常相似,但有点不同,因为我花了一段时间才把头绕在他的周围,尽管他的仍然有效。

  1. 问题: 通常,您的所有代码都在 Unity 中的单个线程上运行,因为 Unity 是单线程的, 然而,当使用像 Firebase 这样需要回调的 API 时,回调函数将由新线程处理。 这可能会导致竞争条件,尤其是在 Unity 等单线程引擎上。

  2. 解决方案(来自 Unity): 从 Unity 2017.X 开始,Unity 现在需要更改 UI 组件才能在主线程(即使用 Unity 启动的第一个线程)上运行。

  3. 有什么影响?: 主要是修改 UI 的调用...

    gameObject.SetActive(true);  // (or false)
    textObject.Text = "some string" // (from UnityEngine.UI)
    
  4. 这与您的代码有何关系:

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. 建议的解决方案: 对于那些需要回调函数的调用,比如...
DatabaseReference.GetValueAsync()

...你可以...

  • 将它们发送到设置为在该初始线程上运行的函数。
  • ...它使用队列来确保它们将按照添加的顺序运行。
  • ...并按照 Unity 团队建议的方式使用单例模式。

实际解决方案

  1. 将下面的代码放置在您的场景中,该游戏对象将始终启用,这样您就有了一个...
    • 始终在本地线程上运行
    • 可以发送这些回调函数在本地线程上运行。
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. 现在从您的代码中,您可以简单地调用...

     UnityMainThread.wkr.AddJob();
    

...使您的代码易于阅读(和管理),如下所示...

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);            
        })

    }
  }
}

【讨论】:

  • 您的解决方案有效。谢谢。另外,您能否确认这不会对性能造成额外开销?
  • 嗨,Soorya。不应该有任何额外的开销。这里的问题是谁(或什么)来做这项工作。内部受信任的人(或线程)(我们知道我们将能够根据需要影响他们),而不是未经审查的“其他人”。 ..但要完成的工作量保持不变。请原谅这个类比,希望它有助于澄清问题。
【解决方案2】:

所以基本上 UI 元素需要在主线程中修改,我找到了这个脚本,它会在主线程中执行你的函数,只需将你的函数放在 Coroutine 和 Enqueue 到脚本(UnityMainThreadDispatcher)。 (您需要在场景中添加一个对象并将 MainThreadDispathcer 脚本添加到其中)

这是我的函数的外观:

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;
}

这是在 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;
}

}
【解决方案3】:

请注意,Firebase 现在有一个很好的 ContinueWithOnMainThread 扩展方法,它比其他建议的答案更优雅地解决了这个问题:

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);
    }
  }
}```

【讨论】:

  • 为其他人标记 using Firebase.Extensions 在此代码 sn-p 中很重要 - 否则,它使用 system.Threading.Tasks' 未扩展版本,只有 ContinueWith
猜你喜欢
  • 2019-06-18
  • 2017-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-20
  • 2013-04-16
相关资源
最近更新 更多