スポンサーリンク
Unity C#・スクリプト実装

UnityのTimeクラス完全ガイド|Time.deltaTimeで滑らかな動きとフレームレート非依存処理を実装する方法

Unity C#・スクリプト実装

Unityでキャラクターを動かすスクリプトを書いていて、「PCだとサクサク動くのに、スマホだとやたら速く動く…」なんて経験はありませんか?

実はこれ、多くの人が最初にぶつかる壁なんです。原因を知らないまま何となく数値をいじって調整していると、環境が変わるたびに動きがバラバラになって、いつまでも安定しません。

この記事では、UnityのTime.deltaTimeを使って、どんな環境でも同じ速さで動く処理を作る方法をお伝えします。移動やタイマーの実装方法はもちろん、Update()とFixedUpdate()の使い分け、ポーズ中の演出コントロール、長時間プレイでも誤差が出ない工夫まで、実際につまずきやすいポイントに沿って解説していきますね。


deltaTimeで移動とタイマーを実装する方法

結論からお伝えすると、動きを環境に左右されないようにするには「1秒あたりの速さ」にTime.deltaTimeを掛け算するだけでOKです。

Time.deltaTimeは、直前のフレームから今のフレームまでにかかった時間を秒単位で教えてくれる値です。フレームレートが高い環境ではこの値が小さくなり、低い環境では大きくなるので、速度に掛け合わせることで「1秒間に進む距離」がどの環境でも一定になる、という仕組みですね。

難しく考えなくても、中学校で習った「距離=速さ×時間」の公式そのままだと思ってもらえれば大丈夫です。

移動処理を実装する

キャラクターやオブジェクトを一定の速さで動かしたいときは、次の手順で実装します。

  1. Update()メソッドの中に処理を書く
  2. 1秒あたりの移動速度を変数として用意する
  3. Input.GetAxisRawなどでキーボードやコントローラーの入力を取得する
  4. 速度・入力方向・Time.deltaTimeを掛け合わせた値を、現在の位置(transform.position)に足し算する
void Update()
{
    float moveSpeed = 5f;
    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");
    Vector3 dir = new Vector3(h, 0, v).normalized;
    transform.position += dir * moveSpeed * Time.deltaTime;
}

ここで一つだけ注意したいのが、方向ベクトルを求めるときの.normalizedです。これを付けずに斜め移動をすると、縦横に動くときより斜めに動くときのほうが速くなってしまいます。地味ですが、体感の違いが結構大きいポイントなので忘れないようにしてくださいね。

タイマー・カウントダウンを実装する

制限時間や経過時間を測りたいときも、考え方は同じです。

  1. 経過時間(または残り時間)を保持する変数を用意する
  2. Update()の中で、その変数に毎フレームTime.deltaTimeを足す(カウントダウンなら引く)
  3. 目標の値に達したら(または0以下になったら)、必要な処理を実行してリセットする
float timer = 0f;
float limit = 5f;

void Update()
{
    timer += Time.deltaTime;
    if (timer >= limit)
    {
        // 敵をスポーンさせるなど、任意の処理
        timer = 0f;
    }
}

この書き方をベースにしておけば、敵の出現間隔やゲームオーバーまでの制限時間など、時間に関わる処理はほとんど応用できます。まずはこの基本形を手元で動かしてみると、感覚がつかみやすいと思いますよ。




UpdateとFixedUpdateどちらを使う?

移動処理を書いていると、Update()FixedUpdate()のどちらに書けばいいのか迷うことがありますよね。ここは判断基準がシンプルなので、覚えてしまえば迷わなくなります。

結論としては「入力の検知はUpdate、物理的な移動処理はFixedUpdate」と分けて考えるのがおすすめです。

入力検知と物理移動を分けて考える

キーを押した瞬間を検知するInput.GetKeyDownのような処理は、必ずUpdate()の中で行ってください。FixedUpdate()は物理演算のタイミングに合わせて呼ばれるため、フレームによっては入力の瞬間を取りこぼしてしまうことがあるからです。

一方で、Rigidbodyに力を加えるような物理的な移動処理はFixedUpdate()の中に書きます。手順にすると、こんな流れになります。

  1. キーの入力状態をUpdate()内で検知し、変数に保存しておく
  2. 保存しておいた入力状態をもとに、FixedUpdate()内で実際の移動処理(力の加算など)を行う
bool jumpPressed = false;

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        jumpPressed = true;
    }
}

