【Unity】Update()とFixedUpdate()とLateUpdate()の違いと使い分け

更新日:2023/05/23

Unityでプログラミングするとき最初に目にするのはUpdate()だと思います。
しかし学習を進めるにつれてFixedUpdate()とLateUpdate()という更新メソッドが出てきます。

僕はこれらの違いに最初は戸惑いました。
そしてある程度理解できたので、これらのメソッドの違いと使い分けをお伝えします。

 

各メソッドの目的

各メソッドの使用目的と、Unityによって呼び出されるタイミングを表にしてみました。

メソッド名目的呼び出しタイミング
Update()描画の直前にオブジェクトの
位置などの状態を更新する
フレーム毎
LateUpdate()特定のオブジェクトの状態が
決定しなければ判断できない
情報を更新する
全オブジェクトのUpdate()後
FixedUpdate()物理演算に関する処理を行う一定時間毎

FixedUpdate()の呼び出し周期は、メニューEdit > project SettingsTimeで変更できます。

project Settings Time

この中の、fixed timestepが周期です。
上の図は0.02秒です。
一秒の呼び出し回数は、1 ÷ 0.02 で 50回になります。

 

各メソッドの使いわけ

通常はUpdate()で変更する

UI上の情報を更新するときや、オブジェクトの移動に物理演算を使用しないときはUpdate()に処理を記述します。

イメージとしては、次のようなコードになります。

public class UpdateTest: MonoBehaviour
{
    void Start()
    { 
          
    }

    void Update()
    {
         // 移動処理
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        
        Vector2 position = transform.position;
        position.x += 3.0f * horizontal * Time.deltaTime;
        position.y += 3.0f * vertical * Time.deltaTime;
        transform.position = position;

         // text更新
        test_text.text = "xxxxx";
    }

}

物理演算はFixedUpdate()で変更する

オブジェクトの衝突など、物理演算を伴う処理はFixedUpdate()で行います。
ゲームでキャラクターを移動するときは、地形や他のキャラクターとの接触判定が必要なので物理演算は必須ですね。

そこで入力の状態をUpdate()で取得して変数にセットします。
次に、FixedUpdate()で変数を参照して移動先を決定後に、衝突処理を行います。

イメージとしては、次のようなコードになります。

public class UpdateTest: MonoBehaviour
{
    Rigidbody2D rigidbody2d;
    float horizontal; 
    float vertical;
    
    // Start is called before the first frame update
    void Start()
    {
       rigidbody2d = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");

         // text更新
        test_text.text = "xxxxx";
    }
    
    void FixedUpdate()
    {
        Vector2 position = rigidbody2d.position;
        position.x += 3.0f * horizontal * Time.deltaTime;
        position.y += 3.0f * vertical * Time.deltaTime;

        rigidbody2d.MovePosition(position);
    }
    
}

Inputで取得できる値は、Update()の呼び出し前に更新されます。
そのため、Update()間にFixedUpdate()が複数回呼び出されたとしても、同じ値が取得されます。

少しムダなので、Update()で変数に格納しています。

ふと気が付いたのですが、FixedUpdate()が複数回呼び出されるとしたら、そのたびに物理演算するのはムダという気がしますね。
しかし他にもFixedUpdate()で位置が変わるオブジェクトがあるので、ムダではないようです。

カメラ等の処理はLateUpdate()で変更する

キャラクターの移動に合わせてカメラを移動するときなど、他のオブジェクトと連動させるときはLateUpdate()を使用します。

連動させるには全てのオブジェクトの位置が決定した後に、カメラを操作する必要があります。
しかしUpdate()は、実行する順番をUnityが決定します。
そのためカメラのUpdate()を最後に呼び出すような制御ができません。
そこで全てのオブジェクトのUpdate()が呼び出された後に、実行されるLateUpdate()を使用します。

オブジェクトをキャラクターに追尾させるときも、キャラクターの位置が決定した後にオブジェクトの位置を決定する必要がありますね。
このような時もLateUpdate()を使用します。

 

入力は(Input)はUpdate()で更新される

前項で、「Inputで取得できる値は、Update()の呼び出し前に更新されます」と書きました。
本当にそうなのか確認してみます。

public class InputTest : MonoBehaviour
{
 
    void Start () {
        QualitySettings.vSyncCount = 0; // Application.targetFrameRate設定を有効にする
        Application.targetFrameRate = 1; // フレームを1秒に1回に変更
    }

    void Update() {
        Debug.Log("Update");
    }

    void FixedUpdate() {
        Debug.Log("FixedUpdate():Vertical = " + Input.GetAxis("Vertical"));
    }
}

Start()で、1秒に一回だけUpdate()が呼び出されるように設定しています。

この状態で一瞬だけ下矢印を入力します。
すると、次のような結果になりました。

FixedUpdate():Vertical = 0
FixedUpdate():Vertical = 0
Update
FixedUpdate():Vertical = -1
FixedUpdate():Vertical = -1
・・・省略
FixedUpdate():Vertical = -1
FixedUpdate():Vertical = -1
Update
FixedUpdate():Vertical = 0
FixedUpdate():Vertical = 0

入力は1秒に満たないので、FixedUpdate()呼び出し中に入力値が変化しているはずです。
しかし、変化後の情報を取得できませんでした。
やはりFixedUpdate()はInputの情報が更新されないようですね。

 

更新タイミングを検証してみる

Update()またはFixedUpdate()の処理時間が長いと、処理落ちなどの原因になります。
どのようなロジックで処理が落ちるのか気になってマニュアルを参照したのですが、詳細な説明が見当たらないためわかりませんでした。

そこで、実際の挙動から推察してみます。

