等待线程完成而不阻塞 UI 线程

Wait for thread to finish without blocking the UI thread

我正在尝试统一创建一个消息框 class,我希望它的工作方式与 windows 表单中等待按下按钮的消息框相同之后执行代码。

        var mbox = MessageBox.Show("Test", "test", MessageBoxButtons.YesNo);
        var test = mbox == DialogResult.Cancel; <- it will wait here

我尝试用两种方式重现它

加入 2 个线程

    public void TestClick()
    {
        Thread thread1 = new Thread(TestMethod);
        thread1.Start();
        thread1.Join();
        Debug.Log("Done");
    }

    private void TestMethod()
    {
        float time = 0;
        while (time <= 20)
        {
            Thread.Sleep(100);
            time++;
            Debug.Log("Im doing heavy work");
        }
    }

这会阻塞主线程,只有在 TestMethod 完成后才会恢复,但我不希望这样,因为在那段时间用户将无法与消息框交互。

异步方式

    public delegate int AsyncTask();

    public void TestClick()
    {
        RunAsyncAndWait();
        Debug.Log("Done");
    }

    public int Method1()
    {
        float time = 0;
        while (time <= 20)
        {
            Thread.Sleep(100);
            time++;
            Debug.Log("Im doing heavy work");
        }
        return 0;
    }


    public void RunAsyncAndWait()
    {
        AsyncTask ac1 = Method1;

        WaitHandle[] waits = new WaitHandle[1];
        IAsyncResult r1 = ac1.BeginInvoke(null, null);
        waits[0] = r1.AsyncWaitHandle;

        WaitHandle.WaitAll(waits);

    }

这与第一个完全一样,但如果我们将 WaitHandle[] 的大小更改为 WaitHandle[] waits = new WaitHandle[2]; 等内容,它的行为会很奇怪。

现在这更符合我的需要,因为它不断地在控制台中写入内容,而不是像以前的方法那样一次发布 21 条消息,但是 运行 它暂停统一场景的那一刻(我可以手动恢复它,程序将 运行 正常)并且它继续在控制台中打印内容,我收到此错误

ArgumentNullException: null handle Parameter name: waitHandles System.Threading.WaitHandle.CheckArray (System.Threading.WaitHandle[] handles, Boolean waitAll) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Threading/WaitHandle.cs:77) System.Threading.WaitHandle.WaitAll (System.Threading.WaitHandle[] waitHandles) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Threading/WaitHandle.cs:109) Assets.Scripts.Test.RunAsyncAndWait () (at Assets/Scripts/Test.cs:40) Assets.Scripts.Test.TestClick () (at Assets/Scripts/Test.cs:16) UnityEngine.Events.InvokableCall.Invoke (System.Object[] args) (at C:/buildslave/unity/build/Runtime/Export/UnityEvent.cs:153) UnityEngine.Events.InvokableCallList.Invoke (System.Object[] parameters) (at C:/buildslave/unity/build/Runtime/Export/UnityEvent.cs:630) UnityEngine.Events.UnityEventBase.Invoke (System.Object[] parameters) (at C:/buildslave/unity/build/Runtime/Export/UnityEvent.cs:765) UnityEngine.Events.UnityEvent.Invoke () (at C:/buildslave/unity/build/Runtime/Export/UnityEvent_0.cs:53) UnityEngine.UI.Button.Press () (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/UI/Core/Button.cs:35) UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/UI/Core/Button.cs:44) UnityEngine.EventSystems.ExecuteEvents.Execute (IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/EventSystem/ExecuteEvents.cs:52) UnityEngine.EventSystems.ExecuteEvents.Execute[IPointerClickHandler] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.EventFunction`1 functor) (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/EventSystem/ExecuteEvents.cs:269) UnityEngine.EventSystems.EventSystem:Update()

第一行听起来好像我可能需要一个回调函数,所以我很快添加了一些东西来测试它

IAsyncResult r1 = ac1.BeginInvoke(ar => Debug.Log("Done"), null);

但运气不好,一切都没有改变。

关于我如何从整体上解决这个问题的任何提示(制作消息框,阻塞线程直到按下按钮),或者可能是关于 Microsoft 如何在 windows 表单中实现该问题的更多信息?

停止尝试使用线程和其他 .NET 异步概念,按照它想要的方式使用 Unity。创建自定义 CustomYieldInstruction 以查看是否显示您的弹出窗口 window。

class WaitWhile: CustomYieldInstruction {
    Func<bool> m_Predicate;

    public override bool keepWaiting { get { return m_Predicate(); } }

    public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
}

用法类似

public GameObject window; //the window that will be shown.

IEnumerator DialogExample()
{
    window.SetActive(true);
    yield return new WaitWhile(() => window.activeInHierarchy);

    //Code here does not run till after the window is deactivated.
}

您通过 StartCoroutine 启动 DialogExample() 或从另一个协程对其执行 yield retrun

WinForms 和Unity 有很大的区别。在 WinForms 中,您有一个用于 UI 的线程,它可能会被模态表单阻止。在 Unity 中,您有多个对象和多个方法,其中脚本执行顺序和一些引擎机制决定它们在每一帧中的执行方式。

但是如果你想在 Unity 中有一个模态消息框,你可以简单地通过添加布尔检查或禁用脚本来阻止特定脚本的 Update 或 FixedUpdate 的执行。第一种方式提供更多选项,但第二种方式更容易。但是请注意,禁用脚本会停止其中的所有内容,除了 InvokeCoroutine.

您可以通过在底层对象上放置一个简单的 SpriteRenderer 或图像来阻止用户与底层对象的交互。此 遮罩 的透明度可以为零,应该是全屏大小并且必须启用 Raycast Target

我更喜欢一个带有全屏遮罩的消息框,它有一个 alpha = .1 的简​​单黑色精灵

public GameObject ModalMessageBox;//game object of message box with a mask

public void TestClick()
{
    StartCoroutine(TestMethod);
    ModalMessageBox.setActive(true);
}

IEnumerator TestMethod()
{
    float time = 0;
    while (time <= 20)
    {
        yield return new WaitForSeconds(.1f);
        time++;
        Debug.Log("Im doing heavy work");
    }
    ModalMessageBox.setActive(false);
}

void Update()
{
    if(ModalMessageBox.activeSelf)
    {
        //handle message box
    }
    else
    {
        //handle normal update stuff
    }
}

请注意,所有其他脚本仍然会 运行。如果你也必须阻止其他脚本的执行,那么你需要一个一个地做。

注:

由于禁用脚本不会停止它启动的协程,因此您不妨禁用脚本本身

public Script1 script1;
public Script2 script2;
public Script3 script3;

void BlockScripts(bool block)
{
    //for singleton scripts:
    Script1.Instance.enabled = !block;
    Script2.Instance.enabled = !block;
    Script3.Instance.enabled = !block;
    //for referenced scripts:
    script1.enabled = !block;
    script2.enabled = !block;
    script3.enabled = !block;

    //self
    enabled = !block;
}

public void TestClick()
{
    StartCoroutine(TestMethod);
    ModalMessageBox.setActive(true);

    BlockScripts(true);
}

IEnumerator TestMethod()
{
    float time = 0;
    while (time <= 20)
    {
        yield return new WaitForSeconds(.1f);
        time++;
        Debug.Log("Im doing heavy work");
    }

    ModalMessageBox.setActive(false);

    BlockScripts(false);
}

void Update()
{
}

其中 Script1、2、3 是单例 类,script1、2、3 是您要阻止的脚本的引用。