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

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

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

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

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

게임 만들다 캐릭터가 ‘돌덩이처럼’ 쿵 떨어지거나, 반대로 ‘풍선처럼’ 느리게 내려와서 액션 타이밍이 전부 어긋나는 사고가 난다. Inspector에서 mass만 바꿔도 느낌이 바뀌는데, 왜 어떤 값은 먹히고 어떤 값은 체감이 없는지 처음엔 감이 안 잡힌다. 낙하 감은 Rigidbody가 네이티브 물리 스텝에서 적분되는 방식(중력, 감쇠, 충돌 해결) 때문에 숫자 조합으로 재현된다. 이 글은 그 조합을 빠르게 잡는 기준을 만든다.​)​)​)​)​)​)​)​)​)​)​)​)

핵심 개념

‘떨어짐 감’은 시각 효과가 아니라 물리 적분 결과의 체감이다. Unity에서 Rigidbody는 매 FixedUpdate 물리 스텝마다 속도/위치를 갱신한다. 이때 useGravity가 켜져 있으면 중력 가속도(Physics.gravity)가 속도에 더해지고, drag는 속도에 비례한 감쇠(선형 감쇠)를 적용한다. mass는 같은 힘을 받았을 때 가속도를 바꾸지만, 중력 가속도 자체는 질량과 무관하게 적용되는 점이 핵심이다.

용어를 게임 제작 맥락으로 잡아두면 세팅이 빨라진다. mass는 ‘밀어낼 때 무거운가’와 ‘충돌 해결에서 얼마나 밀리나’에 더 가깝다. useGravity는 ‘항상 아래로 가속을 주는가’이고, drag는 ‘공기저항처럼 속도를 깎아 종단속도(terminal velocity)를 만드는가’이다. angularDrag는 회전 감쇠라서 낙하 중 회전이 과하게 도는 오브젝트(상자, 시체, 파편)에서 체감이 크다.

초보가 자주 헷갈리는 부분이 “mass를 키우면 더 빨리 떨어질 것”이라는 직감이다. Unity 물리에서 중력은 힘이 아니라 가속도로 적용되는 형태라서(내부적으로 질량을 곱해 힘으로 바꾼 뒤 다시 질량으로 나누는 형태로 상쇄되거나, 아예 가속도 항으로 들어간다) mass만 바꿔서는 낙하 시간은 거의 같다. 대신 충돌 시 다른 물체를 얼마나 밀어내는지, 접촉면에서 얼마나 안정적으로 버티는지 같은 ‘상호작용’이 바뀐다.

낙하 감을 빠르게 잡는 실무 기준은 두 축이다. 1) 낙하 속도 곡선: 초반 가속이 빠른지, 중후반에 속도가 제한되는지(=drag). 2) 착지/충돌 반응: 튕김/미끄러짐/밀림(=mass+material+solver). 여기서 drag는 ‘속도 곡선’을 직접 바꾸는 레버라서 체감이 즉각적이고, mass는 ‘충돌 장면’에서 체감이 커서 테스트 장면을 분리해 확인하는 게 효율적이다.

FallFeelBaseline.cs
1using UnityEngine;
2
3public class FallFeelBaseline : MonoBehaviour
4{
5    [Header("Quick knobs")]
6    public float mass = 1f;
7    public bool useGravity = true;
8    [Range(0f, 10f)] public float drag = 0f;
9    [Range(0f, 10f)] public float angularDrag = 0.05f;
10
11    Rigidbody _rb;
12
13    void Awake()
14    {
15        _rb = GetComponent<Rigidbody>();
16    }
17
18    void OnValidate()
19    {
20        if (_rb == null) _rb = GetComponent<Rigidbody>();
21        if (_rb == null) return;
22
23        _rb.mass = Mathf.Max(0.0001f, mass);
24        _rb.useGravity = useGravity;
25        _rb.drag = drag;
26        _rb.angularDrag = angularDrag;
27    }
28}

이 스크립트를 붙이고 Inspector에서 슬라이더를 움직이면 Play 모드가 아니어도 Rigidbody 값이 즉시 바뀐다. OnValidate는 에디터에서 값이 변경될 때 호출되며, 네이티브 Rigidbody 핸들에 프로퍼티를 전달한다. 낙하 감을 잡을 때 ‘실행-정지-수정’ 루프를 줄여서 체감 테스트 횟수를 늘리는 목적이다.

엔진 관점에서의 내부 동작

