리지드바디가 떨려 보이는 이유
Esc

    리지드바디가 떨려 보이는 이유

    –Unity기초

    Rigidbody(물리로 움직이게 하는 컴포넌트)로 움직이는 캐릭터를 카메라로 따라가 보면, 일정한 속도인데도 화면에서 미세하게 덜덜 떨립니다. Time.deltaTime(화면 한 장, 곧 한 프레임을 그리는 데 걸린 시간)을 곱해 봐도, 카메라 코드를 바꿔 봐도 그대로입니다.

    물리와 화면이 다른 박자로 돌기 때문입니다. 물리는 1초에 50번(0.02초마다)만 위치를 바꾸고, 화면은 그보다 훨씬 자주 그립니다. 물리가 안 돈 프레임에는 화면에 그려지는 위치(Transform 에 담긴 값)가 그대로라서, 몇 장 동안 제자리에 있다가 한 번에 뛰는 것을 반복합니다. 그게 떨림입니다.

    Rigidbody.interpolation 을 Interpolate 로 켜면, 화면을 그릴 때마다 직전 물리 위치와 최신 물리 위치 사이를 지난 시간의 비율만큼 메워서 Transform 에 넣습니다. 물리 계산은 그대로인데 떨림이 사라집니다. 대신 화면은 늘 물리 한 걸음(0.02초) 전의 자리를 보여 줍니다.

    물리 한 걸음 사이에 화면은 여러 장을 그린다

    유니티에서 중력 · 속도 · 충돌을 계산해 물체를 옮겨 주는 부분을 물리 엔진이라고 부릅니다. “속도 5 로 가라” 고만 하면 위치는 물리 엔진이 정합니다.

    물리는 게임 시간 0.02초마다 한 걸음씩 계산합니다. 1초에 50걸음입니다. 이 한 걸음의 길이가 Time.fixedDeltaTime 이고, 걸음마다 FixedUpdate 함수가 불립니다. fps 는 화면이 1초에 그리는 장 수입니다. 화면이 120fps 면 한 장이 1 ÷ 120 = 0.0083초라, 물리 한 걸음 사이에 화면은 0.02 ÷ 0.0083 = 2.4장을 그립니다.

    속도 5(1초에 5m)로 가는 물체라면 물리 한 걸음에 5 × 0.02 = 0.1 씩 갑니다.

    시각 (초) 물리가 돈 횟수 물리 위치 화면에 그린 위치 (Interpolate 끔)
    0.0083 0 0 0
    0.0167 0 0 0 (그대로)
    0.0250 1 0.1 0.1 (한 번에 뜀)
    0.0333 1 0.1 0.1 (그대로)
    0.0417 2 0.2 0.2 (한 번에 뜀)

    화면의 위치가 시간에 고르게 늘지 않고 계단처럼 늡니다. 눈은 “멈췄다, 뛰었다” 를 아주 잘 알아챕니다. 아래 그림의 위 줄이 물리가 정한 위치, 아래 줄이 화면에 그린 위치이고, 경고색 눈금이 바로 앞 장과 같은 자리를 다시 그린 프레임입니다. Interpolate 쪽은 같은 시간 동안 그 설정을 켠 모습입니다.

    deltaTime 을 곱해도 안 고쳐지는 이유

    Time.deltaTime 을 곱하는 것은 내가 Update 에서 직접 위치를 옮길 때 1초에 가는 거리를 fps 와 상관없게 만드는 방법입니다. Rigidbody 는 내가 옮기는 것이 아니라 물리 엔진이 0.02초마다 옮깁니다. 속도에 무엇을 곱하든 위치가 바뀌는 순간은 물리가 도는 순간뿐이라, 그 사이의 프레임에는 위치가 그대로입니다. 문제는 “얼마나 가나” 가 아니라 “언제 바뀌나” 입니다.

    fixedDeltaTime 을 줄여 물리를 더 자주 돌리는 방법도 있지만, 모든 물체의 물리 계산이 그만큼 늘고(0.01 이면 두 배), 화면 fps 와 딱 맞지 않는 한 계단은 남습니다. 물리는 그대로 두고 화면이 물리 사이를 메워 그리는 것이 보간입니다. 보간은 알고 있는 두 값 사이를 비율로 채워 넣는 일로, 0 과 10 사이의 30% 자리는 3 입니다.

    Interpolate 는 지나온 두 위치 사이를 메운다

    Interpolate 를 켜면 유니티는 화면을 그리기 전마다, 물리가 계산해 둔 직전 위치와 최신 위치 사이를 마지막 물리 걸음 뒤 지난 시간의 비율만큼 섞어 Transform 에 넣습니다.

    비율 = (지금 시각 − 마지막으로 물리가 돈 시각) ÷ 0.02
    그릴 위치 = Lerp(직전 물리 위치, 최신 물리 위치, 비율)

    Lerp(a, b, t) 는 보간을 하는 함수입니다. a + (b − a) × t 를 돌려주니 Lerp(0, 10, 0.3) = 3 입니다.

    위 표와 같은 프레임에 이 계산을 넣으면 이렇습니다. 물리는 0.02초에 0.1, 0.04초에 0.2 를 정했습니다.

    시각 마지막 물리 비율 사이 그린 위치 앞 장보다
    0.0083 아직 없음 0
    0.0167 아직 없음 0
    0.0250 0.02 0.005 ÷ 0.02 = 0.25 0 과 0.1 0.025 +0.025
    0.0333 0.02 0.0133 ÷ 0.02 = 0.67 0 과 0.1 0.067 +0.042
    0.0417 0.04 0.0017 ÷ 0.02 = 0.08 0.1 과 0.2 0.108 +0.042
    0.0500 0.04 0.01 ÷ 0.02 = 0.5 0.1 과 0.2 0.15 +0.042

    물리가 한 번이라도 돈 뒤로는 같은 자리를 두 번 그리는 일이 없고, 매 장 0.042 씩 고르게 갑니다. 120fps 에서 한 장에 가야 할 거리 5 × 0.0083 = 0.042 그대로입니다. 물리 걸음은 여전히 0.02초마다 한 번이고, 보간은 Transform 에 넣는 값만 바꿉니다. 충돌이나 속도 계산에는 아무 영향이 없습니다.

    대신 그린 위치를 보면 시각 0.05 에 0.15 입니다. 속도 5 라면 지금 있어야 할 자리는 0.25 인데, 그 0.02초 전의 자리를 그리고 있습니다. 직전과 최신 사이만 메울 수 있으니, 화면은 늘 물리 한 걸음 전을 보여 줍니다. 유니티 매뉴얼도 Interpolate 를 “물리 한 번만큼의 시간 지연이 있다” 고 설명합니다. 위 그림의 Interpolate 쪽에서 화면 줄이 물리 줄보다 0.1 뒤에서 끝나는 이유입니다. 0.02초는 사람이 느끼기 어려운 지연이라 대부분의 게임에서 문제가 되지 않습니다.

    세 가지 설정

    설정 무엇을 그리나 틀릴 수 있나 지연
    None (기본) 최신 물리 위치 그대로 없음 없음. 대신 계단처럼 떨림
    Interpolate 지나온 두 물리 위치 사이 없음 물리 한 걸음
    Extrapolate 최신 위치에서 속도로 예측한 앞 있음 없음. 오히려 앞서 감

    Extrapolate 는 “이 속도면 다음 걸음엔 저기” 라고 앞을 예측해 그려서 지연이 없습니다. 하지만 예측이라 틀립니다. 벽 바로 앞에서 벽 안쪽을 그렸다가, 다음 물리 걸음이 막혔다고 알려 주면 되돌아옵니다. 예측이 틀리는 순간이 하필 부딪히거나 방향을 바꾸는 순간이라 가장 눈에 띄는 곳에서 깨집니다. 그래서 속도가 자주 바뀌거나 다른 물체와 부딪히는 것에는 Interpolate 를, 아무와도 부딪히지 않고 일정한 속도로만 가는 것에만 Extrapolate 를 고려합니다.

    // 보통은 Inspector 의 Interpolate 칸에서 켠다
    rb.interpolation = RigidbodyInterpolation.Interpolate;

    2D 는 Rigidbody2D.interpolation 에 RigidbodyInterpolation2D.Interpolate 를 넣습니다.

    모든 Rigidbody 에 켜지는 않습니다. 보간은 매 프레임 그 물체의 Transform 을 다시 써 넣는 일이라 물체 수만큼 비용이 듭니다. 떨림은 카메라가 따라가는 물체, 곧 플레이어 캐릭터에서 가장 잘 보이니, 유니티도 그 물체에만 켜고 나머지는 None 으로 두기를 권합니다.

    위치를 읽는 곳이 둘로 나뉜다

    보간을 켜면 같은 물체의 위치가 두 군데에 다르게 들어 있습니다. 위 표의 시각 0.05 라면 이렇습니다.

    값 들어 있는 것 시각 0.05 에
    rb.position 물리가 정한 최신 위치. 보간과 상관없이 계단 0.2
    transform.position 보간이 메워 넣은, 화면에 그려지는 위치 0.15

    물리 계산(힘, 속도, 충돌 판정)에는 rb.position 이, 화면에 보이는 것을 따라가는 코드에는 transform.position 이 맞습니다.

    보간을 켠 Rigidbody 는 물리 엔진이 Transform 을 쥐고 있습니다. 코드에서 transform.position 에 직접 값을 넣으면, Physics.SyncTransforms 를 따로 부르지 않는 한 유니티가 그 값을 무시하고 보간한 값으로 덮어씁니다. 그래서 Rigidbody 를 코드로 옮길 때는 목적에 맞는 함수를 씁니다.

    하고 싶은 것 쓰는 것 보간
    코드로 정한 길을 매끈하게 (Kinematic, 곧 힘을 안 받고 코드로만 움직이는 Rigidbody) FixedUpdate 에서 rb.MovePosition 따름. 걸음 사이를 메워 그림
    한 번에 다른 곳으로 순간이동 rb.position = ... 메우지 않고 바로 그 자리

    캐릭터는 매끈한데 카메라가 떤다면

    보간을 켜도 화면 전체가 떤다면 카메라가 언제 움직이는지를 봅니다. 카메라를 FixedUpdate 에서 옮기면, 캐릭터가 아니라 카메라 자신이 1초에 50번만 움직입니다. 캐릭터는 매 장 고르게 그려지는데 카메라가 2장이나 3장마다 한 번씩 뛰니, 카메라에 비친 캐릭터와 배경이 함께 떨립니다.

    카메라가 대상을 따라가는 곳 카메라가 움직이는 박자 결과
    FixedUpdate 물리와 같은 1초에 50번 떨림
    LateUpdate 화면과 같은 매 프레임. 모든 Update 가 끝난 뒤라 그 프레임의 최종 위치를 읽음 매끈

    카메라 코드는 LateUpdate 에 둡니다. Cinemachine 을 쓴다면 Cinemachine Brain 의 Update Method 가 같은 역할입니다. 권장값은 Smart Update(따라가는 대상이 움직이는 방식에 맞춰 알아서 고름)이고, Fixed Update 로 되어 있다면 Smart Update 나 Late Update 로 바꿉니다.

    직접 확인하기

    등속으로 미는 물체의 화면상 속도를 매 프레임 찍어 보면, 보간을 켜고 끈 차이가 숫자로 보입니다.

    Rigidbody rb;
    float lastX;
    
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        lastX = transform.position.x;
    }
    
    void FixedUpdate()
    {
        // 1초에 5. Unity 6 이전 이름은 velocity
        rb.linearVelocity = new Vector3(5f, 0f, 0f);
    }
    
    void LateUpdate()
    {
        // 이번 프레임에 화면에서 움직인 거리
        float x = transform.position.x;
        float moved = x - lastX;
        lastX = x;
    
        // 움직인 거리 ÷ 이번 프레임의 시간 = 화면상 속도
        float speed = moved / Time.deltaTime;
        Debug.Log($"화면상 속도 {speed:F3}");
    }
    설정 찍히는 값
    None 0 이 여러 번 이어지다가 큰 값이 한 번. 화면 fps 가 높을수록 0 이 많음
    Interpolate 거의 매번 5 근처

    정리

    • 물리는 0.02초마다 위치를 바꾸고 화면은 그보다 자주 그려서, Rigidbody 는 몇 프레임 제자리에 있다가 한 번에 뜁니다. 그것이 떨림입니다
    • 내가 옮기는 것이 아니라 물리가 옮기는 것이라 deltaTime 을 곱해도 안 고쳐집니다. 문제는 거리가 아니라 바뀌는 순간입니다
    • Interpolate 는 직전과 최신 물리 위치 사이를 시간 비율만큼 Lerp 해 그립니다. 물리 계산은 그대로이고, 화면이 늘 한 걸음 늦습니다
    • Extrapolate 는 속도로 앞을 예측해 지연이 없지만, 부딪히거나 방향을 바꾸는 순간에 틀립니다
    • 보간은 카메라가 따라가는 물체에만 켭니다
    • 보간을 켜면 rb.position 은 물리 위치, transform.position 은 그린 위치입니다. 옮길 때는 MovePosition, 순간이동은 rb.position 입니다
    • 카메라는 LateUpdate 에서 따라갑니다. Cinemachine 이면 Brain 의 Update Method 를 확인합니다