なお各メソッドでは、Time.deltaTime および Time.fixedDeltaTime というプロパティ値を使用できます。
Time.deltaTimeは前回のフレーム処理から、今回のフレーム処理までの経過時間がセットされています。
Time.fixedDeltaTime は、同様に前回の物理演算処理から今回の処理までの経過時間です。

Time.deltaTime および Time.fixedDeltaTime

これらはタイミングを検証する上で、重要な値となります。

並列処理でないことの確認

まずはUpdate()とFixedUpdate()のタイマー監視が並列処理でないことを確認してみます。

次のようなコードを用意しました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class TestObject : MonoBehaviour
{
    System.Diagnostics.Stopwatch sw,sw2;
    void Start()
    {
        QualitySettings.vSyncCount = 0; // Application.targetFrameRate設定を有効にする
        Application.targetFrameRate = 50;
        sw = new System.Diagnostics.Stopwatch();
        sw2 = new System.Diagnostics.Stopwatch();
        sw2.Start();
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log(sw2.ElapsedMilliseconds/1000f + "秒経過: <color=orange>Update:deltaTime["+Time.deltaTime+"秒]---------></color>");
        sw.Reset();
        sw.Start();
        for(int i = 0; i < 30000000f; i++){

        }
        Debug.Log(sw2.ElapsedMilliseconds/1000f + "秒経過: <color=orange>----"+sw.ElapsedMilliseconds/1000f+"秒----->Update End</color>");
        sw.Stop();
    }
    
    void FixedUpdate()
    {
        Debug.Log(sw2.ElapsedMilliseconds/1000f + "秒経過: FixedUpdate:fixedDeltaTime["+Time.fixedDeltaTime+"秒]");
    } 
}

時間計測のためのタイマーを二つ用意しています。
swは、Update()内の処理時間計測に使用します。
sw2は、初期化時からの時間経過を計測します。

今回はフレームレートを50にセットしました。
これは周期としては0.02秒です。
また、fixed timestepproject Settingsで0.02に設定してあります。

Update()は、処理時間が0.02秒以上になるようにループ処理をおこなっています。
並列処理されているなら、ループ中にFixedUpdate()が呼ばれるはずです。

実行すると、次のようになりました。

並列呼び出しではない

Update()の処理は約0.1秒です。
FixedUpdate()が0.02秒ごとに呼び出されるなら、4回か5回程度割り込むはずですが0回です。
どうやら、並列で処理されていないようですね。

実際のところ、並列だとしたら共通変数の上書きを管理しないといけないので、とても面倒になります。

Update()処理が重い時のタイミング

次は、Update()処理が重い時の呼び出しについて確認してみます。
前項のコードがそのものズバリなので、結果を流用します。

Update()間に呼び出された FixedUpdate() の fixedDeltaTime を合計してみます。

0.1秒から0.12秒になりました。
これは、Update()の処理時間と連動していることが類推できます。

もう少しFixedUpdate()を注視してみます。
fixedDeltaTimeが0.02なのでFixedUpdate()は0.02秒ごとに呼び出されているように感じます。

しかし左端の総経過時間をよく見ると、ほとんど時間が経過していないのがわかります。

時間が経過していない

このことから、フレーム処理で時間オーバーすると、つじつま合わせで物理演算が呼び出されると類推できます。
これにより、見かけ上は物理演算が固定周期で呼び出されていることになります。

FixedUpdate()処理が重い時のタイミング

次は、FixedUpdate()処理が重い時の呼び出しについて確認してみます。
今度はFixedUpdate()で、ループ処理をおこないます。


    void Update()
    {
        Debug.Log(sw2.ElapsedMilliseconds/1000f + "秒経過: Update:deltaTime["+Time.deltaTime+"秒]");
    }
    
    void FixedUpdate()
    {
        Debug.Log(sw2.ElapsedMilliseconds/1000f + "秒経過: <color=orange>FixedUpdate:fixedDeltaTime["+Time.fixedDeltaTime+"秒]---------></color>");
        sw.Reset();
        sw.Start();
        for(int i = 0; i < 30000000f; i++){

        }
        Debug.Log(sw2.ElapsedMilliseconds/1000f + "秒経過: <color=orange>----"+sw.ElapsedMilliseconds/1000f+"秒----->FixedUpdate End</color>");
        sw.Stop();
    }

実行すると、次のような結果になりました。

FixedUpdate()処理が重い時

Update()がほとんど呼び出されなくなってしまいました。
恐らく、物理演算の0.02秒周期を維持するために、この秒数を超えたら即座にFixedUpdate()を呼び出していると類推できます。

ではUpdate()はどんなタイミングで呼び出されるのでしょうか。
deltaTimeをよく見ると、0.333333になっています。

この数字は、Project SettingMaximum Allowd Timestepと同じ値です。

Maximum Allowd Timestep

Maximum Allowd Timestepは翻訳すると「最大許容タイムステップ」となります。
つまりこの秒数だけ、物理演算を連続できるという意味ですね。

この数字を見ると、最低でも1秒に3回はUpdate()が呼ばれるように感じるかもしれません。
しかしよく見ると、FixedUpdate()の処理時間は0.1秒です。
これが10回以上呼び出されています。
つまり1秒以上もUpdate()が呼ばれていないことになります。

実行結果を検証してみると、fixedDeltaTime × 実行回数 が 0.333333 を超えたときに、Update()が呼ばれています。
実際の経過時間は関係ないようですね。

そもそもfixedDeltaTimeは、実際の経過時間が多くても少なくても0.02以外の値を返していません。
固定値と思っていいようです。

両方とも重い時のタイミング

Update()とFixedUpdate()の両方が重い時は、FixedUpdate()処理が重い時のタイミングと同じです。

Maximum Allowd Timestepに達するまでFixedUpdate()が呼ばれた後に、Update()が呼ばれます。

更新日:2023/05/23

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。