Unity中,遊戲主迴圈以可變的time step(時間間隔)作為畫面的更新率,這個時間差在Unity 叫做deltaTime,它的優點是能在不同硬體規格上盡快的執行updates,但也容易造成位移看起來斷斷續續。
Unity的主迴圈
Update與FixedUpdate是交錯執行而非同時執行的,搞錯這點的話可能會不能理解為什麼當time step > Fixed time step會造成看起來不連續的位移了。
我們看到下面這段程式碼,Unity的迴圈大約可能長這樣子:
float currentSimulationTime = 0;
float lastUpdateTime = 0;
while (!quit) // variable steps
{
while (currentSimulationTime < Time.time) // fixed steps
{
FixedUpdate();
Physics.SimulationStep(currentState, Time.fixedDeltaTime);
currentSimulationTime += Time.fixedDeltaTime;
}
Time.deltaTime = Time.time - lastUpdateTime;
Update();
Render();
lastUpdateTime = Time.time;
}
主迴圈中,還有一個子迴圈,用來呼叫FixedUpdate()及物理運算,當當前模擬時間小於 Time.time(從遊戲開始時計算的時間 遊戲暫停時也會停止增加)會執行一次物理運算及FixedUpdate()。
理想上,我們會希望更新的情況長這樣:
實際上:
大多數情況下,遊戲畫面的更新率會大於FixedUpdate(),所以物件位置的更新並不會像畫面更新那樣頻繁的更新位置。
(左邊的攝影機在fixedUpdate中移動)
這裡必須注意,Fixed time的固定不是以固定實時差距執行的,而是以固定的時間差距量運算,以保證物理模擬的正確性。
根據前面的程式碼,可以發現,假設執行Update時卡住了1s,而fixed time step為 0.02s,則在下一次Update前,FixedUpdate()會多執行50次補上之前漏掉的,這導致單次的位移量可能很大。
基於硬體與複雜的畫面變化,遊戲的畫面更新率幾乎是不可能維持穩定的,畫面每次更新時,有大量的運算要執行,一定會延遲個0.多秒,而這就足以造成位移的不連續。
解決方法
我們可以使用內插法或外插法,填滿在兩個Fixedupdate間的畫面,以達成平滑位移。
內插法
使用內插法平滑fixedDeltatime的結果,使繪製出位移的結果(位置)延遲,而這個延遲通常是可接受的。
外插法
外插法,預測物件在下個fixed step應該在哪,避免延遲,但這會更難繪製連續的結果,以及造成額外花費。
這邊有一個使用內插法的解決方案,主要需要三個腳本:
1.InterpolationController:
紀錄最近兩次的fixed step,在Updates與Time.time比較後產生一個全域插值。
using UnityEngine;
using System.Collections;
public class InterpolationController : MonoBehaviour
{
private float[] m_lastFixedUpdateTimes;
private int m_newTimeIndex;
private static float m_interpolationFactor;
public static float InterpolationFactor {
get { return m_interpolationFactor; }
}
public void Start() {
m_lastFixedUpdateTimes = new float[2];
m_newTimeIndex = 0;
}
public void FixedUpdate() {
m_newTimeIndex = OldTimeIndex();
m_lastFixedUpdateTimes[m_newTimeIndex] = Time.fixedTime;
}
public void Update() {
float newerTime = m_lastFixedUpdateTimes[m_newTimeIndex];
float olderTime = m_lastFixedUpdateTimes[OldTimeIndex()];
if (newerTime != olderTime) {
m_interpolationFactor = (Time.time - newerTime) / (newerTime - olderTime);
} else {
m_interpolationFactor = 1;
}
}
private int OldTimeIndex() {
return (m_newTimeIndex == 0 ? 1 : 0);
}
}
2.InterpolatedTransform :
儲存物件最近兩次fixed step的transform,並對兩者使用插值法(使用全域插值)
如果你想移動物件,並不希望使用平滑 呼叫ForgetPreviousTransform後移動物件
這腳本要在加在需要使用FixedUpdate移動的物件上
using UnityEngine;
using System.Collections;
[RequireComponent(typeof(InterpolatedTransformUpdater))]
public class InterpolatedTransform : MonoBehaviour
{
private TransformData[] m_lastTransforms;
private int m_newTransformIndex;
void OnEnable() {
ForgetPreviousTransforms();
}
public void ForgetPreviousTransforms() {
m_lastTransforms = new TransformData[2];
TransformData t = new TransformData(
transform.localPosition,
transform.localRotation,
transform.localScale);
m_lastTransforms[0] = t;
m_lastTransforms[1] = t;
m_newTransformIndex = 0;
}
void FixedUpdate() {
TransformData newestTransform = m_lastTransforms[m_newTransformIndex];
transform.localPosition = newestTransform.position;
transform.localRotation = newestTransform.rotation;
transform.localScale = newestTransform.scale;
}
public void LateFixedUpdate() {
m_newTransformIndex = OldTransformIndex();
m_lastTransforms[m_newTransformIndex] = new TransformData(
transform.localPosition,
transform.localRotation,
transform.localScale);
}
void Update() {
TransformData newestTransform = m_lastTransforms[m_newTransformIndex];
TransformData olderTransform = m_lastTransforms[OldTransformIndex()];
transform.localPosition = Vector3.Lerp(
olderTransform.position,
newestTransform.position,
InterpolationController.InterpolationFactor);
transform.localRotation = Quaternion.Slerp(
olderTransform.rotation,
newestTransform.rotation,
InterpolationController.InterpolationFactor);
transform.localScale = Vector3.Lerp(
olderTransform.scale,
newestTransform.scale,
InterpolationController.InterpolationFactor);
}
private int OldTransformIndex() {
return (m_newTransformIndex == 0 ? 1 : 0);
}
private struct TransformData {
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public TransformData(Vector3 position, Quaternion rotation, Vector3 scale) {
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
}
}FixedUpdate:
將最近一次的transform讀入transform
LatedFixedUpdate:
將當前transform紀錄
Update:將物件位置繪製在兩次Fixed step之間,並用InterpolationController.InterpolationFactor平滑
3. InterpolatedTransformUpdater:
每次FixedUpdate()執行時 呼叫LateFixedUpdate紀錄當前transform
using UnityEngine;
using System.Collections;
public class InterpolatedTransformUpdater : MonoBehaviour
{
private InterpolatedTransform m_interpolatedTransform;
void Awake() {
m_interpolatedTransform = GetComponent<InterpolatedTransform>();
}
void FixedUpdate() {
m_interpolatedTransform.LateFixedUpdate();
}
}
三個腳本必須如上圖這樣設定,附帶InterpolatedTransform的物件必須在FixedUpdate中移動,因為任何在Update中的transformations(pos,rota,scale改變)都會覆寫插值。
參考資料