Unity入門:ゲームループと「フレーム」を正しく理解する

本記事のゴール

初学者が フレーム(frame) と 時間(deltaTime / fixedDeltaTime) を正しく理解し、フレームレートに依存しない動きと 物理挙動の安定化 ができるようになること。


1. そもそも「フレーム」とは?

  • ゲームは「1枚の画」を 連続して更新・描画 することで動いて見えます。
  • この「1回の更新+描画」の単位が フレーム、1秒間に何フレーム描けたかが FPS(Frames Per Second)
    • 60 FPS … 1秒に60回更新(約16.67msごと)
    • 30 FPS … 1秒に30回更新(約33.33msごと)

ポイント:PC性能や負荷でFPSは変動します。フレーム数に依存した計算(例:毎フレーム5px動かす)をすると、PCごとに速度が変わってしまいます。


2. Unityの基本ループと主要メソッド

Unity(Unity 6含む)では、代表的に次の順序で処理が進みます(概念図):

  1. FixedUpdate(必要回数)… 物理(Rigidbody/Collider)用の固定時間ステップ
  2. Update … 毎フレーム1回
  3. LateUpdate … Updateの後処理(カメラ追従など)
  4. 描画(レンダリング)

それぞれの役割:

  • Update:入力、UI、非物理の移動やロジック
  • FixedUpdate物理挙動(Rigidbody の移動、力の適用)
  • LateUpdate追従や後追い計算(キャラの位置が確定した後にカメラを寄せる、など)

3. 時間の扱い:deltaTime ファミリー

  • Time.deltaTime「直近のフレーム間の経過時間(秒)」。UpdateやLateUpdateで使う。
  • Time.fixedDeltaTime「物理ステップの固定時間(秒)」。FixedUpdateで使う。既定値は 0.02(= 50Hz)。
  • Time.unscaledDeltaTimeTime.timeScale の影響を受けない経過時間。ポーズ中のUIアニメ等で使う。
  • Time.timeScaleゲーム全体の時間の速さ。0でポーズ、0.5で半分の速度。物理は基本的にこれに追従。

重要:Update では Time.deltaTime、FixedUpdate では Time.fixedDeltaTime を使うのが原則。


4. フレームレート非依存の移動(Update版)

悪い例(PCによって速度が変わる):

// 毎フレーム 5 ユニット動く → FPSが高いほど速くなる
transform.Translate(Vector3.forward * 5f);

良い例(時間で正規化):

public float speed = 5f;

void Update()
{
    // 1秒あたり speed ユニット移動(FPSに依存しない)
    transform.Translate(Vector3.forward * speed * Time.deltaTime);
}

5. 物理は FixedUpdate+Rigidbody で

物理演算は 一定間隔(fixedDeltaTime)で積分されるため、Rigidbody の移動は FixedUpdate で行います。

3D(Rigidbody)の例

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class PlayerMover : MonoBehaviour
{
    public float speed = 3f;
    Rigidbody _rb;
    Vector3 _input;

    void Awake() => _rb = GetComponent<Rigidbody>();

    void Update()
    {
        // 入力はUpdateで取得(毎フレーム変わるため)
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        _input = new Vector3(h, 0, v).normalized;
    }

    void FixedUpdate()
    {
        // 実際の移動はFixedUpdateで、時間は fixedDeltaTime を使う
        Vector3 delta = _input * speed * Time.fixedDeltaTime;
        _rb.MovePosition(_rb.position + delta);
    }
}

2D(Rigidbody2D)の例

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerMover2D : MonoBehaviour
{
    public float speed = 3f;
    Rigidbody2D _rb;
    Vector2 _input;

    void Awake() => _rb = GetComponent<Rigidbody2D>();

    void Update()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        _input = new Vector2(h, v).normalized;
    }

    void FixedUpdate()
    {
        Vector2 delta = _input * speed * Time.fixedDeltaTime;
        _rb.MovePosition(_rb.position + delta);
    }
}

よくあるミス:Rigidbody を transform.position += … で動かす(補間が乱れ、貫通やガタつきの原因)。MovePosition/MoveRotation を使いましょう。


6. カメラは LateUpdate で「後追い」

キャラクターの移動が終わった  にカメラを寄せるとブレが減ります。

public class FollowCamera : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0, 5, -7);
    public float damping = 6f;

    void LateUpdate()
    {
        if (!target) return;
        // 指数補間(フレームレート非依存のなめらか補間)
        float t = 1f - Mathf.Exp(-damping * Time.deltaTime);
        Vector3 desired = target.position + offset;
        transform.position = Vector3.Lerp(transform.position, desired, t);
        transform.LookAt(target);
    }
}

7. アニメーションとフレーム

Animator の Update Mode は3種類:

  • Normal:timeScale の影響を受ける(通常)
  • Animate Physics:物理ステップに同期(Rigidbody連動のキャラ向け)
  • Unscaled Time:timeScale の影響を受けない(ポーズ中にUIだけ動かしたい等)
