目錄 Table of contents
本篇文章撰寫於2019/11/17,版本為 Unity2018.4
原由
最近卡在一個問題:
「Physics2D.autoSyncTransforms = false 的情況下,Unity內部到底何時會進行 Sync?」
根據官方工程師的講法,如果關閉時應該是只會在FixedUpdate 以前 Sync 一次,其他時間點不會同步。
但經過測試,疑似在 Update 期間也會 Sync 啊?
偏偏Unity官網提供的執行順序圖根本沒有寫到何時Sync。
於是後來翻到了 Unity 有提供一個實驗性的底層 API : PlayerLoop
它號稱可以讓使用者修改 Unity 主迴圈,因此這對我們分析 Unity 執行順序非常有幫助。
流程
將整個主迴圈列出來:
ROOT NODE
Initialization
PlayerUpdateTime
AsyncUploadTimeSlicedUpdate
SynchronizeInputs
SynchronizeState
XREarlyUpdate
EarlyUpdate
PollPlayerConnection
ProfilerStartFrame
GpuTimestamp
AnalyticsCoreStatsUpdate
UnityWebRequestUpdate
ExecuteMainThreadJobs
ProcessMouseInWindow
ClearIntermediateRenderers
ClearLines
PresentBeforeUpdate
ResetFrameStatsAfterPresent
UpdateAllUnityWebStreams
UpdateAsyncReadbackManager
UpdateStreamingManager
UpdateTextureStreamingManager
UpdatePreloading
RendererNotifyInvisible
PlayerCleanupCachedData
UpdateMainGameViewRect
UpdateCanvasRectTransform
XRUpdate
UpdateInputManager
ProcessRemoteInput
ScriptRunDelayedStartupFrame
UpdateKinect
DeliverIosPlatformEvents
TangoUpdate
DispatchEventQueueEvents
DirectorSampleTime
PhysicsResetInterpolatedTransformPosition
SpriteAtlasManagerUpdate
PerformanceAnalyticsUpdate
FixedUpdate
ClearLines
NewInputFixedUpdate
DirectorFixedSampleTime
AudioFixedUpdate
ScriptRunBehaviourFixedUpdate
DirectorFixedUpdate
LegacyFixedAnimationUpdate
XRFixedUpdate
PhysicsFixedUpdate
Physics2DFixedUpdate
DirectorFixedUpdatePostPhysics
ScriptRunDelayedFixedFrameRate
PreUpdate
PhysicsUpdate
Physics2DUpdate
CheckTexFieldInput
IMGUISendQueuedEvents
NewInputUpdate
SendMouseEvents
AIUpdate
WindUpdate
UpdateVideo
Update
ScriptRunBehaviourUpdate
ScriptRunDelayedDynamicFrameRate
ScriptRunDelayedTasks
DirectorUpdate
PreLateUpdate
AIUpdatePostScript
DirectorUpdateAnimationBegin
LegacyAnimationUpdate
DirectorUpdateAnimationEnd
DirectorDeferredEvaluate
UNetUpdate
EndGraphicsJobsAfterScriptUpdate
ParticleSystemBeginUpdateAll
ScriptRunBehaviourLateUpdate
ConstraintManagerUpdate
PostLateUpdate
PlayerSendFrameStarted
DirectorLateUpdate
ScriptRunDelayedDynamicFrameRate
PhysicsSkinnedClothBeginUpdate
UpdateRectTransform
UpdateCanvasRectTransform
PlayerUpdateCanvases
UpdateAudio
VFXUpdate
ParticleSystemEndUpdateAll
EndGraphicsJobsAfterScriptLateUpdate
UpdateCustomRenderTextures
UpdateAllRenderers
EnlightenRuntimeUpdate
UpdateAllSkinnedMeshes
ProcessWebSendMessages
SortingGroupsUpdate
UpdateVideoTextures
UpdateVideo
DirectorRenderImage
PlayerEmitCanvasGeometry
PhysicsSkinnedClothFinishUpdate
FinishFrameRendering
BatchModeUpdate
PlayerSendFrameComplete
UpdateCaptureScreenshot
PresentAfterDraw
ClearImmediateRenderers
PlayerSendFramePostPresent
UpdateResolution
InputEndFrame
TriggerEndOfFrameCallbacks
GUIClearEvents
ShaderHandleErrors
ResetInputAxis
ThreadedLoadingDebug
ProfilerSynchronizeStats
MemoryFrameMaintenance
ExecuteGameCenterCallbacks
ProfilerEndFrame
蠻複雜的,需要一個視覺化的UI比較好觀察。
在網路上找到一位網友 Lotte 用 PlayerLoop 寫了一個視覺化看主迴圈執行順序的腳本
這不但是一個很好參考的使用案例,更是一個功能強大的腳本。
也因此接下來就用這個腳本來修改成我要的功能。
首先,先建一個 Singleton 的測試腳本掛在有 Rigidbody2D 的物件上。
腳本寫了一個檢查是否 Sync 的方法,回傳 True 代表已經 Sync 了。
public static bool TestIfSyncTransforms() {
// Singleton.
if (s_Instance == null) {
return false;
}
// Check if sync transform.
if ((Vector2)s_Instance.transform.position == s_Instance.m_Rigidbody2D.position) {
// Modify transform for next checking.
s_Instance.transform.position += Vector3.right;
return true;
}
return false;
}
在 Default PlayerLoop 內將的每個 Update 之間插入一個 CustomSyncTestUpdate。
// 修改Lotte腳本內的方法
PlayerLoopSystem GenerateCustomLoop() {
// Note: this also resets the loop to its defalt state first.
var playerLoop = PlayerLoop.GetDefaultPlayerLoop();
hasCustomPlayerLoop = true;
// 僅插入從FixedUpdate至Update之間的所有內部Update
int subSystemListFrom = 2; // Fixed Update
int subSystemListTo = 4; // Update
for (int i = subSystemListFrom; i <= subSystemListTo; i++) {
var updateSystem = playerLoop.subSystemList[i];
var newList = new List<PlayerLoopSystem>(updateSystem.subSystemList);
// Insert the start event.
newList.Insert(0, CustomSyncTestUpdate.GetNewSystem(string.Format("{0} Start", updateSystem.type.Name)));
for (int j = 1; j < newList.Count; j++) {
var subUpdate = newList[j];
newList.Insert(j + 1, CustomSyncTestUpdate.GetNewSystem(string.Format("After {0}", subUpdate.type.Name)));
j += 1;
}
// convert the list back to an array and plug it into the Update system.
updateSystem.subSystemList = newList.ToArray();
// dont forget to put our newly edited System back into the main player loop system!!
playerLoop.subSystemList[i] = updateSystem;
}
return playerLoop;
}
public struct CustomSyncTestUpdate {
public static PlayerLoopSystem GetNewSystem(string testScope) {
return new PlayerLoopSystem() {
type = typeof(CustomSyncTestUpdate),
updateDelegate = () => UpdateFunction(testScope)
};
}
public static void UpdateFunction(string testScope) {
bool sync = SyncTransformsTester.TestIfSyncTransforms();
if (sync) {
// 若有Sync則Log以紅字顯示
Debug.LogFormat("<color=red>[{0}]: {1}</color>", testScope, sync);
} else {
Debug.LogFormat("[{0}]: {1}", testScope, sync);
}
}
}
插入後的主迴圈長這樣:
另外在測試物件上的腳本也添加以下 Code 來檢查是否 Sync :
(Log 中以綠色字體顯示)
private void FixedUpdate() {
Debug.Log("<color=green>FixedUpdate!</color>");
bool sync = TestIfSyncTransforms();
if (sync) {
Debug.LogFormat("<color=green>[{0}]: {1}</color>", "FixedUpdate", sync);
}
}
private void Update() {
Debug.Log("<color=green>Update!</color>");
bool sync = TestIfSyncTransforms();
if (sync) {
Debug.LogFormat("<color=green>[{0}]: {1}</color>", "Update", sync);
}
}
private void LateUpdate() {
Debug.Log("<color=green>LateUpdate!</color>");
bool sync = TestIfSyncTransforms();
if (sync) {
Debug.LogFormat("<color=green>[{0}]: {1}</color>", "Update", sync);
}
}
測試結果
經 Log 發現,僅在 Physics2DFixedUpdate 時更新。
照順序來看,可以得知 Physics2DFixedUpdate 就是 InternalPhysicsUpdate。
而且 Sync 只會在 InternalPhysicsUpdate 的時候才更新。照工程師的說法,應該是一進去就先 Sync。
經測試,以 Transform.position
為優先,Rigidbody2D.position
會直接被蓋掉。
但是之前若將 Rigidbody2D.Interpolation
打開,會變以下結果:
多了個 Physics2DUpdate 的時候會 Sync。
這個 Physics2DUpdate 是每次 Update() 以前都會跑的,位於 PreUpdate 階段。
可以理解成,Interpolation 修改 transform.position 的地方就是這個 Physics2DUpdate。
所以當他發現 transform.position
是 dirty 狀態時會直接 Sync,而不會覆蓋過去。
結論
當你要修改 Rigidbody2D 物件的座標時,不管改 Transform.position
或是 Rigidbody2D.position
本身都可行,但有以下注意事項:
- 若同時修改 Rigidbody 和 Transform,以 Transform 為主。
- 根據官網說明,修改Transform效能較差,因為它需要多走Sync的步驟。
- 關閉
autoSyncTransforms
的情況下:- 沒有開啟 Interpolation 時,每次 InternalPhysics 執行前才會 Sync。
- 開啟 Interpolation 時,除了每次 InternalPhysics 執行前會 Sync 以外,每次 Update 以前也會 Sync。
- 所以頻繁更新 Transform 的情況下就別開 Interpolation 了,因為有開跟沒開一樣都會被蓋掉。
2021/08/29更新
最近在測試時發現Unity2019.4已經不是上面那個邏輯了==
差別是不管有沒有開啟Interpolation,每次Update以前都會在PreUpdate.Physics2DUpdate階段Sync回Rigidbody。