유니티 입문2026년 03월 01일· 8 min read

12. Rigidbody 질량·중력·드래그가 움직임을 바꾸는 이유(엔진 내부 포함)

Unity Rigidbody의 mass, useGravity, drag가 왜 다른 움직임을 만드는지 Player Loop, C#→네이티브 바인딩, FixedUpdate 적분 관점에서 설명한다. 실습과 성능 함정 포함. 2026 기준 실무 팁 제공.

12. Rigidbody 질량·중력·드래그가 움직임을 바꾸는 이유(엔진 내부 포함)

Rigidbody 질량·중력·드래그가 움직임을 바꾸는 이유(엔진 내부 포함)

게임 만들다 갑자기 캐릭터가 미끄러지듯 날아가거나, 같은 키 입력인데 어떤 오브젝트는 둔하게 움직이고 어떤 오브젝트는 미친 듯이 가속되는 사고가 난다. Inspector에서 mass를 10으로 바꾸고 drag를 1로 올렸더니 ‘느려진 것 같긴 한데’ 왜 그렇게 되는지 감이 없다. Unity 물리는 프레임마다 위치를 직접 옮기는 시스템이 아니라, FixedUpdate 타이밍의 적분과 제약(Constraint) 해석 결과로 Transform을 갱신한다. 이 구조를 모르면 값 조합을 맞추는 데만 시간을 쓰게 된다.

핵심 개념

Rigidbody의 mass, useGravity, drag는 ‘움직임을 만드는 입력’이 아니라 ‘물리 적분기의 계수’에 가깝다. Unity에서 힘(Force)과 가속도(Acceleration)는 FixedUpdate의 물리 스텝에서 누적되고, 그 결과로 속도와 위치가 갱신된다. 그래서 같은 AddForce 호출이라도 ForceMode에 따라 mass가 영향을 주기도 하고, 아예 무시되기도 한다.

mass(질량)는 관성의 크기다. 엔진 내부에서는 선형 운동에 대해 a = F / m이 적용된다. 힘을 일정하게 주면 질량이 큰 물체는 같은 시간 동안 속도가 덜 오른다. 반대로 ‘가속도’를 직접 주는 모드(Acceleration)는 질량과 무관하게 속도가 오른다. 실무에서 “왜 무거운 박스가 더 빨리 미끄러지지?” 같은 질문은 ForceMode를 잘못 쓴 경우가 많다.

useGravity는 Rigidbody에 중력 가속도를 매 스텝 더할지 여부다. 중요한 점은 중력은 ‘힘’처럼 보여도 Unity 내부에서는 가속도 형태로 적용된다(질량에 무관하게 같은 중력 가속도). 그래서 mass를 1에서 100으로 바꿔도 낙하 가속도는 같다. 다만 공기 저항(여기서는 drag)을 켜면 종단 속도(terminal velocity)가 달라져서 “무거우면 더 빨리 떨어진다”처럼 보이는 상황이 생긴다.

drag는 속도에 비례해 감속되는 항이다. Unity UI에는 단순 숫자 하나지만, 물리 스텝에서 속도를 감쇠시키는 계수로 쓰인다. drag를 올리면 같은 힘을 계속 줘도 속도가 어느 지점에서 더 이상 잘 안 오른다. 이 현상은 ‘힘이 약해서’가 아니라, 매 스텝 감쇠가 누적되기 때문이다.

용어를 5개만 정확히 잡고 가면 디버깅이 쉬워진다. 1) FixedUpdate: 물리 스텝(고정 시간)에서 힘/속도/충돌을 처리한다. 2) fixedDeltaTime: 물리 스텝 간격, 기본 0.02초(초당 50스텝). 3) AddForce: 힘/가속도/속도변화 중 어떤 형태로 누적할지 ForceMode로 결정한다. 4) drag: 선형 속도 감쇠 계수. 5) interpolation: 렌더링 프레임과 물리 프레임 불일치를 보간해 화면 떨림을 줄인다.

아래 코드는 mass/drag/useGravity 조합이 실제로 어떤 숫자 차이를 만드는지 ‘콘솔 출력’으로 확인하는 최소 실험이다. 예를 들어 thrust=10, fixedDeltaTime=0.02(초당 50스텝)일 때 ForceMode.Force로 1초간 누르면, drag=0이면 m=1은 이론상 Δv≈(10/1)*1=10m/s, m=10은 Δv≈1m/s에 가깝게 나온다(충돌/마찰이 없다는 가정). 같은 조건에서 drag=2를 주면 로그의 v가 10m/s까지 못 가고 3~6m/s 근처에서 증가가 둔해지는 패턴이 나타난다.

