Unity 引擎的游戏逻辑和渲染,都是在主线程中进行的,和大多数其他游戏引擎一样,Unity 也是一个单线程的引擎。这与常规开发的思路可能不太一样,通常认为多线程能够很大程度上提升程序的性能效率,何况是游戏这种需要大量资源和大量计算的复杂程序。其实不然,单线程能为游戏引擎开发和游戏开发带来很多好处,未经过精心设计的多线程也可能会为中后期开发带来灾难性的后果。但这不是本文要讨论的重点,感兴趣的话可以自行搜索游戏引擎设计和多线程相关的资料。
综合考虑众多因素,大量游戏引擎都采用了单线程的设计,但这也带来了一些问题,Unity 提供的大多数非值类型对象都是不允许在主线程之外访问的,但很多时候应该使用或者无法避免使用多线程,同时子线程也必须要和主线程发生交互,例如:
- 1.相对独立且运算复杂的独立模块,比如网络通讯。游戏与服务端的数据交互,解析结果最终需要反馈到游戏逻辑中呈现给玩家。
- 2.支付、广告、统计分析等众多第三方SDK的逻辑代码会在各自的线程上执行,但SDK各种功能的状态和结果是需要通知游戏主线程的。
目前我使用的方法是利用 C# 的委托功能,将子线程回调主线程的函数以事件形式注册到委托上,主线程使用一个管理类,按一定频率去查询尚未执行的子线程事件,并依次执行。
定义待执行的子线程事件队列:
private static bool NoUpdate = true;
private static List<Action> UpdateQueue = new List<Action>();
private static List<Action> UpdateRunQueue = new List<Action>();
添加待执行子线程事件的接口:
public static void ExecuteUpdate(Action action)
{
lock (UpdateQueue)
{
UpdateQueue.Add(action);
NoUpdate = false;
}
}
主线程上的轮询方法:
private void Update()
{
lock (UpdateQueue)
{
if (NoUpdate) return;
UpdateRunQueue.AddRange(UpdateQueue);
UpdateQueue.Clear();
NoUpdate = true;
for (var i = 0; i < UpdateRunQueue.Count; i++)
{
var action = UpdateRunQueue[i];
if (action == null) continue;
action();
}
UpdateRunQueue.Clear();
}
}
可以将这些方法都写在一个 MonoBehaviour
的单例类中,子线程通过单例静态接口访问到这个管理类进行事件添加,主线程通过 Unity 自身生命周期的 Update()
去轮询执行子线程的回调事件,对以上代码稍加复制和修改,就可以实现 ExecuteLateUpdate
和 ExecuteFixedUpdate
接口,在必要的时候将子线程事件安排到一帧的末尾或者在物理更新中执行。
这样做也有一些问题,所有的子线程回调在主线程上的执行都不是立即的,根据轮询时机的不同,子线程发起的回调事件的执行会有一定的延时,如果是附加到 Update
中执行则最多会延迟一帧的时间,但帧数受到设备性能的影响,延时时间也会有波动,如果附加到 FixedUpdate
执行,延时问题会得到所改善。
通过子线程在主线程上创建协程和延时执行:
public static void ExecuteDelay(Action action, float delayTime, bool timeScale = true)
{
ExecuteCoroutine(DelayCoroutine(action, delayTime, timeScale));
}
private static IEnumerator DelayCoroutine(Action action, float delayTime, bool timeScale = true)
{
if (timeScale)
{
yield return new WaitForSeconds(delayTime);
}
else
{
yield return new WaitForSecondsRealtime(delayTime);
}
action();
}