void FixedUpdate()
{
    if (jumpPressed)
    {
        rb.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);
        jumpPressed = false;
    }
}

FixedUpdate内でdeltaTimeを使うとどうなる?

実はFixedUpdate()の中でTime.deltaTimeを使っても、特別な書き方をする必要はありません。FixedUpdate()内で呼び出すと、自動的に固定間隔であるTime.fixedDeltaTimeの値が返ってくる仕組みになっているからです。

つまり、移動処理を関数として使い回したい場合でも、そのままTime.deltaTimeと書いておけば、呼び出される場所に応じて正しい値を返してくれるということですね。

ちなみに、この固定間隔(Fixed Timestep)の数値は「Edit」→「Project Settings」→「Time」から変更できます。ただ、数値を小さくすると精度は上がる分、処理の負荷も増えるトレードオフがあるので、既定値のまま様子を見て、必要になったときだけ調整するくらいで十分だと思います。

UpdateとFixedUpdateの役割の違いをもう少し詳しく知りたい方は、こちらの記事も参考にしてみてください。




ポーズ中もUIだけ動かす方法

ゲームにポーズ機能をつけたとき、「メニューは動くのに、演出用のアニメーションまで止まってしまった」という経験はありませんか。これはTime.timeScaleの仕組みを知っておくと、迷わず対処できるようになります。

timeScaleで止まるもの・止まらないものを見分ける

ポーズ処理の基本は、Time.timeScale = 0を実行することです。こうすると、Time.deltaTimeの値が0になるため、Time.deltaTimeを使っているすべての処理が実質的にストップします。

ここで判断のポイントになるのが、次の分かれ目です。

  • Time.deltaTimeに依存している処理 → ポーズ中は止まる
  • Time.unscaledDeltaTimeに依存している処理 → ポーズ中も動き続ける

ちなみにTime.timeScaleには0.1のような小さい値を入れることもできて、この場合はスローモーション演出として使えます。用途に応じて数値を調整してみてください。

UIやアニメーションだけ動かし続ける方法

ポーズ画面のカウントダウンやボタンの点滅演出など、ポーズ中でも動かしたい処理がある場合は、次のように対応します。

  1. スクリプトで時間を管理している処理は、Time.deltaTimeの代わりにTime.unscaledDeltaTimeを使うように書き換える
  2. Animatorを使っている場合は、コンポーネントの「Update Mode」を「Unscaled Time」に変更する
  3. DOTweenを使っている場合は、アニメーションの処理に.SetUpdate(true)を追加する(バージョンによって挙動が異なることがあるので、手元の環境で一度動作を確認しておくと安心です)
float uiTimer = 0f;

void Update()
{
    uiTimer += Time.unscaledDeltaTime;
}

この書き換えをしておくだけで、「ポーズ中はゲーム内の動きだけ止めて、UI演出は動かし続ける」という自然な挙動が作れます。逆に言えば、この使い分けを知らないまま実装すると、ポーズ画面自体がカクついたり反応しなくなったりするので、意識しておく価値は十分にあると思います。

ポーズ機能をもっと本格的に作り込みたい方は、こちらの記事も参考になります。

また、「timeScaleを0にしたのに動きが止まらない」「逆に戻したのに動かない」といったトラブルが起きた場合は、こちらの記事で原因を確認してみてください。




長時間・高速移動で起きる誤差と対策

ここまでの内容で、日常的な移動やタイマー処理はほぼカバーできています。ただ、次のようなケースに当てはまる場合は、もう少し踏み込んだ対策を知っておくと安心です。

  • ゲームを24時間以上、連続で起動させ続ける可能性がある
  • レール移動のギミックなど、ミリ単位でズレてほしくない処理がある
  • 処理が重くなるタイミングで、キャラクターが壁をすり抜けるような現象が起きたことがある

どれにも当てはまらない場合は、ここから先は読み飛ばしてもらって大丈夫です。

float型の精度限界とtimeAsDoubleへの切り替え

Time.timeはゲーム開始からの経過時間を返してくれますが、実はfloat型で扱われているため、長時間動かし続けると小数点以下の精度が限界に達してしまいます。結果として、時間の計算に細かいズレが出てくることがあるんです。

これを避けたい場合は、Time.timeの代わりにdouble型で扱えるTime.timeAsDoubleを使うのがおすすめです。特に、経過時間の差分計算や、長時間の累積加算をするような処理では、こちらに切り替えておくと安心感が違います。