RigidbodyBasicsProbe.cs
1using UnityEngine;
2
3public class RigidbodyBasicsProbe : MonoBehaviour
4{
5    public Rigidbody rb;
6    public float thrust = 10f;
7
8    void Reset()
9    {
10        rb = GetComponent<Rigidbody>();
11    }
12
13    void FixedUpdate()
14    {
15        if (Input.GetKey(KeyCode.Space))
16            rb.AddForce(Vector3.forward * thrust, ForceMode.Force);
17
18        if (Time.frameCount % 20 == 0)
19            Debug.Log($"m={rb.mass:F1}, drag={rb.drag:F2}, g={rb.useGravity}, v={rb.linearVelocity.z:F3}");
20    }
21}

Play 모드에서 Space를 누른 채로 mass를 1→10, drag를 0→1로 바꾸면 로그의 v 증가량이 즉시 달라진다. 같은 thrust=10에서 mass=1이면 0.5초 동안 v가 대략 5m/s 근처까지 올라가고, mass=10이면 0.5초 동안 0.5m/s 근처로만 올라간다(바닥 마찰/충돌 영향이 적다는 전제). drag=1을 넣으면 초반엔 올라가도 2~4초쯤부터 증가폭이 눈에 띄게 줄어든다. 이 차이는 Update 호출 빈도와 무관하고, FixedUpdate에서의 적분 결과라서 fixedDeltaTime을 0.02→0.01로 바꾸면 동일한 drag라도 감쇠가 더 자주 적용되어 체감이 달라진다.

엔진 관점에서의 내부 동작

C#의 Rigidbody는 네이티브 물리 바디(PhysX 기반)로 가는 래퍼다. Rigidbody 컴포넌트는 C# 객체이지만, 실제 질량/속도/충돌 형상은 C++ 런타임 쪽에 존재한다. C#에서 rb.mass = 10 같은 대입을 하면, 바인딩을 통해 네이티브 바디의 속성이 갱신된다. 그래서 Inspector에서 mass를 바꾸는 행위도 결국 네이티브 상태 변경이다.

Player Loop 관점에서 물리는 Update가 아니라 FixedUpdate 타이밍에 진행된다. 내부적으로는 ‘물리 월드 스텝’이 고정 시간 단위로 여러 번 실행될 수 있다. 렌더 프레임이 느려지면 한 프레임에 물리 스텝이 2~3번 돌고, 반대로 렌더 프레임이 빠르면 물리 스텝이 0번인 프레임도 생긴다. 그래서 힘을 Update에서 주면 프레임레이트에 따라 힘이 들쭉날쭉해지고, FixedUpdate에서 주면 스텝 수에 맞춰 일관된다.

AddForce 호출은 즉시 위치를 바꾸지 않는다. 대부분의 ForceMode에서 ‘누적 버퍼’에 힘/가속도/속도변화를 기록해두고, 물리 스텝에서 이를 읽어 통합(integration)한다. 이 설계는 충돌 해결과 제약(관절, 접촉) 해석을 한 번에 풀기 위해 필요하다. 힘을 즉시 적용해 Transform을 옮기면 충돌이 뚫리거나 제약이 깨지는 부작용이 커진다.

중력은 useGravity가 켜진 바디에 대해 물리 스텝마다 g 벡터를 더한다. 중요한 포인트는 ‘중력은 질량에 비례해 힘이 커진다’는 교과서 표현을 엔진에서는 ‘가속도 형태로 적용’해 같은 낙하 가속도를 만든다는 점이다. 그래서 mass가 커도 자유낙하 가속도는 같다. 대신 drag가 있으면 감쇠가 속도에 비례하므로, 최종적으로 도달하는 종단 속도는 drag 크기에 크게 좌우된다.

drag는 물리 스텝에서 속도를 감쇠시키는 계수로 반영된다. 구현은 엔진마다 다르지만, 일반적으로 v = v * (1 - k*dt) 또는 v = v * exp(-k*dt) 형태의 감쇠가 들어간다. dt가 fixedDeltaTime이고 k가 drag에 대응한다. 그래서 drag를 2배로 올리는 것은 ‘마찰이 2배’ 같은 감각이 아니라 ‘스텝당 감쇠율이 더 커짐’이다. fixedDeltaTime을 바꾸면 같은 drag라도 체감이 달라진다.

메모리 관점에서 Rigidbody 자체는 네이티브 메모리에 큰 덩어리가 있고, C# 객체는 핸들/포인터를 들고 있는 형태다. rb.linearVelocity 같은 프로퍼티 접근은 네이티브에서 값을 복사해 C# 구조체(Vector3)로 돌려준다. 이때 Vector3는 값 타입이라 보통 GC Alloc은 없다. 다만 GetComponent<Rigidbody>()를 매 프레임 호출하면 컴포넌트 탐색 비용과 바인딩 호출 비용이 누적된다.

