前言
Unity的大部分API(例如,与游戏对象交互,修改组件属性等)都需要在主线程中调用。然而,有时你可能在另一个线程(例如,网络请求,长时间运行的计算等)中完成一些工作,并且在完成后需要更新Unity的某些东西。在这种情况下,你不能直接从那个线程调用Unity API,因为这可能会导致未定义的行为或错误。
虽然你可以在其他线程中进行计算密集型的任务(例如,AI计算,物理模拟等),但是你不能在其他线程中直接调用Unity的API。因此你需要类似DwkUnityMainThreadDispatcher
这样的类,它可以让你在其他线程中安全地调用Unity的API。
例如,你可以这样使用它:
DwkUnityMainThreadDispatcher.Instance().Enqueue(() =>
{
// 在这里调用Unity API
});
UnityMainThreadDispatcher
类代码
DwkUnityMainThreadDispatcher类原貌:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DwkUnityMainThreadDispatcher : MonoBehaviour
{
private static DwkUnityMainThreadDispatcher instance;
private readonly Queue<System.Action> actions = new Queue<System.Action>();
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public static DwkUnityMainThreadDispatcher Instance()
{
if (!instance)
{
throw new System.Exception("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene.");
}
return instance;
}
public void Enqueue(System.Action action)
{
lock (actions)
{
actions.Enqueue(action);
}
}
public void Update()
{
while (actions.Count > 0)
{
actions.Dequeue().Invoke();
}
}
}
举例:在非主线程中播放Unity声音
if (sum > 0)
{
DwkUnityMainThreadDispatcher.Instance().Enqueue(() =>
{
audioSource1.PlayOneShot(audioSource1.clip);
});
//Debug.Log("加分特效");
}
附录:
实现单例模式的标准流程
private static DwkUnityMainThreadDispatcher instance;
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public static DwkUnityMainThreadDispatcher Instance()
{
if (!instance)
{
throw new System.Exception("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene.");
}
return instance;
}
这个单例模式的实现步骤如下:
-
声明一个私有静态变量
instance
来存储单例实例。 -
在
Awake()
方法中,检查instance
是否为null
。如果为null
,则将instance
设置为当前实例,并使用DontDestroyOnLoad(gameObject)
确保在加载新场景时不会销毁这个对象。如果instance
不为null
,则销毁当前游戏对象,这样可以确保只有一个DwkUnityMainThreadDispatcher
实例存在。 -
提供一个公共静态方法
Instance()
来获取单例实例。如果instance
为null
,则抛出一个异常,提示用户需要在场景中添加MainThreadExecutor Prefab。
这种方式的单例模式在Unity中很常见,因为它可以确保在整个游戏运行期间,只有一个DwkUnityMainThreadDispatcher
实例存在,而且可以在任何地方通过DwkUnityMainThreadDispatcher.Instance()
访问这个实例。
允许其他线程将任务安全地调度到主线程中执行
private readonly Queue<System.Action> actions = new Queue<System.Action>();
public void Enqueue(System.Action action)
{
lock (actions)
{
actions.Enqueue(action);
}
}
public void Update()
{
while (actions.Count > 0)
{
actions.Dequeue().Invoke();
}
}
-
在其他线程中,当你需要执行一些必须在主线程中进行的操作(例如,调用Unity的API)时,你可以创建一个
System.Action
,这个System.Action
包含了你想要执行的操作。 -
你将这个
System.Action
传递给Enqueue
方法。Enqueue
方法将这个System.Action
添加到actions
队列中。这个过程是线程安全的,因为Enqueue
方法使用了lock
关键字来确保在多线程环境下,添加任务到队列的操作是线程安全的。 -
在主线程中,Unity每一帧都会调用
Update
方法。在Update
方法中,它会检查actions
队列中是否有任务。如果有,它会使用Dequeue
方法取出队列中的第一个任务,并使用Invoke
方法执行这个任务。这个过程会一直进行,直到actions
队列中没有任务为止。
这样,你就可以在其他线程中安全地调度任务到主线程中执行了。这对于在其他线程中进行一些耗时的操作,然后需要更新Unity的某些东西(例如,更新UI,创建游戏对象等)非常有用。
-
private readonly Queue<System.Action> actions = new Queue<System.Action>();
这行代码创建了一个队列,用于存储要在主线程中执行的任务。每个任务都是一个System.Action
,也就是一个无参数且无返回值的委托。 -
public void Enqueue(System.Action action)
这个方法用于将一个任务添加到队列中。这个任务将在主线程中执行。lock (actions)
这行代码确保了在多线程环境下,添加任务到队列的操作是线程安全的。 -
public void Update()
这个方法在每一帧都会被Unity调用,它在主线程中执行。在这个方法中,它会执行队列中的所有任务。这是通过在队列中有任务时调用actions.Dequeue().Invoke()
来实现的。Dequeue()
方法移除并返回队列中的第一个任务,Invoke()
方法则执行这个任务。
这部分代码提供了一种在主线程中安全执行任务的方式。你可以在任何线程中使用Enqueue
方法来添加一个任务,这个任务将在主线程中执行。这样,你就可以安全地从任何线程调用Unity API了。