double startTime;

void Start()
{
    startTime = Time.timeAsDouble;
}

void Update()
{
    double elapsed = Time.timeAsDouble - startTime;
}

積分公式でズレを防ぐ

毎フレームTime.deltaTimeを足し続ける移動処理は、実はごくわずかな計算誤差が少しずつ積み重なっていく性質があります。普段はほとんど気づかないレベルですが、長時間のレール移動のように高い精度が求められる処理では、これが無視できないズレになることもあります。

そういった場面では、毎フレーム位置を足し算していくのではなく、経過時間と初期位置から現在位置を直接計算する方法が有効です。

  • 等速直線運動:現在位置 = 初期位置 + 速度 × 経過時間
  • 等加速度運動:現在位置 = 初期位置 + 初速度 × 経過時間 + 1/2 × 加速度 × 経過時間の2乗

この計算方法なら、誤差が積み重なる心配がなく、いつの時点でも正確な位置を割り出せます。

処理落ち時のすり抜けを防ぐmaximumDeltaTime

スマホがスリープから復帰した直後や、一時的に処理が重くなったタイミングでは、1フレームにかかる時間が急に長くなることがあります。このときTime.deltaTimeの値も一気に大きくなるため、対策をしていないと、オブジェクトが壁を一瞬ですり抜けてしまうような現象が起きることがあります。

これを防ぐために、UnityにはTime.maximumDeltaTimeという上限値が用意されていて、初期設定では約0.333秒に制限されています。この仕組みのおかげで、極端に大きなdeltaTimeが移動処理に使われることは基本的にありません。

ただし、実際の経過時間であるTime.unscaledTimeはそのまま進む一方で、ゲーム内時間であるTime.timeやそこから計算される移動量は、上限値の分しか進まないという違いがあります。この差を意識しておくと、「復帰直後だけ動きが少しずれる気がする」といった違和感にも納得がいくと思います。

すり抜け自体をもっと詳しく知りたい方は、こちらの記事もあわせてチェックしてみてください。

ちなみに、ここで紹介したTime.deltaTimeやFixedUpdateの仕組みは、2020.2以降のバージョンでかなり安定して動くように改善されています。以前はディスプレイの表示タイミングとの微妙なズレによって、動きに細かいジッターが出ることがあったのですが、現在のバージョンを使っているなら、この点はあまり気にしなくて大丈夫です。

こうしたUnityのAPIの仕組みを、C#の基礎も含めてもう一度しっかり体系立てて学び直したいという方には、書籍で整理して理解を深めるのもひとつの方法だと思います。

Unityの教科書 Unity 6完全対応版
✅ Amazonでチェックする✅ 楽天でチェックする




まとめ

Time.deltaTimeの考え方さえ押さえてしまえば、環境によって動きがバラバラになる悩みはほとんど解消できます。まずは移動やタイマー処理に組み込んでみて、そこからUpdateとFixedUpdateの使い分け、ポーズ中の演出制御と、必要になったタイミングで少しずつ知識を足していくくらいで十分です。

もし今のプロジェクトで長時間稼働やシビアな精度が求められる場面が出てきたら、そのときはtimeAsDoubleや積分計算による対策を思い出してもらえたらと思います。


よくある質問(FAQ)

Q
Time.deltaTimeとCoroutineの待機処理、どちらを使うべき?
A

移動やフェードのように、毎フレーム少しずつ変化させたい処理にはTime.deltaTimeが向いています。一方で、「3秒ごとに敵をスポーンさせる」のように、ある程度まとまった間隔でポツポツと処理を実行したい場合は、Coroutineを使ったほうがシンプルに書けることが多いです。

両方とも時間に関わる処理ですが、役割が少し違うので、連続的な変化かどうかを基準に選んでみてください。

Q
モバイル端末でTime.deltaTimeの値は不安定になりやすい?
A

スマホやタブレットは、処理の負荷やバックグラウンドからの復帰タイミングによって、フレーム間隔が急に伸びることがあります。その結果、Time.deltaTimeの値が一時的に大きくブレることは実際にあります。

対策としては、記事内で紹介したTime.maximumDeltaTimeで極端な値を抑えることに加えて、Application.targetFrameRateであらかじめ目標のフレームレートを設定しておくと、動きの安定感が増しやすくなります。気になる場合は、この2つをセットで見直してみるとよいと思います。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク