---
title: "FixedUpdate 가 따로 도는 이유"
description: "유니티 물리는 화면 프레임과 상관없이 0.02초마다 한 번씩 계산합니다. 프레임마다 지난 시간을 모아 두었다가 0.02초씩 덜어 내며 FixedUpdate 를 부르기 때문에, 한 프레임에 0번 불리기도 하고 두 번 불리기도 합니다. 간격을 고정하는 이유와 그 때문에 생기는 입력 씹힘을 숫자로 따라갑니다."
author: "유니얼"
pubDate: 2026-11-05
category: "game-dev/math"
tags: ["Unity", "기초"]
canonical: https://blog.unialgames.com/posts/math-fixed-timestep/
---
점프 입력을 `FixedUpdate` 안에서 `Input.GetKeyDown` 으로 받았더니, 고사양 PC 에서는 점프가 가끔 씹히고 저사양 노트북에서는 가끔 두 번 뜁니다.

유니티의 물리는 **화면 프레임(화면 한 장)과 따로, 정해진 간격(기본 0.02초)마다 한 번씩** 계산합니다. 1초에 그리는 프레임 수가 fps 라, 60fps 면 1초에 60장입니다.
`Update` 는 프레임마다 한 번, `FixedUpdate` 는 물리가 계산할 때마다 한 번 불리는 함수입니다.

> **매 프레임 지난 프레임이 걸린 시간(`Time.deltaTime`)을 통에 붓고, 통에 0.02초 이상 있으면 0.02 씩 덜어 내며 FixedUpdate 를 부릅니다.
> fps 가 높으면 FixedUpdate 가 안 불리는 프레임이 생기고, 낮으면 한 프레임에 여러 번 불립니다.
> 그래서 한 프레임에만 참인 입력(`GetKeyDown`)은 Update 에서 받아 두고 FixedUpdate 에서 씁니다.**

## 물리는 왜 간격을 고정하나

물리 엔진은 시간을 조금씩 앞으로 돌리며 계산합니다. 한 번 돌리는 계산을 **물리 한 걸음**(step)이라고 하고, 걸음마다 물체를 옮기고 부딪힌 것을 찾아 밀어냅니다. 한 걸음이 맡는 시간이 **간격**입니다.

물리 엔진은 걸음마다 `속도 += 가속도 × 간격`, `위치 += 속도 × 간격` 을 계산합니다. 간격 동안 속도가 조금씩 바뀌는데, 계산은 한 가지 속도로 한 걸음을 다 간 것으로 칩니다.
이런 어림셈이라 **간격이 달라지면 결과가 달라집니다.** 처음 속도 8 로 위로 뛰고 중력이 10 이면 정확한 꼭대기는 3.2 입니다. 0.2초 간격으로 계산하면 2.4, 0.05초 간격이면 3.0 입니다. 간격이 클수록 많이 모자랍니다.

간격을 화면 프레임에 맞추면 이 간격이 기계마다 달라집니다. 그래서 떼어 놓습니다.

| 간격이 화면 프레임을 따라가면 | 간격을 고정하면 |
| --- | --- |
| 30fps 기계(간격 0.033초)와 144fps 기계(간격 0.007초)의 점프 높이가 다름 | 어느 기계든 0.02초 간격이라 같은 높이 |
| 끊긴 프레임 하나가 0.3초면 한 걸음에 크게 이동해 벽을 지나치거나 깊이 박힘 | 0.3초를 0.02초 열다섯 걸음으로 나눠 계산 |
| 같은 입력을 넣어도 프레임 시간에 따라 결과가 조금씩 다름 | 걸음의 크기가 같아 결과를 되풀이하기 쉬움. 리플레이와 네트워크 동기화의 첫 조건 |

그래서 화면은 기계가 그릴 수 있는 만큼 그리고, 물리는 늘 같은 간격으로 걷게 둘을 떼어 놓습니다. 이렇게 늘 같은 값으로 정해 둔 간격을 **고정 간격**(fixed timestep)이라고 부릅니다.
이 간격이 `Time.fixedDeltaTime` 이고, Project Settings 의 Time 에서 바꿀 수 있습니다. 기본값 0.02초는 1초에 50번입니다.

## 프레임마다 몇 번 도나: 쌓아 두고 덜어 낸다

화면 프레임은 제멋대로 길고 짧은데 물리는 0.02초씩 걸어야 합니다. 그래서 유니티는 지난 시간을 통에 모아 두고, 0.02초가 찰 때마다 한 걸음씩 덜어 냅니다.
이 통을 흔히 **누산기**(accumulator, 쌓아 두는 곳)라고 부르고, Glenn Fiedler 의 「Fix Your Timestep!」 이 이 방식을 정리한 대표 글입니다. 유니티는 프레임이 올 때마다 이렇게 합니다.