처음에 나도 FixedUpdate와 Update의 분리를 ‘그냥 규칙’로만 외웠다가, 이동이 미세하게 떨리는 버그로 3시간을 날렸다. Profiler에서 Physics.Simulate가 한 프레임에 두 번 찍히는 걸 보고서야, 렌더 프레임이 밀릴 때 물리 스텝이 여러 번 돈다는 걸 체감했다. Update에서 AddForce를 호출하니 한 프레임에 힘이 2번, 어떤 프레임엔 0번 들어가서 속도가 톱니처럼 바뀌고, 그게 카메라에서 떨림으로 보였다.

아래 코드는 Player Loop에서 물리 스텝이 몇 번 실행되는지 감으로가 아니라 숫자로 확인한다. FixedUpdate 카운트를 Update에서 읽어 콘솔에 찍으면, 프레임 드랍 상황에서 물리가 ‘추가로 따라잡는’ 장면이 보인다.

PhysicsStepCounter.cs
1using UnityEngine;
2
3public class PhysicsStepCounter : MonoBehaviour
4{
5    int fixedCount;
6    float acc;
7
8    void FixedUpdate()
9    {
10        fixedCount++;
11    }
12
13    void Update()
14    {
15        acc += Time.unscaledDeltaTime;
16        if (acc >= 1f)
17        {
18            Debug.Log($"1초 동안 FixedUpdate={fixedCount}, fps≈{1f / Time.unscaledDeltaTime:F1}, fixedDt={Time.fixedDeltaTime:F3}");
19            fixedCount = 0;
20            acc = 0f;
21        }
22    }
23}

Game 뷰에서 Stats를 켜고(게임 뷰 우상단 Stats 버튼), 인위적으로 부하를 주면 Update fps가 흔들리는 동안에도 FixedUpdate 카운트가 50 근처로 유지되려 한다. 이 동작이 ‘물리 일관성’을 위한 설계 의도다. 반대로 fixedDeltaTime을 0.01로 줄이면 FixedUpdate가 초당 100번이 되어 CPU가 급격히 늘어난다.

ForceMode에 따라 mass가 왜 다르게 취급되는지도 엔진 설계다. 게임플레이 입력은 “초당 몇 m/s 빨라져야 한다” 같은 의도가 많고, 폭발/충격은 “순간적으로 속도를 바꿔야 한다”가 많다. 그래서 Force(힘), Acceleration(가속도), Impulse(충격량), VelocityChange(속도변화)로 분리해 네이티브에서 서로 다른 누적 경로를 탄다. 아래 코드는 같은 키 입력을 4가지 모드로 바꿔가며 속도 변화량이 mass에 어떻게 반응하는지 보여준다.

ForceModeProbe.cs
1using UnityEngine;
2
3public class ForceModeProbe : MonoBehaviour
4{
5    public Rigidbody rb;
6    public float value = 10f;
7    public ForceMode mode = ForceMode.Force;
8
9    void Awake()
10    {
11        if (!rb) rb = GetComponent<Rigidbody>();
12    }
13
14    void FixedUpdate()
15    {
16        if (Input.GetKey(KeyCode.F)) rb.AddForce(Vector3.forward * value, mode);
17        if (Input.GetKeyDown(KeyCode.R)) rb.linearVelocity = Vector3.zero;
18
19        if (Time.frameCount % 30 == 0)
20            Debug.Log($"mode={mode}, m={rb.mass:F1}, v={rb.linearVelocity.z:F3}");
21    }
22}

Inspector에서 mode를 Force→Acceleration→Impulse→VelocityChange로 바꾸고, mass를 1과 10으로 번갈아 테스트하면 로그 패턴이 갈린다. Force/Impulse는 mass가 커질수록 같은 입력에서 v가 덜 변한다. Acceleration/VelocityChange는 mass를 바꿔도 v 변화가 거의 같다. 이게 ‘왜 무거운데도 똑같이 튄다’ 같은 현상의 정체다.

실습하기

1단계: 프로젝트 설정(물리 스텝이 흔들리지 않게)

Unity Hub → New project → 3D(Core) 템플릿을 선택한다. 버전은 2022 LTS 이상이면 된다. 프로젝트가 열리면 Edit → Project Settings → Time에서 Fixed Timestep을 0.02로 유지한다. 실험 중에 이 값을 건드리면 drag 체감이 같이 바뀌어서 원인 분리가 어려워진다.

Edit → Project Settings → Physics에서 Gravity가 (0, -9.81, 0)인지 확인한다. 여기 값을 바꾸면 useGravity의 효과가 통째로 바뀐다. 실습 동안은 기본값을 유지하고, 마지막에만 Gravity를 바꿔 비교한다.

2단계: 씬 구성(값이 눈으로 보이게 만들기)

Hierarchy 우클릭 → 3D Object → Plane로 바닥을 만든다. 이어서 Hierarchy 우클릭 → 3D Object → Cube를 3개 만든 뒤 이름을 Cube_M1, Cube_M10, Cube_Drag로 바꾼다. 각각 Position을 (0,2,0), (2,2,0), (4,2,0)로 둔다.

