以下是关于Unity源码PlayerLoop的简要流程和相关核心点的说明:
1. PlayerLoop简介
PlayerLoop是Unity引擎的主循环(Main Loop)系统,负责驱动游戏的各个阶段(如输入、物理、渲染、脚本更新等)的执行。
它本质上是一个分阶段、分模块的调度系统,保证每一帧都按照既定顺序执行各类引擎和用户逻辑。
2. ReentrancyChecker
ReentrancyChecker是Unity内部用于检测PlayerLoop重入(即递归调用)的工具。
- 作用:防止PlayerLoop在未完成时被再次进入,避免死循环或状态错乱。
- 实现:通常是一个静态计数器或标志位,在进入PlayerLoop时+1,退出时-1,如果发现已进入则报错或断言。
3. IsWorldPlaying
IsWorldPlaying用于判断当前世界(World)是否处于“运行”状态。
- 作用:有些PlayerLoop阶段只在游戏运行时执行(如Update),编辑器暂停或非Play模式下不执行。
- 实现:通常是一个全局或上下文状态标志,PlayerLoop各阶段会根据此状态决定是否执行。
4. s_defaultLoop
s_defaultLoop是Unity内置的默认PlayerLoop配置。
- 作用:定义了每一帧各个阶段(如Initialization、Update、LateUpdate、PreRender等)的顺序和内容。
- 结构:是一个嵌套的
PlayerLoopSystem
树状结构,描述了所有子系统的执行顺序。 - 可通过
PlayerLoop.GetDefaultPlayerLoop()
获取。
5. PlayerLoopSystem组织方式
PlayerLoopSystem是PlayerLoop的核心数据结构,定义如下(伪代码):
struct PlayerLoopSystem
{
Type type; // 阶段类型(如Update、FixedUpdate等)
PlayerLoopSystem[] subSystemList; // 子阶段
UpdateFunction updateDelegate; // 处理函数
IntPtr updateFunction; // 原生函数指针
IntPtr loopConditionFunction; // 条件函数
}
- 树状结构:每个PlayerLoopSystem可以有多个子系统,形成多层嵌套。
- 类型标识:每个阶段用Type区分(如UnityEngine.PlayerLoop.Update)。
- 处理函数:每个阶段可绑定C#委托或原生函数。
6. PlayerLoopSystem的处理函数
- updateDelegate:C#层的委托,指向该阶段要执行的函数。
- updateFunction:原生层的函数指针,通常用于C++实现的引擎模块。
- loopConditionFunction:判断该阶段是否需要执行的条件函数。
执行时,PlayerLoop会遍历PlayerLoopSystem树,依次调用每个阶段的处理函数。
7. ExecutePlayerLoop
ExecutePlayerLoop是PlayerLoop的核心执行入口。
- 作用:递归遍历PlayerLoopSystem树,按顺序执行每个阶段的处理函数。
- 伪代码流程如下:
void ExecutePlayerLoop(PlayerLoopSystem system)
{
if (system.loopConditionFunction == null || system.loopConditionFunction())
{
if (system.updateDelegate != null)
system.updateDelegate();
else if (system.updateFunction != IntPtr.Zero)
CallNativeFunction(system.updateFunction);
if (system.subSystemList != null)
{
foreach (var sub in system.subSystemList)
ExecutePlayerLoop(sub);
}
}
}
- 主循环:每帧由Unity底层驱动,调用顶层PlayerLoopSystem(如s_defaultLoop)。
- 递归执行:每个阶段先执行自身处理函数,再递归执行子阶段。
总结流程图
- 主循环启动(如每帧由引擎底层调用)
- ReentrancyChecker检测重入
- IsWorldPlaying判断是否执行
- 从s_defaultLoop开始递归
- PlayerLoopSystem树遍历
- 检查loopConditionFunction
- 执行updateDelegate或updateFunction
- 递归子系统
- 一帧结束
参考
下面继续详细补充PlayerLoop相关的进阶内容,包括自定义、调试、常见用法和注意事项:
8. 自定义PlayerLoop
Unity允许开发者通过C# API自定义PlayerLoop流程,插入、替换或移除某些阶段,以实现特殊的执行顺序或功能。
8.1 获取和设置PlayerLoop
// 获取当前PlayerLoop
var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop();
// 修改playerLoop(如插入自定义阶段)
// ...
// 设置新的PlayerLoop
UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop);
8.2 插入自定义阶段
假设你要在Update阶段插入自己的处理:
using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
public class MyCustomSystem
{
public static void MyUpdate()
{
Debug.Log("My Custom Update");
}
}
// 获取默认PlayerLoop
var loop = PlayerLoop.GetDefaultPlayerLoop();
// 找到Update阶段
for (int i = 0; i < loop.subSystemList.Length; i++)
{
if (loop.subSystemList[i].type == typeof(Update))
{
// 构造自定义阶段
var mySystem = new PlayerLoopSystem
{
type = typeof(MyCustomSystem),
updateDelegate = MyCustomSystem.MyUpdate
};
// 插入到Update子系统的开头
var subs = loop.subSystemList[i].subSystemList.ToList();
subs.Insert(0, mySystem);
loop.subSystemList[i].subSystemList = subs.ToArray();
break;
}
}
// 应用修改
PlayerLoop.SetPlayerLoop(loop);
8.3 注意事项
- 修改PlayerLoop后,所有MonoBehaviour的生命周期(如Update)依然会被正常调度。
- 自定义阶段的执行顺序完全由你插入的位置决定。
- 不建议频繁动态修改PlayerLoop,通常在游戏初始化时设置一次即可。
9. PlayerLoop调试与分析
9.1 查看PlayerLoop结构
可以通过递归遍历PlayerLoopSystem
树,打印出所有阶段和子阶段,帮助理解执行顺序。
void PrintPlayerLoop(PlayerLoopSystem system, int depth = 0)
{
Debug.Log(new string(' ', depth * 2) + system.type);
if (system.subSystemList != null)
{
foreach (var sub in system.subSystemList)
PrintPlayerLoop(sub, depth + 1);
}
}
9.2 性能分析
- 自定义阶段中应避免耗时操作,防止卡顿。
- 可用Profiler标记自定义阶段,便于性能分析。
10. PlayerLoop常见用法
- 自定义调度系统:如ECS、Job System等可通过PlayerLoop插入自定义调度点。
- 热更框架:如ILRuntime、HybridCLR等热更方案,常通过PlayerLoop插入热更入口。
- 特殊渲染/物理/网络同步:可在特定阶段插入自定义逻辑,保证时序正确。
11. PlayerLoop注意事项
- 兼容性:自定义PlayerLoop在不同Unity版本可能有差异,注意API变动。
- 生命周期:PlayerLoop只在Play模式下完整运行,编辑器下部分阶段不会执行。
- 线程安全:PlayerLoop所有阶段默认在主线程执行,需自行管理多线程逻辑。
12. 典型PlayerLoop阶段(Unity 2021+)
- Initialization
- EarlyUpdate
- FixedUpdate
- PreUpdate
- Update
- PreLateUpdate
- PostLateUpdate
- TimeUpdate
- PhysicsUpdate
- ScriptRunBehaviourUpdate(MonoBehaviour.Update)
- ScriptRunDelayedDynamicFrameRate
- ScriptRunDelayedTasks
每个阶段下又有多个子阶段,具体可用PrintPlayerLoop
方法查看。
下面继续详细补充PlayerLoop相关的进阶内容,包括自定义、调试、常见用法和注意事项:
8. 自定义PlayerLoop
Unity允许开发者通过C# API自定义PlayerLoop流程,插入、替换或移除某些阶段,以实现特殊的执行顺序或功能。
8.1 获取和设置PlayerLoop
// 获取当前PlayerLoop
var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop();
// 修改playerLoop(如插入自定义阶段)
// ...
// 设置新的PlayerLoop
UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop);
8.2 插入自定义阶段
假设你要在Update阶段插入自己的处理:
using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
public class MyCustomSystem
{
public static void MyUpdate()
{
Debug.Log("My Custom Update");
}
}
// 获取默认PlayerLoop
var loop = PlayerLoop.GetDefaultPlayerLoop();
// 找到Update阶段
for (int i = 0; i < loop.subSystemList.Length; i++)
{
if (loop.subSystemList[i].type == typeof(Update))
{
// 构造自定义阶段
var mySystem = new PlayerLoopSystem
{
type = typeof(MyCustomSystem),
updateDelegate = MyCustomSystem.MyUpdate
};
// 插入到Update子系统的开头
var subs = loop.subSystemList[i].subSystemList.ToList();
subs.Insert(0, mySystem);
loop.subSystemList[i].subSystemList = subs.ToArray();
break;
}
}
// 应用修改
PlayerLoop.SetPlayerLoop(loop);
8.3 注意事项
- 修改PlayerLoop后,所有MonoBehaviour的生命周期(如Update)依然会被正常调度。
- 自定义阶段的执行顺序完全由你插入的位置决定。
- 不建议频繁动态修改PlayerLoop,通常在游戏初始化时设置一次即可。
9. PlayerLoop调试与分析
9.1 查看PlayerLoop结构
可以通过递归遍历PlayerLoopSystem
树,打印出所有阶段和子阶段,帮助理解执行顺序。
void PrintPlayerLoop(PlayerLoopSystem system, int depth = 0)
{
Debug.Log(new string(' ', depth * 2) + system.type);
if (system.subSystemList != null)
{
foreach (var sub in system.subSystemList)
PrintPlayerLoop(sub, depth + 1);
}
}
9.2 性能分析
- 自定义阶段中应避免耗时操作,防止卡顿。
- 可用Profiler标记自定义阶段,便于性能分析。
10. PlayerLoop常见用法
- 自定义调度系统:如ECS、Job System等可通过PlayerLoop插入自定义调度点。
- 热更框架:如ILRuntime、HybridCLR等热更方案,常通过PlayerLoop插入热更入口。
- 特殊渲染/物理/网络同步:可在特定阶段插入自定义逻辑,保证时序正确。
11. PlayerLoop注意事项
- 兼容性:自定义PlayerLoop在不同Unity版本可能有差异,注意API变动。
- 生命周期:PlayerLoop只在Play模式下完整运行,编辑器下部分阶段不会执行。
- 线程安全:PlayerLoop所有阶段默认在主线程执行,需自行管理多线程逻辑。
12. 典型PlayerLoop阶段(Unity 2021+)
- Initialization
- EarlyUpdate
- FixedUpdate
- PreUpdate
- Update
- PreLateUpdate
- PostLateUpdate
- TimeUpdate
- PhysicsUpdate
- ScriptRunBehaviourUpdate(MonoBehaviour.Update)
- ScriptRunDelayedDynamicFrameRate
- ScriptRunDelayedTasks
每个阶段下又有多个子阶段,具体可用PrintPlayerLoop
方法查看。