Rigidbody.mass 같은 C# 프로퍼티는 순수 C# 필드가 아니다. 내부적으로는 C# 래퍼가 네이티브(C++) 엔진 오브젝트(PhysX의 rigid body 또는 Unity 물리 래퍼)에 값을 전달하는 바인딩 호출이다. 그래서 mass를 바꾸는 순간 C# 힙에 값이 저장되는 게 아니라, 네이티브 쪽 구조체/객체에 있는 질량/역질량(inverse mass) 캐시가 갱신된다.

Player Loop 관점에서 물리는 Update가 아니라 FixedUpdate 타이밍에 진행된다. 유니티는 프레임마다 Accumulate한 시간을 기준으로 Physics.Simulate(내부 호출)를 0회 이상 수행한다. Time.fixedDeltaTime이 0.02라면 50Hz이고, 렌더 프레임이 20fps로 떨어지면 한 프레임에 물리 스텝을 2~3번 돌려 따라잡는다. 낙하 감이 프레임레이트에 덜 흔들리는 이유가 여기 있다.

useGravity는 ‘중력 항을 적분에 넣을지’ 여부다. 내부적으로는 각 rigid body에 중력 적용 플래그가 있고, 물리 스텝에서 v += g * dt가 들어간다(개념적으로). Physics.gravity는 월드 전역 벡터이고, Rigidbody는 per-body로 on/off만 제공한다. 그래서 “중력 배율”을 만들려면 AddForce(ForceMode.Acceleration)로 별도의 가속도를 더하는 방식이 일반적이다.

drag는 종종 ‘마찰’로 오해되지만, 공기저항처럼 속도에 비례해 감쇠시키는 선형 감쇠(linear damping)다. 물리 엔진은 스텝마다 v *= (1 / (1 + drag * dt)) 같은 형태로 감쇠를 적용하거나, 근사식을 쓴다. 그래서 drag를 올리면 종단속도가 생기고, 낙하가 ‘끈적하게’ 느려진다. 반대로 바닥에서 미끄러짐은 물리 머티리얼의 마찰 계수와 접촉 해결이 좌우한다.

mass가 낙하 시간에 거의 영향을 못 주는 이유는 중력 가속도가 질량과 무관하기 때문이다. 뉴턴식으로는 F = m*g이고 a = F/m이므로 a = g로 상쇄된다. PhysX도 기본 중력은 가속도 기반으로 적용된다. 다만 충돌 해결(impulse)에서는 질량/역질량이 직접 들어가서, 같은 속도로 바닥을 칠 때 반동/상대 물체 밀림이 달라진다.

메모리 관점에서 Rigidbody는 C# 객체와 네이티브 객체가 1:1로 매칭된다. C# 쪽 Rigidbody 레퍼런스는 ‘네이티브 핸들을 가리키는 래퍼’이고, 실제 시뮬레이션 상태(속도, 잠자기 상태, 접촉 캐시)는 네이티브 메모리에 있다. 그래서 GetComponent<Rigidbody>()는 네이티브 컴포넌트 리스트를 조회하는 비용이 있고, Update에서 매 프레임 호출하면 그 비용이 누적된다. Awake에서 캐싱하면 호출 횟수가 1회로 고정된다.

PlayerLoopPhysicsProbe.cs
1using UnityEngine;
2
3public class PlayerLoopPhysicsProbe : MonoBehaviour
4{
5    Rigidbody _rb;
6    float _t;
7
8    void Awake()
9    {
10        _rb = GetComponent<Rigidbody>();
11        Debug.Log($"Awake frame={Time.frameCount}");
12    }
13
14    void Start()
15    {
16        Debug.Log($"Start frame={Time.frameCount}");
17    }
18
19    void FixedUpdate()
20    {
21        _t += Time.fixedDeltaTime;
22        Debug.Log($"FixedUpdate t={_t:F3} frame={Time.frameCount} v={_rb.linearVelocity.y:F3}");
23    }
24
25    void Update()
26    {
27        Debug.Log($"Update dt={Time.deltaTime:F3} frame={Time.frameCount}");
28    }
29}

이 스크립트를 Cube에 붙이고 Play를 누르면 Console에 FixedUpdate 로그가 Update보다 여러 번 찍히는 프레임이 생긴다(에디터에서 Game 뷰가 무거울수록 잘 보인다). 낙하 속도(v)가 FixedUpdate에서만 연속적으로 변하고, Update는 렌더 프레임 기준으로 호출되는 사실이 눈으로 확인된다. 떨어짐 감은 FixedUpdate의 적분 결과이고, Update에서 Transform을 건드리면 물리와 렌더가 싸우기 시작한다.