각 Cube를 클릭하고 Inspector → Add Component → Rigidbody를 추가한다. Cube_M1은 Mass=1, Drag=0, Use Gravity 체크. Cube_M10은 Mass=10, Drag=0, Use Gravity 체크. Cube_Drag는 Mass=1, Drag=2, Use Gravity 체크. 그리고 3개 모두 Constraints에서 Freeze Rotation X/Z를 체크한다. 회전이 섞이면 이동 관찰이 어려워진다.

3단계: 코드 작성 및 테스트(콘솔 숫자 + 화면 움직임)

Project 창에서 Assets 우클릭 → Create → C# Script로 PhysicsLabController를 만든다. 빈 GameObject를 만들고(Hierarchy 우클릭 → Create Empty) 이름을 PhysicsLab로 바꾼 뒤, PhysicsLabController를 붙인다. Inspector에서 Cube 3개 Rigidbody를 드래그해 참조로 연결한다.

Play를 누르면 3개 큐브가 동시에 앞으로 밀린다. Game 뷰에서는 Cube_M10이 가장 늦게 가속되고, Cube_Drag는 초반엔 움직이다가 속도가 빨리 죽는다. Console에는 0.5초마다 속도가 찍힌다. 숫자가 눈에 들어오면 감각적 튜닝이 아니라 ‘계수 맞추기’로 접근이 바뀐다.

PhysicsLabController.cs
1using UnityEngine;
2
3public class PhysicsLabController : MonoBehaviour
4{
5    public Rigidbody cubeM1;
6    public Rigidbody cubeM10;
7    public Rigidbody cubeDrag;
8
9    public float thrust = 30f;
10    public bool applyGravity = true;
11
12    float t;
13
14    void Start()
15    {
16        cubeM1.useGravity = applyGravity;
17        cubeM10.useGravity = applyGravity;
18        cubeDrag.useGravity = applyGravity;
19    }
20
21    void FixedUpdate()
22    {
23        var f = Vector3.forward * thrust;
24        cubeM1.AddForce(f, ForceMode.Force);
25        cubeM10.AddForce(f, ForceMode.Force);
26        cubeDrag.AddForce(f, ForceMode.Force);
27
28        t += Time.fixedDeltaTime;
29        if (t >= 0.5f)
30        {
31            Debug.Log($"M1 v={cubeM1.linearVelocity.z:F2} | M10 v={cubeM10.linearVelocity.z:F2} | Drag v={cubeDrag.linearVelocity.z:F2}");
32            t = 0f;
33        }
34    }
35}

같은 thrust인데 M10이 느린 이유는 F/m에서 m이 커졌기 때문이다. Drag가 큰 큐브가 어느 순간 정체되는 이유는 매 스텝 속도 감쇠가 누적되면서, 추진으로 얻는 속도 증가량과 감쇠로 잃는 속도 감소량이 균형을 이루기 때문이다. 이 균형점이 종단 속도처럼 보인다.

중력만 따로 확인하려면, 위 코드에서 thrust를 0으로 두고 높이를 10 정도로 올린다. Mass를 1과 10으로 바꿔도 바닥에 닿는 시간이 거의 같다. Drag를 올리면 낙하가 눈에 띄게 느려진다. ‘질량이 큰데 왜 똑같이 떨어지지?’라는 첫 의문이 여기서 해소된다.

GravityVsMassProbe.cs
1using UnityEngine;
2
3public class GravityVsMassProbe : MonoBehaviour
4{
5    public Rigidbody rb;
6    public float logEvery = 0.2f;
7    float acc;
8
9    void Awake()
10    {
11        if (!rb) rb = GetComponent<Rigidbody>();
12    }
13
14    void FixedUpdate()
15    {
16        acc += Time.fixedDeltaTime;
17        if (acc >= logEvery)
18        {
19            Debug.Log($"t={Time.time:F2} m={rb.mass:F1} drag={rb.drag:F2} y={rb.position.y:F2} vy={rb.linearVelocity.y:F2}");
20            acc = 0f;
21        }
22    }
23}

Cube_M1과 Cube_M10에 이 스크립트를 각각 붙이고, Drag=0에서 낙하 로그를 비교하면 vy가 거의 같은 곡선으로 내려간다. Drag=2로 올리면 vy가 특정 값 아래로 더 이상 크게 내려가지 않는다. 이게 중력(가속도)과 drag(감쇠)가 만나는 지점이다.

심화 활용

한 문단 요약: mass는 Force/Impulse에서만 가속도를 바꾸는 분모로 작동하고, useGravity는 물리 스텝마다 중력 가속도를 더할지 결정하며, drag는 fixedDeltaTime마다 속도를 감쇠시켜 종단 속도를 만든다. 같은 입력이라도 FixedUpdate 스텝 수와 ForceMode가 다르면 결과가 달라진다.

