Unity UGUI Button OnClick 與 Input System 的時間差問題
在 Unity 中同時使用 UGUI Button 的 OnClick 事件與 New Input System 監聽同一個點擊動作時,會發現兩者幾乎總是同時觸發,但 Input System 永遠比 UGUI OnClick 早約 0.05 秒(50ms)。
真實點擊發生: 12:00:00.000
Input System 觸發: 12:00:00.000 ✅ 幾乎即時
UGUI OnClick 觸發: 12:00:00.050 ⏳ 延遲約 50ms
這不是隨機的誤差,而是架構上的必然結果。
兩者的時間點並不一樣
Input System 的執行路徑
Input System 在每幀極早期直接從設備讀取狀態,幾乎與真實輸入同步:
硬體輸入 → Input System polling → callback 觸發
它走的是 事件驅動(event-driven) 架構,輸入狀態改變的當下就觸發,不需要等待任何 UI 系統。
UGUI OnClick 的執行路徑
UGUI 走的是完全不同的路徑,需要經過多個系統的串聯處理:
EventSystem.Update()
→ StandaloneInputModule / InputSystemUIInputModule
→ PointerEventData 建立
→ GraphicRaycaster 射線檢測
→ ExecuteEvents.Execute()
→ Button.OnClick.Invoke() ← 這裡才觸發
整條鏈都在 EventSystem.Update() 裡執行,屬於普通的 MonoBehaviour.Update 呼叫順序。
有幾個因素疊加造成這個延遲差距固定約 50ms
1. 幀邊界延遲
Input System 在幀的早期觸發,而 EventSystem.Update() 在同幀較晚才執行。即使在同一幀,也存在執行順序差距。
2. PointerEventData 狀態機
UGUI 必須確認完整的 PointerDown → PointerUp → Click 流程發生在同一個 UI 元件上,這個確認本身就跨越了多個處理步驟。
3. GraphicRaycaster 射線檢測
每次 EventSystem.Update() 都要對 UI Canvas 做射線檢測,複雜的 UI 層級會增加這個耗時。
4. 一幀延遲(最少)
在某些設定下,InputSystemUIInputModule 讀取輸入狀態和 Input System callback 觸發之間,存在至少一幀的讀取時間差(約 16ms @ 60fps)。
麻煩點在于 UGUI OnClick 的時間戳不代表真實點擊時間
這是最核心的問題所在:
UGUI
OnClick觸發的時刻,記錄的是「EventSystem 處理到這個事件的時間」,而不是「使用者真正點擊的時間」。因此,比較兩者的觸發時間,等同於在比較兩個不同的時間零點,這個差距無法透過程式邏輯消除。
解決方案
Case 1 用 Input System 記錄真實時間戳
如果你需要精確的點擊時間(例如音遊判定、Combo 系統),應以 Input System 的時間為唯一基準,UGUI OnClick 只負責觸發動作:
private double _realClickTime;
void OnEnable()
{
_clickAction.performed += ctx =>
{
// ctx.time 才是真實的輸入時間
_realClickTime = ctx.time;
};
}
// Button 的 OnClick 事件指向這裡
public void OnButtonClick()
{
// 使用 Input System 記錄的真實時間,而非 Time.time
ProcessClick(_realClickTime);
}
Case2 不要用 UGUI OnClick,自己做 Raycast
如果不需要 UGUI 的事件系統,可以在 Input System callback 裡自行判斷是否點在目標上:
_clickAction.performed += ctx =>
{
if (IsPointerOverTargetButton())
{
HandleClick(ctx.time);
}
};
private bool IsPointerOverTargetButton()
{
var pointerData = new PointerEventData(EventSystem.current)
{
position = Mouse.current.position.ReadValue()
};
var results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerData, results);
return results.Any(r => r.gameObject == targetButton.gameObject);
}
Case3. 選一個系統,不要兩個同時監聽
最簡單的做法是只用其中一個系統處理同一個動作,避免時間基準不一致的問題:
// 只用 Input System,Button 的 OnClick 留空
_clickAction.performed += ctx => HandleClick();
// 或者只用 UGUI OnClick,完全不監聽 Input System
// (在 Button Inspector 裡設定即可)
留言
張貼留言