GetComponentCostDemo.cs
1using UnityEngine;
2
3public class GetComponentCostDemo : MonoBehaviour
4{
5    Rigidbody _cached;
6    public int iterations = 2000;
7
8    void Awake()
9    {
10        _cached = GetComponent<Rigidbody>();
11    }
12
13    void Update()
14    {
15        float t0 = Time.realtimeSinceStartup;
16        for (int i = 0; i < iterations; i++)
17        {
18            var rb = GetComponent<Rigidbody>();
19            rb.useGravity = rb.useGravity;
20        }
21        float t1 = Time.realtimeSinceStartup;
22
23        float t2 = Time.realtimeSinceStartup;
24        for (int i = 0; i < iterations; i++)
25        {
26            _cached.useGravity = _cached.useGravity;
27        }
28        float t3 = Time.realtimeSinceStartup;
29
30        Debug.Log($"GetComponent loop={(t1 - t0) * 1000f:F3}ms, cached loop={(t3 - t2) * 1000f:F3}ms");
31        enabled = false;
32    }
33}

실행하면 1프레임에 한 번만 로그가 찍히고 스크립트가 꺼진다. 환경마다 수치는 다르지만 GetComponent 루프 쪽이 눈에 띄게 더 오래 걸린다. 이유는 GetComponent가 C# 배열에서 찾는 게 아니라 네이티브 쪽 컴포넌트 컨테이너를 조회하고, 결과를 C# 래퍼로 돌려주는 경로를 타기 때문이다. 낙하 감 튜닝 스크립트를 Update에서 돌리며 GetComponent를 반복 호출하면 물리 자체보다 스크립트 비용이 먼저 튄다.

한 문단 요약: 낙하 감은 FixedUpdate 물리 스텝에서 중력 가속과 drag 감쇠가 적분된 결과이고, mass는 낙하 속도보다 충돌 반응을 바꾼다. C# 프로퍼티는 네이티브 바인딩 호출이라 GetComponent/프로퍼티 세팅을 반복하면 비용이 누적된다.

실습하기

1단계: 프로젝트 설정(물리 스텝 기준 고정)​)​)​)​)​)​)​)​)​)​)​)​)

Unity 2022.3 LTS 또는 2023.2+ 기준. Unity Hub → New project → 3D(Core) 선택 후 생성한다. 프로젝트가 열리면 Edit → Project Settings → Time로 이동해서 Fixed Timestep이 0.02인지 확인한다(기본 50Hz). 이 값이 바뀌면 낙하 곡선이 달라져서 튜닝 숫자가 재사용이 안 된다.

Edit → Project Settings → Physics로 이동해 Gravity가 (0, -9.81, 0)인지 확인한다. 테스트 중에는 중력을 건드리지 않는 편이 낫다. 중력 자체를 바꾸면 모든 Rigidbody가 영향을 받아서 ‘낙하 감’이 아니라 ‘월드 규칙’을 바꾼 셈이 된다.

2단계: 씬 구성(테스트 장면을 분리)​)​)​)​)​)​)​)​)​)​)​)​)

Hierarchy 우클릭 → 3D Object → Plane 생성, Position (0,0,0), Scale (5,1,5). Hierarchy 우클릭 → 3D Object → Cube 생성, Position (0,5,0). Cube 선택 → Inspector → Add Component → Rigidbody 추가한다. Rigidbody의 Use Gravity 체크 ON, Drag 0, Angular Drag 0.05, Mass 1로 시작한다.

낙하 감은 ‘공중 구간’과 ‘착지 구간’이 섞이면 판단이 흐려진다. Plane은 마찰/반발이 기본값이라 착지 후 미끄러짐이 생길 수 있다. Plane 선택 → Add Component → Physic Material은 이번 실습에서는 생략하고, Cube의 낙하 시간과 속도만 먼저 본다. 착지 감은 심화 섹션에서 분리한다.