패턴 1: ‘무게감’은 mass가 아니라 가속도 제어로 만든다

실무에서 플레이어 이동을 Rigidbody로 만들 때 mass를 올려 무게감을 만들면, 충돌 반응/폭발/넉백까지 전부 둔해진다. 의도는 ‘이동만 둔하게’인데 물리 전체가 무거워진다. 이동 입력은 Acceleration 모드로 주고, 넉백은 Impulse로 분리하면 의도 분리가 된다.

이 패턴이 필요한 이유는 엔진 내부에서 Force 누적 버퍼가 한 덩어리이기 때문이다. 이동과 넉백이 같은 Force로 섞이면 질량 변경 하나로 둘 다 변한다. Acceleration(질량 무시)로 이동을 고정하고, Impulse(질량 반영)로 충격을 주면 튜닝 축이 분리된다.

DualChannelForces.cs
1using UnityEngine;
2
3public class DualChannelForces : MonoBehaviour
4{
5    public Rigidbody rb;
6    public float moveAccel = 25f;
7    public float knockbackImpulse = 6f;
8
9    void Awake()
10    {
11        if (!rb) rb = GetComponent<Rigidbody>();
12    }
13
14    void FixedUpdate()
15    {
16        float x = Input.GetAxisRaw("Horizontal");
17        float z = Input.GetAxisRaw("Vertical");
18        Vector3 wish = new Vector3(x, 0f, z).normalized;
19
20        rb.AddForce(wish * moveAccel, ForceMode.Acceleration);
21
22        if (Input.GetKeyDown(KeyCode.K))
23            rb.AddForce(-transform.forward * knockbackImpulse, ForceMode.Impulse);
24    }
25}

mass를 1→50으로 바꿔도 이동 가속은 거의 유지되고, K 키로 주는 넉백은 무거울수록 덜 튄다. 플레이어 조작감(입력)은 일정하게, 물리 반응(충격)은 무게에 비례하게 만들 때 이 분리가 먹힌다.

패턴 2: drag는 ‘브레이크’가 아니라 ‘종단 속도’로 쓴다

차량/비행체에서 drag를 브레이크처럼 올렸다 내렸다 하면, fixedDeltaTime과 결합해 감각이 환경에 따라 흔들린다. 대신 drag는 기본 공기저항으로 두고, 감속은 반대 방향 힘(Acceleration 또는 Force)으로 구현하면 재현성이 좋아진다. 엔진 내부에서 drag는 매 스텝 자동 감쇠라서 입력과 직접 연결하기 어렵다.

종단 속도를 의도적으로 만들고 싶다면 drag를 올려 “더 빨라지지 않는 최고속”을 만든다. 최고속을 숫자로 고정하고 싶으면 속도 클램프를 추가한다. 이때 Clamp는 네이티브 바디의 속도를 직접 바꾸는 연산이라, 물리 스텝 직후에 적용해야 튀는 프레임이 줄어든다.

TerminalVelocityController.cs
1using UnityEngine;
2
3public class TerminalVelocityController : MonoBehaviour
4{
5    public Rigidbody rb;
6    public float accel = 30f;
7    public float maxSpeed = 8f;
8
9    void Awake()
10    {
11        if (!rb) rb = GetComponent<Rigidbody>();
12    }
13
14    void FixedUpdate()
15    {
16        rb.AddForce(transform.forward * accel, ForceMode.Acceleration);
17
18        Vector3 v = rb.linearVelocity;
19        Vector3 flat = new Vector3(v.x, 0f, v.z);
20        if (flat.magnitude > maxSpeed)
21        {
22            Vector3 clamped = flat.normalized * maxSpeed;
23            rb.linearVelocity = new Vector3(clamped.x, v.y, clamped.z);
24        }
25    }
26}

Game 뷰에서는 어느 시점 이후 더 빨라지지 않고, Console에서 linearVelocity를 찍으면 평평한 값으로 유지된다. drag만으로 최고속을 만들면 환경에 따라 값이 흔들릴 수 있는데, clamp를 같이 쓰면 숫자 목표가 고정된다.

내 흑역사 1: ‘무거운 오브젝트가 더 빨리 떨어진다’고 착각한 적이 있다. 원인은 mass가 아니라 drag였다. 작은 오브젝트는 drag=1, 큰 오브젝트는 drag=0으로 설정되어 있었고, 그래서 큰 오브젝트가 종단 속도에 덜 막혀 더 빨리 떨어져 보였다. Console에 vy를 찍으니 중력 가속 구간은 같고, 종단 속도 구간만 달랐다.