```text
쌓인 시간 += 이번 프레임의 deltaTime

while (쌓인 시간 >= 0.02)
{
    FixedUpdate 와 물리 한 걸음
    쌓인 시간 -= 0.02
}

Update
화면 그리기
```

FixedUpdate 들이 먼저 돌고, 그다음에 그 프레임의 `Update` 가 한 번 돕니다. 0.02 를 덜어 내고 남은 자투리는 다음 프레임으로 넘깁니다. 그래서 길게 보면 **1초에 정확히 50번** 물리가 돕니다.
아래 그림은 30fps 와 120fps 에서 프레임마다 FixedUpdate 가 몇 번 도는지입니다. 가로가 시간이고, 한 칸이 0.01초입니다.

<x-vec>
<script type="application/json">
{
 "label": "프레임마다 FixedUpdate 가 몇 번 도나",
 "view": [
  -0.4,
  -1.1,
  7.6,
  3.1
 ],
 "dwell": 2.6,
 "cases": [
  {
   "name": "30fps",
   "steps": [
    {
     "caption": "30fps. 위 줄이 화면 프레임, 아래 줄이 물리가 도는 자리입니다. 물리는 0.02초마다 한 번이라는 약속만 있고, 언제 실제로 계산할지는 프레임이 올 때 정합니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 1 (0.0333초). 쌓인 시간은 0.0333초. 0.02 를 1번 뺄 수 있어 FixedUpdate 를 1번 부릅니다. 남은 0.0133초는 다음 프레임으로.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        3.333,
        2
       ],
       "color": "accent"
      },
      {
       "id": "c1",
       "type": "line",
       "from": [
        3.333,
        2
       ],
       "to": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 1번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 2 (0.0667초). 쌓인 시간은 0.0467초. 0.02 를 2번 뺄 수 있어 FixedUpdate 를 2번 부릅니다. 남은 0.0067초는 다음 프레임으로.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        3.333,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        6.667,
        2
       ],
       "color": "accent"
      },
      {
       "id": "c2",
       "type": "line",
       "from": [
        6.667,
        2
       ],
       "to": [
        4,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "c3",
       "type": "line",
       "from": [
        6.667,
        2
       ],
       "to": [
        6,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 2번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    }
   ]
  },
  {
   "name": "120fps",
   "steps": [
    {
     "caption": "120fps. 위 줄이 화면 프레임, 아래 줄이 물리가 도는 자리입니다. 물리는 0.02초마다 한 번이라는 약속만 있고, 언제 실제로 계산할지는 프레임이 올 때 정합니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 1 (0.0083초). 쌓인 시간은 0.0083초. 0.02 보다 작아 FixedUpdate 를 한 번도 안 부릅니다. 그대로 다음 프레임으로 넘깁니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "accent"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 0번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 2 (0.0167초). 쌓인 시간은 0.0167초. 0.02 보다 작아 FixedUpdate 를 한 번도 안 부릅니다. 그대로 다음 프레임으로 넘깁니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        1.667,
        2
       ],
       "color": "accent"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 0번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 3 (0.0250초). 쌓인 시간은 0.0250초. 0.02 를 1번 뺄 수 있어 FixedUpdate 를 1번 부릅니다. 남은 0.0050초는 다음 프레임으로.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        1.667,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f3",
       "type": "point",
       "at": [
        2.5,
        2
       ],
       "color": "accent"
      },
      {
       "id": "c1",
       "type": "line",
       "from": [
        2.5,
        2
       ],
       "to": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 1번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 4 (0.0333초). 쌓인 시간은 0.0133초. 0.02 보다 작아 FixedUpdate 를 한 번도 안 부릅니다. 그대로 다음 프레임으로 넘깁니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "muted"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        1.667,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f3",
       "type": "point",
       "at": [
        2.5,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f4",
       "type": "point",
       "at": [
        3.333,
        2
       ],
       "color": "accent"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 0번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 5 (0.0417초). 쌓인 시간은 0.0217초. 0.02 를 1번 뺄 수 있어 FixedUpdate 를 1번 부릅니다. 남은 0.0017초는 다음 프레임으로.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        1.667,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f3",
       "type": "point",
       "at": [
        2.5,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f4",
       "type": "point",
       "at": [
        3.333,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f5",
       "type": "point",
       "at": [
        4.167,
        2
       ],
       "color": "accent"
      },
      {
       "id": "c2",
       "type": "line",
       "from": [
        4.167,
        2
       ],
       "to": [
        4,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 1번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 6 (0.0500초). 쌓인 시간은 0.0100초. 0.02 보다 작아 FixedUpdate 를 한 번도 안 부릅니다. 그대로 다음 프레임으로 넘깁니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        1.667,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f3",
       "type": "point",
       "at": [
        2.5,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f4",
       "type": "point",
       "at": [
        3.333,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f5",
       "type": "point",
       "at": [
        4.167,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f6",
       "type": "point",
       "at": [
        5.0,
        2
       ],
       "color": "accent"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 0번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    },
    {
     "caption": "프레임 7 (0.0583초). 쌓인 시간은 0.0183초. 0.02 보다 작아 FixedUpdate 를 한 번도 안 부릅니다. 그대로 다음 프레임으로 넘깁니다.",
     "items": [
      {
       "id": "tl",
       "type": "line",
       "from": [
        0,
        2
       ],
       "to": [
        7.2,
        2
       ],
       "color": "muted",
       "label": "화면 프레임",
       "labelAt": [
        1.4,
        2.55
       ]
      },
      {
       "id": "pl",
       "type": "line",
       "from": [
        0,
        0
       ],
       "to": [
        7.2,
        0
       ],
       "color": "muted",
       "label": "FixedUpdate (0.02초마다)",
       "labelAt": [
        3.0,
        0.45
       ]
      },
      {
       "id": "t1",
       "type": "point",
       "at": [
        2,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t2",
       "type": "point",
       "at": [
        4,
        0
       ],
       "color": "accent2"
      },
      {
       "id": "t3",
       "type": "point",
       "at": [
        6,
        0
       ],
       "color": "muted"
      },
      {
       "id": "f1",
       "type": "point",
       "at": [
        0.833,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f2",
       "type": "point",
       "at": [
        1.667,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f3",
       "type": "point",
       "at": [
        2.5,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f4",
       "type": "point",
       "at": [
        3.333,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f5",
       "type": "point",
       "at": [
        4.167,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f6",
       "type": "point",
       "at": [
        5.0,
        2
       ],
       "color": "muted"
      },
      {
       "id": "f7",
       "type": "point",
       "at": [
        5.833,
        2
       ],
       "color": "accent"
      },
      {
       "id": "acc",
       "type": "text",
       "at": [
        5.6,
        1.0
       ],
       "color": "warn",
       "label": "이번 프레임 FixedUpdate 0번"
      },
      {
       "id": "tl1",
       "type": "text",
       "at": [
        2,
        -0.55
       ],
       "color": "muted",
       "label": "0.02초"
      },
      {
       "id": "tl2",
       "type": "text",
       "at": [
        4,
        -0.55
       ],
       "color": "muted",
       "label": "0.04초"
      },
      {
       "id": "tl3",
       "type": "text",
       "at": [
        6,
        -0.55
       ],
       "color": "muted",
       "label": "0.06초"
      },
      {
       "id": "t0",
       "type": "text",
       "at": [
        0,
        -0.55
       ],
       "color": "muted",
       "label": "0"
      }
     ]
    }
   ]
  }
 ],
 "ticks": false
}
</script>
</x-vec>

| fps | 한 프레임 | 프레임마다 FixedUpdate |
| --- | --- | --- |
| 30 | 0.033초 | 1번, 2번, 2번, 1번, 2번, 2번 … 평균 1.67번 |
| 50 | 0.02초 | 늘 1번 |
| 120 | 0.0083초 | 0번, 0번, 1번, 0번, 1번 … 평균 0.42번 |

어느 쪽이든 1초 동안 물리가 도는 횟수는 50번으로 같습니다. 달라지는 것은 **그 50번이 프레임 사이에 어떻게 흩어지느냐**입니다.

`FixedUpdate` 안에서 `Time.deltaTime` 을 읽으면 자동으로 `Time.fixedDeltaTime`, 곧 0.02 가 나옵니다. 그래서 같은 이동 코드를 어느 쪽에 두든 `Time.deltaTime` 을 곱하는 규칙은 같습니다.

## 그래서 입력이 씹히거나 두 번 들어간다

`Input.GetKeyDown` 은 **키를 누른 그 프레임 한 번만** 참입니다. 다음 프레임이면 다시 거짓입니다.

| 상황 | 누른 프레임에 FixedUpdate 가 | 결과 |
| --- | --- | --- |
| 120fps | 0번 불림 | FixedUpdate 는 참인 순간을 못 봄. **점프가 씹힘** |
| 50fps | 1번 불림 | 정상 |
| 30fps | 2번 불림 | 그 프레임 동안 GetKeyDown 이 계속 참. **두 번 뜀** |

그래서 입력은 `Update` 에서 받아 표시만 해 두고, 물리를 건드리는 일은 `FixedUpdate` 에서 그 표시를 보고 한 번만 합니다.

```csharp
public float jumpPower = 5f;
Rigidbody rb;
bool jumpPressed;   // Update 가 받아 두고 FixedUpdate 가 쓰는 표시

void Start()
{
    rb = GetComponent<Rigidbody>();
}

void Update()
{
    // Update 는 프레임마다 꼭 한 번 불리니 누른 순간을 놓치지 않는다
    if (Input.GetKeyDown(KeyCode.Space))
    {
        jumpPressed = true;
    }
}

void FixedUpdate()
{
    if (jumpPressed)
    {
        // Impulse 는 한 번에 속도를 바꾸는 순간적인 힘
        rb.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);
        jumpPressed = false;   // 한 번 쓰고 지운다. 두 번 불려도 두 번 뛰지 않는다
    }
}
```

누르고 있는 동안 계속 참인 값(`Input.GetKey`, `GetAxis`)은 어느 쪽에서 읽어도 괜찮습니다.
반대로 `Rigidbody` 에 힘을 주거나 속도를 바꾸는 코드는 `FixedUpdate` 에 둡니다. `Update` 에서 매 프레임 힘을 주면 물리 한 걸음 사이에 힘이 몇 번 쌓이느냐가 fps 를 따라갑니다.

## 무거운 프레임이 이어지면

한 프레임이 0.3초 걸리면 쌓인 시간이 0.3 이라 FixedUpdate 를 15번 불러야 합니다.
그 15번이 또 시간을 먹어 다음 프레임이 더 길어지면 다음엔 더 많이 불러야 합니다. 쌓인 시간이 점점 불어나 게임이 멈추듯 느려집니다. 이 악순환을 흔히 **죽음의 소용돌이**(spiral of death)라고 부릅니다.

이를 막으려고 유니티는 한 프레임에 흐를 수 있는 게임 시간에 상한을 둡니다. Project Settings 의 Time 에 있는 Maximum Allowed Timestep, 코드로는 `Time.maximumDeltaTime` 이고 기본값은 약 0.333초(1/3초)입니다.
한 프레임이 1초 걸려도 게임 시간은 0.333초만 흐른 것으로 칩니다. 그래서 FixedUpdate 도 많아야 0.333 ÷ 0.02 ≈ 16번까지만 불립니다.
넘는 시간은 버려지니, 그런 순간에는 게임 시간이 실제 시간보다 느리게 갑니다.

## 간격을 바꾸면

| `fixedDeltaTime` | 1초에 | 얻는 것 | 치르는 것 |
| --- | --- | --- | --- |
| 0.01 | 100번 | 정확해지고, 빠른 물체가 덜 뚫고 지나감 | 물리 계산이 두 배 |
| 0.02 (기본) | 50번 | | |
| 0.0333 | 30번 | 물리 계산이 줄어 모바일에 유리 | 오차가 커지고 빠른 물체가 잘 뚫음 |

간격을 화면 fps 와 맞출 필요는 없습니다. 둘은 일부러 떼어 놓은 것입니다.
다만 물리 간격이 화면 프레임보다 길면 물리로 움직이는 물체가 떨려 보입니다. 120fps 화면에서는 프레임 두세 장마다 한 번만 물리가 돌아서, 몇 장 동안 같은 자리에 있다가 한 번에 옮겨지기 때문입니다.
`Rigidbody` 의 Interpolate 설정을 켜면 두 물리 걸음 사이의 자리를 화면 프레임마다 채워 그려 이 떨림을 메웁니다.

## 정리

- 물리는 화면 프레임과 따로 `Time.fixedDeltaTime`(기본 0.02초)마다 한 번씩 계산합니다
- 간격을 고정하는 이유는 물리 계산이 간격에 따라 결과가 달라지는 어림셈이라서입니다. 고정하면 기계가 달라도 같은 결과를 내고, 긴 프레임에도 한 걸음이 커지지 않습니다
- 프레임마다 deltaTime 을 쌓고 0.02 씩 덜어 내며 FixedUpdate 를 부릅니다. 자투리는 다음 프레임으로 넘어갑니다
- fps 가 높으면 FixedUpdate 가 0번인 프레임이, 낮으면 여러 번인 프레임이 생깁니다. 1초에 50번인 것은 같습니다
- 한 프레임에만 참인 입력은 Update 에서 받아 두고 FixedUpdate 에서 한 번 쓰고 지웁니다
- Rigidbody 에 힘을 주는 코드는 FixedUpdate 에 둡니다
- 한 프레임에 흐를 수 있는 게임 시간은 `Time.maximumDeltaTime`(기본 약 0.333초)까지라, 무거운 프레임이 이어져도 FixedUpdate 가 끝없이 불어나지 않습니다