FallFeelLab.cs
1using UnityEngine;
2
3public class FallFeelLab : MonoBehaviour
4{
5    [Header("Assign in Inspector")]
6    public Rigidbody target;
7
8    [Header("Tuning")]
9    public bool useGravity = true;
10    public float mass = 1f;
11    [Range(0f, 10f)] public float drag = 0f;
12
13    [Header("Readout")]
14    public float height = 5f;
15
16    float _startY;
17
18    void Awake()
19    {
20        if (target == null) target = GetComponent<Rigidbody>();
21        _startY = transform.position.y;
22    }
23
24    void Start()
25    {
26        Apply();
27        ResetDrop();
28    }
29
30    void Apply()
31    {
32        target.mass = Mathf.Max(0.0001f, mass);
33        target.useGravity = useGravity;
34        target.drag = drag;
35    }
36
37    void ResetDrop()
38    {
39        target.linearVelocity = Vector3.zero;
40        target.angularVelocity = Vector3.zero;
41        transform.position = new Vector3(0f, height, 0f);
42        _startY = transform.position.y;
43    }
44
45    void Update()
46    {
47        if (Input.GetKeyDown(KeyCode.R))
48        {
49            Apply();
50            ResetDrop();
51        }
52    }
53}

Cube에 이 스크립트를 붙이고 Inspector에서 Target에 Cube의 Rigidbody를 드래그한다. Play 후 R을 누르면 같은 높이에서 다시 떨어진다. 실행하면 Game 뷰에서 Cube가 항상 y=height에서 시작하고, drag 값을 바꾼 뒤 R을 눌렀을 때 낙하 속도 곡선이 달라지는 체감이 생긴다. mass를 바꾼 뒤 R을 눌러도 낙하 시간은 크게 변하지 않는다는 점이 눈에 들어온다.

3단계: 수치로 확인(시간·속도 로그)​)​)​)​)​)​)​)​)​)​)​)​)

체감만으로 튜닝하면 팀 내 합의가 어렵다. 낙하 시작부터 바닥 접촉까지 시간을 찍으면 숫자로 대화가 된다. Cube가 Plane에 닿는 순간을 OnCollisionEnter로 잡고, 낙하 시간과 충돌 직전 속도를 출력한다. 이 로그는 drag 조합을 비교할 때 특히 유용하다.

Hierarchy 우클릭 → Create Empty 생성 후 이름을 FallManager로 바꾸고, 아래 스크립트를 붙인다. Inspector에서 Target에 Cube Rigidbody를 할당한다. Play 후 Console에서 “fallTime”과 “impactVy”가 찍히는지 확인한다. drag를 0→1→3으로 바꾸고 R로 재낙하시키면 fallTime이 늘고 impactVy 절댓값이 줄어든다.

FallMetrics.cs
1using UnityEngine;
2
3public class FallMetrics : MonoBehaviour
4{
5    public Rigidbody target;
6    public float groundY = 0f;
7
8    float _startTime;
9    bool _armed;
10
11    void Start()
12    {
13        if (target == null)
14        {
15            Debug.LogError("FallMetrics: target Rigidbody is not assigned");
16            enabled = false;
17            return;
18        }
19        Arm();
20    }
21
22    public void Arm()
23    {
24        _startTime = Time.time;
25        _armed = true;
26    }
27
28    void FixedUpdate()
29    {
30        if (!_armed) return;
31        if (target.position.y <= groundY + 0.01f && target.linearVelocity.y <= 0f)
32        {
33            float fallTime = Time.time - _startTime;
34            float impactVy = target.linearVelocity.y;
35            Debug.Log($"fallTime={fallTime:F3}s, impactVy={impactVy:F3} m/s, drag={target.drag}, mass={target.mass}");
36            _armed = false;
37        }
38    }
39}

FixedUpdate에서 조건을 체크하는 이유가 있다. 물리 위치/속도는 물리 스텝에서 갱신되기 때문에 Update에서 읽으면 렌더 보간(Interpolation) 설정에 따라 ‘보이는 위치’와 ‘물리 위치’가 어긋날 수 있다. 같은 드래그 값인데도 프레임마다 fallTime이 흔들리는 현상을 처음 겪었을 때, 원인은 Update에서 측정한 것과 FixedUpdate에서 측정한 것의 타이밍 차이였다.

심화 활용

패턴 1: 중력 배율을 per-object로 만든다(ForceMode.Acceleration)​)​)​)​)​)​)​)​)​)​)​)​)

Unity는 Rigidbody에 gravityScale 같은 값을 기본 제공하지 않는다. 이유는 엔진 설계상 월드 중력은 전역이고, per-body는 on/off 정도만 제공하는 편이 물리 엔진 통합이 단순해지기 때문이다. 그래서 캐릭터만 더 빠르게 떨어지게 만들고 싶으면 AddForce를 가속도 모드로 넣는다. 이 방식은 mass와 무관하게 가속도를 추가하므로, ‘낙하 감’만 바꾸고 충돌 질량감은 유지할 수 있다.