내 흑역사 2: 모바일에서만 이동이 둔해지는 버그를 겪었다. Profiler에서 Physics.Simulate 시간이 길어져 fixed 스텝이 한 프레임에 2번씩 돌았고, Update에서 AddForce를 주는 코드가 힘을 과하게 누적했다. 디버깅 과정에서 FixedUpdate 카운터 로그를 심고, 입력 적용을 FixedUpdate로 옮긴 뒤에야 재현이 사라졌다. 콘솔에는 ‘1초 동안 FixedUpdate=70’ 같은 로그가 찍혔다.

자주 하는 실수

실수 1: Update에서 AddForce를 호출한다

증상: 같은 키 입력인데 PC에서는 정상, 프레임 드랍이 있는 기기에서는 가속이 들쭉날쭉하다. 카메라가 미세하게 떨리거나, 충돌 순간에 튀는 프레임이 생긴다.

원인: 물리는 FixedUpdate 스텝에서 적분되는데, Update는 렌더 프레임 기준이라 호출 횟수가 변한다. 한 프레임에 물리 스텝이 2번이면 Update에서 누적한 힘이 2번 적분되거나, 반대로 물리 스텝이 0번이면 입력이 늦게 반영된다.

해결: 힘/속도 변경은 FixedUpdate에서 처리한다. 입력 샘플링은 Update에서 하고, 결과(의도 벡터)를 필드에 저장해 FixedUpdate에서 사용한다.

실수 2: mass를 올리면 낙하가 빨라질 거라고 생각한다

증상: mass를 1→100으로 올렸는데도 낙하 시간이 비슷하다. 그래서 Gravity 값을 바꾸거나, Transform.position으로 강제로 내리려 한다.

원인: Unity 중력은 가속도로 적용되어 질량과 무관하게 같은 낙하 가속도를 만든다. 교과서의 ‘중력은 힘’ 표현을 그대로 적용하면 직관이 어긋난다.

해결: 낙하 속도를 바꾸고 싶으면 Gravity(프로젝트 설정)나 drag, 또는 커스텀 힘을 조정한다. ‘무거우면 더 빨리’ 같은 연출은 drag를 줄이거나, 추가 가속도를 더하는 방식으로 만든다.

실수 3: drag를 0.5 올렸는데 체감이 과하게 변한다

증상: drag를 조금만 올렸는데 이동이 급격히 둔해지고, 최고속이 예상보다 낮아진다. 플랫폼이나 fixedDeltaTime 변경 후 감각이 망가진다.

원인: drag는 매 물리 스텝에 누적 감쇠로 들어간다. dt가 바뀌면 같은 drag라도 감쇠 누적량이 달라진다. ‘초당 감속’이 아니라 ‘스텝당 감쇠’에 가까운 제어라서 민감하다.

해결: fixedDeltaTime을 먼저 고정하고 drag를 조정한다. 최고속 목표가 있으면 drag 단독에 의존하지 말고 속도 clamp를 병행한다.

실수 4: Rigidbody 이동을 Transform으로 섞어서 쓴다

증상: 충돌이 뚫리거나, 벽에 닿을 때 떨림이 생긴다. 콘솔에 특별한 에러는 없지만 물리 반응이 이상하다.

원인: 물리 엔진은 네이티브 바디 상태를 기준으로 충돌을 푼다. Transform을 직접 바꾸면 네이티브 바디와 Transform 동기화가 강제로 일어나고, 다음 스텝에서 큰 보정이 들어가거나 터널링이 생긴다.

해결: 이동은 rb.MovePosition/MoveRotation 또는 힘 기반으로 통일한다. 순간 이동이 필요하면 rb.position을 바꾸고 rb.linearVelocity를 적절히 리셋해 에너지 보존 문제를 막는다.

실수 5: GetComponent<Rigidbody>()를 FixedUpdate에서 매번 호출한다

증상: 오브젝트 수가 늘면 CPU가 늘고, Profiler에서 Script.Update/FixedUpdate 아래에 GetComponent 비용이 누적된다. 프레임당 0.05~0.3ms씩 올라가는 경우도 있다(기기/컴포넌트 수에 따라 차이).

원인: GetComponent는 네이티브에 있는 컴포넌트 리스트를 탐색하고 바인딩을 통해 C# 래퍼를 반환한다. 매 호출마다 탐색/바인딩 오버헤드가 들어간다.

해결: Awake/Start에서 캐싱한다. 다형성 구조가 필요하면 TryGetComponent를 1회만 호출해 필드에 저장한다.

GetComponentCacheExample.cs
1using UnityEngine;
2
3public class GetComponentCacheExample : MonoBehaviour
4{
5    Rigidbody rb;
6
7    void Awake()
8    {
9        rb = GetComponent<Rigidbody>();
10    }
11
12    void FixedUpdate()
13    {
14        rb.AddForce(Vector3.forward * 5f, ForceMode.Force);
15    }
16}

