小屋創作

日誌2018-12-14 23:09

【筆記】解決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_interpolationFacto
r = (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改變)都會覆寫插值。

參考資料

Timesteps and Achieving Smooth Motion in Unity
Unity官方手冊
FixedUpdate真的是固定的时间间隔执行吗?聊聊游戏定时器



5

24

LINE 分享

相關創作

【yotoo】240521

【孤身獨戰的無畏】佛挪輕鬆通關

【OPTCG 】紅綠「多尼多尼·喬巴」動物牌組(08環境)

留言

開啟 APP

face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】