GravityMultiplier.cs
1using UnityEngine;
2
3public class GravityMultiplier : MonoBehaviour
4{
5    public Rigidbody target;
6    [Range(0f, 5f)] public float gravityMultiplier = 2f;
7
8    void Awake()
9    {
10        if (target == null) target = GetComponent<Rigidbody>();
11    }
12
13    void FixedUpdate()
14    {
15        if (!target.useGravity) return;
16
17        Vector3 g = Physics.gravity;
18        Vector3 extra = g * (gravityMultiplier - 1f);
19        target.AddForce(extra, ForceMode.Acceleration);
20    }
21}

Play 후 gravityMultiplier를 1→2→3으로 바꾸면 낙하가 즉시 빨라진다. mass를 0.5에서 10으로 바꿔도 낙하 속도 곡선은 거의 동일하게 유지된다. 왜냐하면 ForceMode.Acceleration은 ‘힘’이 아니라 ‘가속도’를 네이티브 물리 스텝에 직접 더하는 경로라서 질량 항이 들어가지 않는다.

패턴 2: 드래그로 종단속도를 설계하고, 착지 감은 Material로 분리한다​)​)​)​)​)​)​)​)​)​)​)​)

드래그를 올리면 종단속도가 생겨서 ‘공중에서 속도가 제한되는’ 느낌이 난다. 다만 착지에서 튕김/미끄러짐까지 드래그로 해결하려고 하면 공중 구간이 둔해져 게임 템포가 무너진다. 착지 감은 Collider의 Physic Material(마찰/반발)과 Collision Detection, Solver Iteration이 더 직접적이다.

LandingFeelSplitter.cs
1using UnityEngine;
2
3public class LandingFeelSplitter : MonoBehaviour
4{
5    public Collider groundCollider;
6    public PhysicMaterial sticky;
7    public PhysicMaterial slippery;
8
9    [Header("Toggle")]
10    public bool stickyLanding = true;
11
12    void Update()
13    {
14        if (Input.GetKeyDown(KeyCode.T))
15        {
16            stickyLanding = !stickyLanding;
17            if (groundCollider != null)
18                groundCollider.material = stickyLanding ? sticky : slippery;
19
20            Debug.Log($"Ground material={(stickyLanding ? "sticky" : "slippery")}");
21        }
22    }
23}

Hierarchy에서 Plane 선택 → Inspector에서 Physic Material을 두 개 만든다(Project 창 우클릭 → Create → Physic Material). sticky는 Dynamic/Static Friction을 1, Bounciness 0. slippery는 Friction 0, Bounciness 0으로 둔다. Play 후 T를 누르면 Cube가 착지 후 미끄러지는 정도가 확 바뀐다. 공중 낙하 곡선은 그대로고 착지 감만 분리되어 조절된다.

처음에 나도 mass로 ‘무겁게 쿵’ 느낌을 만들려다가 계속 실패했다. 낙하가 무거워지지 않아서 mass를 50, 100으로 올리니 이번엔 캐릭터가 작은 상자들을 밀어버려 레벨이 망가졌다. 콘솔엔 에러가 없고, 그냥 플레이 감이 이상해지는 타입이라 더 늦게 눈치챘다. 3시간 삽질 끝에 알아낸 건 ‘무겁게 쿵’은 낙하 속도(중력/드래그)와 착지 시 반발/마찰(머티리얼) 조합으로 만들어야 하고, mass는 상호작용 설계용이라는 점이었다.

또 한 번은 FixedUpdate에서 AddForce로 중력 배율을 넣어놨는데, 어떤 씬에서만 낙하가 들쭉날쭉했다. Profiler를 열어보니 Physics.Simulate가 한 프레임에 3번씩 돌고 있었고(프레임 드랍), 그때마다 AddForce가 누적되니 체감이 과장됐다. 해결은 Time.fixedDeltaTime을 건드리는 게 아니라, ‘추가 중력’이 스텝 횟수에 비례해 들어가는 게 정상임을 받아들이고, 테스트 장면은 60fps 유지되는 환경에서 수치를 먼저 확정한 뒤 저사양 보정(최대 낙하 속도 클램프)을 별도로 넣는 방식이었다.

MaxFallSpeedClamp.cs
1using UnityEngine;
2
3public class MaxFallSpeedClamp : MonoBehaviour
4{
5    public Rigidbody target;
6    public float maxDownSpeed = 25f;
7
8    void Awake()
9    {
10        if (target == null) target = GetComponent<Rigidbody>();
11    }
12
13    void FixedUpdate()
14    {
15        Vector3 v = target.linearVelocity;
16        if (v.y < -maxDownSpeed)
17        {
18            v.y = -maxDownSpeed;
19            target.linearVelocity = v;
20        }
21    }
22}