Profiler에서 Deep Profile을 켜면 GetComponent가 호출 스택에 보인다. 캐싱 후에는 그 항목이 사라지고, 남는 비용은 AddForce 바인딩과 Physics.Simulate 쪽이다. 수십~수백 개 오브젝트가 동일 패턴이면 체감이 난다.

성능 최적화 체크리스트

  • 힘/속도 변경 로직이 FixedUpdate에만 존재한다(입력 샘플링은 Update, 적용은 FixedUpdate).
  • Time.fixedDeltaTime을 프로젝트 초기에 고정하고, 튜닝 중에 임의로 바꾸지 않는다.
  • ForceMode를 의도에 맞게 분리한다(이동=Acceleration, 넉백=Impulse 같은 채널 분리).
  • drag로 최고속을 만들 때 fixedDeltaTime 변경에 따른 체감 변화를 테스트한다(PC/모바일 동일 씬).
  • 속도 클램프가 필요하면 FixedUpdate에서 rb.linearVelocity를 제한하고, 프레임 기반 보간과 충돌을 함께 확인한다.
  • Rigidbody Interpolation 설정을 확인한다(None/Interpolate/Extrapolate) — 화면 떨림이면 먼저 여기부터 본다.
  • Transform.position 직접 변경과 Rigidbody 물리 이동을 섞지 않는다(순간이동은 velocity 리셋 포함).
  • GetComponent/TryGetComponent는 Awake에서 캐싱하고, 반복 호출은 Profiler로 비용을 확인한다.
  • Profiler에서 Physics.Simulate, Rigidbody.AddForce(스크립트), FixedUpdate 호출 횟수를 함께 본다(스텝 증가 여부).
  • 충돌이 뚫리면 Collision Detection(Discrete/Continuous)와 Fixed Timestep을 같이 점검한다.
  • 대량 오브젝트는 Rigidbody Sleep(잠자기) 상태를 활용하고, 불필요한 AddForce 호출로 깨우지 않는다.
  • 드래그/중력 튜닝은 실제 속도 로그를 남긴다(y, vy, v magnitude) — 감각만으로 맞추지 않는다.

자주 묻는 질문

mass를 10으로 올렸는데도 점프 높이가 그대로인 이유가 뭔가?

점프를 AddForce에서 ForceMode.VelocityChange나 ForceMode.Acceleration으로 주면 질량이 계산에서 빠진다. VelocityChange는 ‘즉시 속도 변화’를 적용하는 경로라서 m이 커도 같은 Δv가 들어간다. 반대로 Force나 Impulse는 F/m 또는 J/m 형태로 들어가 질량이 커질수록 덜 뜬다. 점프가 “항상 같은 높이”여야 하면 VelocityChange를 쓰고, “무거우면 덜 뛰어야” 하면 Impulse를 쓴다. 다음 학습 키워드는 ForceMode별 수식, 적분 단계(velocity integration), 그리고 애니메이션 루트모션과 물리 점프 혼합 시의 우선순위다.

useGravity를 끄고 직접 중력을 구현하면 뭐가 달라지나?

useGravity는 네이티브 물리 스텝에서 자동으로 중력 가속도를 더하는 스위치다. 끄고 직접 구현하면 중력 방향/크기를 상황별로 바꾸기 쉽고(행성 중력, 자기장), 특정 상태에서만 중력을 적용하는 제어가 쉬워진다. 대신 구현을 Update에서 하면 프레임레이트에 따라 중력 누적이 달라져 버그가 난다. FixedUpdate에서 rb.AddForce(gVector, ForceMode.Acceleration)처럼 가속도 모드로 적용하면 기본 중력과 같은 성질을 유지한다. 다음 학습 키워드는 커스텀 중력 필드, CharacterController와 Rigidbody의 선택, 그리고 네이티브 Physics.Simulate 타이밍이다.

drag를 올리면 왜 최고속이 생기나? 힘을 계속 주는데도 속도가 안 오른다.

drag는 물리 스텝마다 속도를 감쇠시키는 항이라서, 추진으로 얻는 속도 증가량과 drag로 잃는 감소량이 같아지는 지점이 생긴다. 그 지점 이후에는 매 스텝 ‘올라간 만큼 깎여서’ 속도가 거의 유지된다. 이 현상은 프레임 기반이 아니라 fixedDeltaTime 기반 누적이라서 dt를 바꾸면 최고속도 바뀐다. 최고속을 숫자로 고정하려면 drag만 의존하지 말고 linearVelocity 클램프를 병행한다. 다음 학습 키워드는 terminal velocity, exponential damping, fixedDeltaTime 변경이 게임플레이에 미치는 영향이다.

Update와 FixedUpdate를 나누는 이유가 진짜로 성능 때문인가?

