登入
首頁 所有文章 Unity3D 解決Unity中使用FixedUpdate位移時的不連續
   2018-12-16 00:49:20(10個月前)    356點閱   1喜歡  0收藏
解決Unity中使用FixedUpdate位移時的不連續  
首先聲明,這篇筆記的主要資料來源來自此:Timesteps and Achieving Smooth Motion in Unity,是我閱讀理解並查證相關資料後的整理。
 

 
Unity中,遊戲主迴圈以可變的time step(時間間隔)作為畫面的更新率,這個時間差在Unity 叫做deltaTime,它的優點是能在不同硬體規格上盡快的執行updates,但也容易造成位移看起來斷斷續續。
 
Unity的主迴圈
 
Unity的運作順序,在官方提供的手冊中有提供:https://docs.unity3d.com/uploads/Main/monobehaviour_flowchart.svg
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();
    }

void FixedUpdate() {
        m_interpolatedTransform.LateFixedUpdate();
    }
}

 
三個腳本必須如上圖這樣設定,附帶InterpolatedTransform的物件必須在FixedUpdate中移動,因為任何在Update中的transformations(pos,rota,scale改變)都會覆寫插值。
 
參考資料
 
Timesteps and Achieving Smooth Motion in Unity
Unity官方手冊
FixedUpdate真的是固定的时间间隔执行吗?聊聊游戏定时器
             

如要發表回覆,請先 登入

  0則回覆