이 클램프는 ‘프레임 드랍에서만 과장되는 낙하’를 완충하는 안전장치다. 실행하면 drag를 낮게 두고도 낙하 속도가 -maxDownSpeed 이하로 내려가지 않는다. 네이티브 물리 스텝에서 속도를 적분한 뒤, C#에서 다시 속도를 덮어쓰는 방식이라 물리적으로 완벽하진 않지만, 액션 게임에서 카메라/애니메이션 타이밍을 보호하는 용도로 자주 쓴다.

자주 하는 실수

실수 1: mass를 올리면 더 빨리 떨어진다고 믿는다​)​)​)​)​)​)​)​)​)​)​)​)

증상: mass를 1에서 20으로 올렸는데 낙하 시간이 거의 변하지 않는다. 대신 착지할 때 바닥 오브젝트를 더 밀거나, 다른 Rigidbody를 밀어내며 장면이 어수선해진다.

원인: 중력은 가속도 기반으로 적용되어 질량과 상쇄된다. mass는 충돌 임펄스 계산과 solver에서 역질량으로 들어가서 ‘상호작용’만 바뀐다.

해결: 낙하 속도 곡선은 drag 또는 추가 가속도(AddForce, ForceMode.Acceleration)로 조절하고, mass는 밀림/충돌 우선순위 설계용으로 제한한다. 낙하 감과 상호작용 감을 같은 슬라이더로 해결하려는 시도를 끊는다.

실수 2: Update에서 Transform.position을 내려서 ‘낙하’를 만든다​)​)​)​)​)​)​)​)​)​)​)​)

증상: 오브젝트가 바닥에 닿을 때 튕기거나 떨리고, Collider를 뚫고 내려가기도 한다. 콘솔에는 에러가 없는데 충돌이 불안정하다.

원인: 물리는 FixedUpdate에서 통합되는데 Update에서 Transform을 직접 바꾸면 네이티브 물리 상태와 렌더 트랜스폼이 경쟁한다. 다음 물리 스텝에서 큰 보정이 걸리며 터널링이 발생한다.

해결: Rigidbody가 붙은 오브젝트는 MovePosition/MoveRotation 또는 AddForce로 움직인다. ‘낙하’는 useGravity와 물리 스텝에 맡기고, 위치 텔레포트는 Reset 시점처럼 의도된 순간에만 쓴다.

실수 3: drag로 바닥 마찰까지 해결하려 한다​)​)​)​)​)​)​)​)​)​)​)​)

증상: 착지 후 미끄러짐을 줄이려고 drag를 5 이상으로 올렸더니 공중에서 둔해지고 점프/낙하 템포가 죽는다. 캐릭터가 ‘물속’처럼 움직인다.

원인: drag는 접촉 여부와 무관하게 속도 전체를 감쇠한다. 바닥에서 미끄러짐은 접촉 마찰(Physic Material)과 접촉 solver가 결정한다.

해결: 공중 감은 drag로, 착지 감은 Physic Material의 friction/bounciness로 분리한다. 필요하면 착지 중에만 드래그를 순간적으로 올리는 방식(상태 기반)을 쓴다.

실수 4: FixedUpdate에서 GetComponent를 매번 호출한다​)​)​)​)​)​)​)​)​)​)​)​)

증상: 물리 오브젝트가 많아지면 프레임이 갑자기 떨어진다. Profiler에서 Scripts 쪽이 올라가는데, 물리 계산(Physics.Simulate)보다 스크립트가 더 비싸게 나온다.

원인: GetComponent는 네이티브 컴포넌트 컨테이너 조회를 수행하고, 반복 호출 시 호출 오버헤드가 누적된다. FixedUpdate는 프레임당 여러 번 호출될 수 있어 비용이 더 커진다.

해결: Awake/Start에서 Rigidbody를 캐싱하고, 루프에서 캐시를 사용한다. Profiler에서 Timeline 모드로 GetComponent 호출 스파이크가 사라지는지 확인한다.

실수 5: Interpolate 설정을 모르고 측정/카메라를 Update에서 맞춘다​)​)​)​)​)​)​)​)​)​)​)​)

