Time.timeの落とし穴

これは反省文です。
これらの問題に開発終盤に気づくと、影響範囲が広くなりがちです。
知識として入れておきましょう。

Time.time

どんなチュートリアルにも最初の方に出てくる、言わずと知れた経過時間を取得する方法。
超基本的なものだと思って、気軽に使っていませんか?
今日はそんな Time.time の落とし穴について語っていきます。

落とし穴1 : Time.time の精度

Time.time はfloatです。
さて、floatが取れる範囲をパッと想像できますか?
MSDNを見ると ±3.4*(10^38) と書いてあります。
しかし、今回気にしたいのは指数部の精度です。

ここから長々と説明を書くつもりだったのですが、
元UTJの安原さんによる「浮動小数点数の限界を把握する」という非常に分かりやすい記事を発見したため、
説明は全てそちらに託します。

プレイヤーがゲームをつけっぱなしで24時間放置した場合、
(スマホでは無さそうですが、PCゲームだと付けたまま寝落ちとかありそうですよね。)
Time.time は0.0166どころか、0.016という小数点3桁の精度ですら正しく刻むことが出来ない(0.015625秒になる)、ということになります。

(0.015625 が常に加算出来るわけではありませんが簡単のため)
もし、1フレームが0.015625秒になったと仮定した場合、1秒毎に0.0625秒の差が生じてきます。
これは、正常に動いているのにも関わらず毎秒3.75フレーム分ゲームが遅くなっていくことを意味します。

実際、 Time.time を使っている分には内部ではもっと精度の良い時間を取得しているはずなので、大きな問題にはなりません。
Time.time の差分(開始時間を保持しておいて、終了時に引き算するとか)や、
Time.deltaTime を自分で足して管理している変数などは要注意です。

対策としては、

  • Time.timeAsDouble を使う
  • Time.deltaTime を加算する変数はdoubleにする

などがあげられます。

「うちのゲームは24時間も起動し続けないので問題ないよ」という方は、落とし穴2へお進みください。

落とし穴2 : Time.timeTime.unscaledTime の違い

突然ですが問題です。
Time.timeTime.unscaledTime の違いはなんでしょう?

Time.timeScale に依存しない」というのはよく知られている違いですが、
Time.maximumDeltaTime の影響を受けない」という重要な違いがあります。

「maximumDeltaTimeって、処理落ちしたときでもdeltaTimeが一定以上大きくならないようにするためのものだっけ。それがどうしたの?」
という方、こんなケースを想像してみてください。
maximumDeltaTime = 1f とします。

iPadでゲームを1時間遊んで、そのままスリープモードに入れました。
この時、処理落ちもtimeScaleの変更も特になければ Time.frameCount = 216000, Time.time = 3600f, Time.unscaledTime = 3600f です。
24時間後、続きを遊ぼうとiPadのスリープを解除しました。
もちろん、スリープしている間フレームは進まないので Time.frameCount = 216001 と次のフレームになります。
timeはmaximumDeltaTimeの値だけ進み Time.time = 3601f となり、1フレームで1秒も進んでいますが、大きな問題はないでしょう。
そう、24時間もスリープしていたのにtimeは1秒しか進まないのです。これはmaximumDeltaTimeのおかげです。

ここでunscaledTimeはというと Time.unscaledTime = 90001f になります。
落とし穴1と同じく、この時点でunscaledTimeは0.016を刻むことが出来なくなります。
24時間ゲームを起動し続けるよりも、どんなプラットフォームでも、誰でも起こり得そうなのが恐ろしいところですね。

まとめ

普段何気なく使っている Time.time ですが、
時間を使いたいときは、何の目的でどういう精度で何をしたいのか、を考え適切な方法で取得しましょう。

え、もう2022年になるんだし、全部doubleで良くないですか?