// 例:物理と同期させたい
animator.updateMode = AnimatorUpdateMode.AnimatePhysics;

8. コルーチンと時間

  • yield return null; … 次の フレーム まで待つ(スケール影響なし)
  • yield return new WaitForSeconds(t); … スケール影響あり(timeScale=0 だと止まる)
  • yield return new WaitForSecondsRealtime(t); … スケール影響なし(ポーズ中も動く)
IEnumerator Blink(TextMeshProUGUI label)
{
    while (true)
    {
        label.enabled = !label.enabled;
        yield return new WaitForSecondsRealtime(0.5f); // ポーズ中も点滅
    }
}

9. VSync とフレームレート制御

  • VSync(垂直同期):画面リフレッシュと描画を同期。画面のティアリングを防止。Quality 設定の VSync Count で制御。
  • Application.targetFrameRate:任意の上限FPSを指定(VSyncが有効だと VSyncが優先 されることに注意)。
void Start()
{
    QualitySettings.vSyncCount = 0;          // VSync無効(検証用)
    Application.targetFrameRate = 60;        // 60FPSを狙う
}

実機(Android/iOS)やエディタでは挙動が異なる場合あり。最終ターゲット環境で検証 すること。


10. FPSを測るシンプルな表示

開発中の計測用(OnGUIはデバッグ限定でOK):

using UnityEngine;

public class FpsMeter : MonoBehaviour
{
    float _accum, _time;
    int _frames;
    float _fps;

    void Update()
    {
        _frames++;
        _accum += Time.unscaledDeltaTime; // ポーズの影響を受けない
        if (_accum >= 0.5f)               // 0.5秒ごとに更新
        {
            _fps = _frames / _accum;
            _frames = 0;
            _accum = 0f;
        }
    }

    void OnGUI()
    {
        GUI.Label(new Rect(10, 10, 150, 30), $"{_fps:F1} FPS");
    }
}

11. 典型的な落とし穴と対策

  1. UpdateでRigidbodyを位置移動 → FixedUpdate+Move系 に切り替える
  2. deltaTimeを掛け忘れ → 速度×時間 の形に統一
  3. FixedUpdateで Time.deltaTime を使う → Time.fixedDeltaTime に修正
  4. ポーズ中の演出が止まらない/止まってしまう
    • 止めたい:WaitForSeconds(スケールに従う)
    • 止めたくない:unscaledDeltaTime / WaitForSecondsRealtime
  5. 急な負荷増でワープ(大きすぎるdeltaTime)
    • Time.maximumDeltaTime を小さめに設定して一度に進む最大時間を制限する方法もある(高度な対策)。

12. 実験して理解を深める(ミニ課題)

  1. VSyncのON/OFF と targetFrameRate を切り替えて FPSの変化を観察
  2. Updateで移動 と FixedUpdateでRigidbody移動 を比べ、段差での貫通/ガタつきを検証
  3. timeScale=0 にして、WaitForSeconds と WaitForSecondsRealtime の違いを体感
  4. カメラ追従を Update と LateUpdate で切り替え、ブレの違いを比較

13. まとめ(チェックリスト)

  • 非物理の移動は Update × Time.deltaTime
  • 物理挙動は FixedUpdate × Time.fixedDeltaTime + Rigidbody.MovePosition
  • カメラや追従は LateUpdate
  • ポーズやUIエフェクトは unscaledDeltaTime / WaitForSecondsRealtime
  • VSync と Application.targetFrameRate の関係を理解
  • まずは ターゲット端末でFPSを計測、体感と数値で確認

付録:最小サンプル(非物理・物理・カメラ)

非物理の一定速度移動(キーボード)

using UnityEngine;

public class SimpleMove : MonoBehaviour
{
    public float speed = 4f;

    void Update()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 dir = new Vector3(h, 0, v).normalized;

        transform.position += dir * speed * Time.deltaTime;
    }
}

物理での等速移動(Rigidbody)

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class PhysicsMove : MonoBehaviour
{
    public float speed = 4f;
    Rigidbody rb;
    Vector3 dir;

    void Awake() => rb = GetComponent<Rigidbody>();

    void Update()
    {
        dir = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized;
    }

    void FixedUpdate()
    {
        rb.MovePosition(rb.position + dir * speed * Time.fixedDeltaTime);
    }
}

LateUpdateでの滑らかカメラ追従

using UnityEngine;

public class SmoothFollow : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0, 5, -8);
    public float damping = 6f;

    void LateUpdate()
    {
        if (!target) return;
        float t = 1f - Mathf.Exp(-damping * Time.deltaTime);
        transform.position = Vector3.Lerp(transform.position, target.position + offset, t);
        transform.LookAt(target);
    }
}

訪問数 3 回, 今日の訪問数 3回

Unity

Posted by hidepon