증상: Game 뷰에서는 부드럽게 떨어지는데, 로그로 찍은 y나 속도는 계단처럼 튄다. 카메라가 오브젝트를 따라갈 때 미세하게 떨린다.

원인: Rigidbody Interpolation은 렌더 프레임에서 물리 결과를 보간해 보여준다. Update에서 물리 값을 읽으면 보간 전/후가 섞여서 판단이 흔들린다.

해결: 측정은 FixedUpdate에서 하고, 카메라 추적은 LateUpdate에서 보간된 Transform을 기준으로 한다. 필요하면 Rigidbody Interpolate를 Interpolate로 두고, 속도 기반 연출은 FixedUpdate에서 계산한 값을 별도 변수로 전달한다.

FixedToLateBridge.cs
1using UnityEngine;
2
3public class FixedToLateBridge : MonoBehaviour
4{
5    public Rigidbody target;
6    public Transform cameraRig;
7    public Vector3 offset = new Vector3(0f, 2f, -6f);
8
9    float _lastFixedVy;
10
11    void Awake()
12    {
13        if (target == null) target = GetComponent<Rigidbody>();
14    }
15
16    void FixedUpdate()
17    {
18        _lastFixedVy = target.linearVelocity.y;
19    }
20
21    void LateUpdate()
22    {
23        if (cameraRig == null) return;
24        cameraRig.position = target.transform.position + offset;
25        if (Time.frameCount % 30 == 0)
26            Debug.Log($"renderY={target.transform.position.y:F3}, lastFixedVy={_lastFixedVy:F3}");
27    }
28}

이 브리지는 ‘물리에서 계산한 값’과 ‘렌더에서 보이는 값’을 섞지 않게 해준다. 실행하면 30프레임마다 renderY(보간된 트랜스폼)와 lastFixedVy(물리 스텝 속도)가 같이 찍힌다. 카메라 떨림을 잡을 때 이 분리가 없으면 원인 추적이 계속 꼬인다.

성능 최적화 체크리스트

  • Time.fixedDeltaTime을 프로젝트 초기에 고정하고(예: 0.02), 튜닝 숫자를 그 값에 종속시킨다
  • 낙하 감 테스트 씬을 따로 만들고(Plane+Cube), 착지/마찰 요소를 초기에 분리한다
  • Rigidbody는 Awake에서 캐싱하고 FixedUpdate/Update에서 GetComponent를 호출하지 않는다
  • 낙하 속도 곡선은 drag(선형 감쇠)와 추가 가속도(ForceMode.Acceleration)로 설계한다
  • mass는 낙하 속도용이 아니라 충돌 상호작용(밀림/반동/안정성) 설계용으로 다룬다
  • 측정(낙하 시간/충돌 직전 속도)은 FixedUpdate에서 수행해 보간/프레임 의존을 제거한다
  • Rigidbody Interpolation을 켠 경우, 카메라/연출은 LateUpdate에서 Transform 기준으로 처리한다
  • 빠른 낙하를 만들 때는 drag를 과하게 올리기보다 최대 낙하 속도 클램프 같은 안전장치를 병행한다
  • Profiler에서 Physics.Simulate와 Scripts 비용을 분리해 보고, 스크립트 오버헤드가 물리를 가리는지 확인한다
  • Collision Detection(Discrete/Continuous)을 낙하 속도에 맞게 조정해 터널링을 방지한다
  • 많은 Rigidbody를 스폰하는 장면은 풀링을 적용하고, Instantiate/Destroy로 인한 GC/스파이크를 제거한다
  • 물리 오브젝트 수가 많으면 Solver Iteration/Contact Offset 조정 전후를 같은 테스트로 비교한다

자주 묻는 질문

mass를 올렸는데도 낙하 속도가 거의 같은 이유가 뭔가?

Unity 물리에서 중력은 ‘힘’이 아니라 ‘가속도 항’으로 취급되는 쪽에 가깝다. 뉴턴식으로 써도 F=m*g, a=F/m이라 a=g로 상쇄된다. 그래서 질량을 올려도 자유낙하 시간은 거의 유지된다. 대신 충돌 해결에서 임펄스 계산은 역질량(inverse mass)을 직접 쓰므로, 다른 Rigidbody를 얼마나 밀어내는지, 접촉에서 얼마나 안정적인지가 달라진다. 다음 학습 키워드는 PhysX impulse resolution, inverse mass, solver iteration이다.

drag를 0에서 3으로 올리면 왜 ‘종단속도’처럼 느껴지나?