핵심은 성능보다 결정성(일관성)이다. 물리 엔진은 고정 시간 스텝에서 충돌/제약을 풀고 적분한다. 렌더 프레임은 기기 성능에 따라 변하므로, 물리를 렌더 프레임에 맞추면 같은 입력이라도 결과가 달라진다. Unity는 Player Loop에서 FixedUpdate 구간에 물리 스텝을 배치하고, 필요하면 한 프레임에 여러 번 돌려 ‘따라잡기’를 한다. 그래서 물리 입력은 FixedUpdate에서 주는 게 맞고, 화면 반응은 interpolation으로 부드럽게 만든다. 다음 학습 키워드는 PlayerLoop 구조, Physics.Simulate, interpolation/extrapolation의 원리다.

Rigidbody 속도 읽기/쓰기(linearVelocity)가 GC Alloc을 만들까?

대부분의 경우 Vector3는 값 타입이라 GC Alloc은 없다. 다만 boxing이 발생하는 형태(예: object로 캐스팅, 비제네릭 컬렉션에 넣기, string.Format 남발)로 쓰면 할당이 생길 수 있다. 또, Debug.Log를 매 스텝 호출하면 문자열 생성으로 GC가 생기기 쉽다. 속도 로그는 간격을 두고 찍거나, 개발 빌드에서만 켠다. Profiler의 GC Alloc 컬럼을 보고 ‘Script’ 구간에서 할당이 생기는지 확인하면 된다. 다음 학습 키워드는 boxing, 문자열 할당, Profiler Timeline에서 GC 원인 추적이다.

같은 Rigidbody인데 에디터에서는 괜찮고 빌드에서만 더 미끄럽거나 둔하게 느껴진다.

빌드에서 성능이 떨어지면 한 프레임에 물리 스텝이 여러 번 돌거나(따라잡기), 반대로 렌더 프레임이 빨라 물리 스텝이 없는 프레임이 늘어난다. Update에서 힘을 주는 코드가 있으면 체감이 크게 흔들린다. 또 Quality 설정, VSync, targetFrameRate 차이로 Update 빈도가 달라져 입력 샘플링이 달라질 수도 있다. 해결은 1) 물리 적용을 FixedUpdate로 고정 2) interpolation 설정 점검 3) fixedDeltaTime과 maximumDeltaTime(Time settings) 점검 순서로 간다. 다음 학습 키워드는 maximumDeltaTime, 모바일 CPU 스로틀링, 물리 스텝 드랍 시 보정 전략이다.

mass/drag 값을 실무에서 어떤 기준으로 잡나? 숫자에 단위가 있나?

Unity의 물리 단위는 ‘미터-킬로그램-초’에 가까운 관례를 따르지만, 스케일이 정확히 현실과 1:1이어야 하는 시스템은 아니다. 중요한 건 씬 스케일(1유닛=1m 가정), fixedDeltaTime, 힘의 크기(Acceleration/Force)와 함께 세트로 맞추는 것이다. 실무에서는 먼저 목표 속도/가속(예: 0→6m/s를 0.4초)에 맞춰 Acceleration을 잡고, 최고속이 필요하면 drag 또는 clamp로 제한한다. mass는 충돌/넉백/관절 안정성을 좌우하므로 ‘이동감’이 아니라 ‘물리 반응’ 기준으로 잡는 편이 튜닝 축이 덜 꼬인다. 다음 학습 키워드는 스케일링, joint 안정화(mass ratio), solver iteration, 물리 머티리얼 마찰과 drag의 역할 분리다.

관련 글

12. Rigidbody 질량·중력·드래그가 움직임을 바꾸는 이유(엔진 내부 포함)

12. Rigidbody 질량·중력·드래그가 움직임을 바꾸는 이유(엔진 내부 포함)

Unity Rigidbody의 mass, useGravity, drag가 왜 다른 움직임을 만드는지 Player Loop, C#→네이티브 바인딩, FixedUpdate 적분 관점에서 설명한다. 실습과 성능 함정 포함. 2026 기준 실무 팁 제공.

14. Rigidbody 질량·중력·드래그로 떨어짐 감 빠르게 세팅하는 법

14. Rigidbody 질량·중력·드래그로 떨어짐 감 빠르게 세팅하는 법

Rigidbody mass·useGravity·drag·angularDrag가 낙하 감각에 미치는 영향을 Player Loop·C++ 물리 스텝 관점에서 설명하고, 빠른 세팅 절차와 실무 패턴까지 다룬다. (초보용)​)​)​)​)​)​)​)​)​)​)​)

13. Rigidbody 질량·중력·드래그로 물리 움직임 만들기와 엔진 내부 흐름

13. Rigidbody 질량·중력·드래그로 물리 움직임 만들기와 엔진 내부 흐름

Unity Rigidbody의 mass, useGravity, drag 설정이 FixedUpdate에서 네이티브 물리로 어떻게 반영되는지, 성능·GC·설계 이유까지 함께 설명한다(초보자용). 154자 내외 구성. 154자 내외 구성. 154자 내외 구성.