drag는 속도에 비례해 감쇠를 거는 선형 감쇠(linear damping)다. 물리 스텝마다 속도를 적분한 뒤 감쇠를 적용하니, 시간이 지날수록 중력으로 늘어나는 속도와 감쇠로 줄어드는 속도가 균형을 이루는 지점이 생긴다. 그 지점이 체감상 종단속도다. 실제 공기저항은 v^2 항이 섞이지만 게임에서는 선형 감쇠만으로도 충분히 ‘느낌’이 만들어진다. 다음 학습 키워드는 damping model, terminal velocity, fixed timestep integration이다.

Update와 FixedUpdate가 나뉜 이유가 낙하 감에 어떤 영향을 주나?

Update는 렌더 프레임 기준이라 dt가 매 프레임 달라지고, FixedUpdate는 고정 dt로 여러 번 실행될 수 있다. 물리 적분은 고정 dt에서 안정적이어서, 같은 입력/같은 초기조건이면 결과가 덜 흔들린다. 낙하 감이 프레임레이트에 따라 달라지는 현상은 대개 Update에서 물리 상태를 바꾸거나, Update에서 측정/카메라를 섞어서 생긴다. 물리 관련 힘/속도 변경은 FixedUpdate, 카메라 추적은 LateUpdate로 분리하면 재현성이 올라간다. 다음 학습 키워드는 Unity Player Loop, accumulator, interpolation이다.

Rigidbody.useGravity를 끄고 직접 내려도 되나?

가능하지만 ‘물리와 합의된 방식’으로 내려야 한다. Transform.position을 Update에서 내리면 네이티브 물리 상태와 충돌 해결이 불안정해진다. useGravity를 끄고도 Rigidbody를 유지하려면 FixedUpdate에서 AddForce(Acceleration)로 원하는 가속도를 주거나, MovePosition으로 목표 위치를 이동시키는 편이 낫다. 또한 충돌이 중요한 오브젝트라면 Collision Detection 모드를 속도에 맞게 올리고(Continuous), 터널링이 없는지 확인해야 한다. 다음 학습 키워드는 kinematic vs dynamic, MovePosition, continuous collision detection이다.

GetComponent를 캐싱하면 왜 빨라지나? GC Alloc도 줄어드나?

GetComponent는 컴포넌트 배열을 C#에서 단순 조회하는 게 아니라, 네이티브 쪽 GameObject/Component 컨테이너를 조회하고 결과를 C# 래퍼로 연결하는 경로를 탄다. 이 호출이 Update/FixedUpdate에서 반복되면 호출 오버헤드가 누적된다. 캐싱은 이 조회를 1회로 고정한다. 보통 GetComponent 자체가 매번 GC Alloc을 크게 만들진 않지만(버전에 따라 다름), 반복 호출로 인한 CPU 시간이 먼저 문제 된다. Profiler에서 Scripts 샘플을 열어 GetComponent가 핫스팟인지 확인하는 게 확실하다. 다음 학습 키워드는 internal call, native binding, profiler timeline이다.

낙하가 빠를 때 바닥을 뚫고 지나간다. mass나 drag 문제인가?

대부분은 충돌 검출 방식과 스텝 크기 문제다. Discrete 충돌은 한 스텝 전/후 위치로만 충돌을 판단하니, 한 스텝에서 collider를 통과할 만큼 빠르면 놓친다(터널링). 해결은 Rigidbody의 Collision Detection을 Continuous(또는 Continuous Dynamic)로 올리고, 너무 빠른 낙하를 의도했다면 최대 낙하 속도 클램프나 fixedDeltaTime 조정 같은 안전장치를 둔다. mass/drag는 보조적이고, 핵심은 ‘한 물리 스텝에서 이동하는 거리’다. 다음 학습 키워드는 tunneling, CCD, fixed timestep stability이다.

Inspector에서 값을 바꾸는데 Play 중에만 적용되거나, 정지하면 원복되는 이유가 뭔가?

Play 모드에서 변경한 값은 런타임 인스턴스에 적용되고, 정지하면 씬 에셋 상태로 돌아간다. Rigidbody 같은 컴포넌트는 네이티브 상태도 함께 바뀌는데, Play 종료 시 엔진이 씬을 리로드하면서 직렬화된 값으로 다시 생성한다. 그래서 튜닝 값은 ScriptableObject나 프리팹에 저장하거나, OnValidate로 에디터 상태에서 즉시 반영되게 만들어야 한다. 다음 학습 키워드는 serialization, domain reload, prefab overrides이다.

관련 글

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

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

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

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

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

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

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

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

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