점프가 포물선이 되는 이유
Esc

    점프가 포물선이 되는 이유

    –Unity기초

    CharacterController 로 점프를 만들려고 점프 키를 누르면 y 를 2 만큼 올리고, 매 프레임 조금씩 내렸더니 캐릭터가 순간이동하듯 올라갔다가 엘리베이터처럼 일정하게 내려옵니다. 진짜 점프는 빠르게 솟았다가 꼭대기에서 잠깐 멈칫하고, 점점 빨라지며 떨어집니다.

    중력이 위치가 아니라 속도를 바꾸기 때문입니다. 점프 키는 위로 향한 속도를 한 번 주고, 중력은 매 프레임 그 속도를 조금씩 아래로 바꿉니다. 속도가 일정하게 줄어드니, 위치는 처음엔 많이 오르다가 점점 덜 오르고, 꼭대기를 지나면 점점 더 많이 떨어집니다. 그렇게 그려지는 곡선이 포물선입니다.

    매 프레임 속도 += 중력 × 시간, 위치 += 속도 × 시간 두 줄이면 포물선이 나옵니다. 이 계산은 한 번에 맡는 시간(계산 간격)이 크면 오차가 커져서, fps 가 다르면 점프 높이도 조금씩 달라집니다. 원하는 높이 h 로 뛰게 하려면 처음 속도를 √(2 × 중력 × h) 로 줍니다.

    중력은 속도를 바꾸는 가속도다

    속도는 위치가 1초에 얼마나 바뀌나이고, 단위는 m/s(미터 매 초)입니다. 속도가 위로 8 이면 1초에 8m 오르고, 0.5초면 4m 오릅니다. 속도가 1초에 얼마나 바뀌는지는 가속도라 부르고, 단위는 m/s²(1초마다 m/s 가 얼마)입니다. 가속도가 아래로 10 이면 속도가 1초마다 10 씩 아래로 바뀝니다. 위로 8 이던 속도는 0.5초 뒤 3, 0.8초 뒤 0, 1초 뒤 −2(아래로 2) 가 됩니다.

    중력이 바로 이 가속도입니다. 지구에서는 아래로 약 9.81 이고, 유니티의 기본 중력 Physics.gravity 도 (0, −9.81, 0) 입니다. 아래 계산은 암산하기 쉽게 −10 으로 둡니다.

    속도와 가속도는 둘 다 방향이 있으니 벡터(방향과 길이를 가진 화살표, 유니티에서는 Vector3)로 씁니다. 게임은 시간을 프레임(화면에 그리는 그림 한 장) 단위로 끊어서 계산합니다. 계산 한 번이 맡는 시간을 dt(델타 타임)라고 부르고, Update 안이면 지난 프레임이 걸린 초인 Time.deltaTime 이 그것입니다. dt 초가 지날 때마다 이렇게 바꿉니다.

    속도 = 속도 + 가속도 × dt
    위치 = 위치 + 속도 × dt

    0.2초씩 계산하면

    처음 속도 (2, 8) 로 뛰었다고 합니다. 가로로 1초에 2, 위로 1초에 8 입니다. 중력은 (0, −10), 계산 간격 dt 는 눈으로 따라가기 쉽게 0.2초로 크게 잡습니다. 가로 속도는 아무것도 바꾸지 않으니 계속 2 이고, 세로 속도만 0.2초마다 10 × 0.2 = 2 씩 줄어듭니다. 속도를 먼저 바꾸고, 바뀐 속도로 위치를 옮깁니다.

    시각 속도 y 이번에 오른 양 (속도 y × 0.2) 높이
    0 8 0
    0.2 6 1.2 1.2
    0.4 4 0.8 2.0
    0.6 2 0.4 2.4
    0.8 0 0 2.4
    1.0 −2 −0.4 2.0
    1.2 −4 −0.8 1.2
    1.4 −6 −1.2 0

    오르는 양이 1.2, 0.8, 0.4, 0 으로 일정하게 줄고, 내려오는 양이 0.4, 0.8, 1.2 로 일정하게 늘어납니다. 속도가 일정하게 바뀌면 위치는 이렇게 휘어집니다.

    포물선과 t²: 잘게 쪼개면 나오는 정확한 식

    포물선은 공을 비스듬히 던졌을 때 그리는 곡선입니다. 꼭대기를 가운데에 두고 왼쪽과 오른쪽이 같은 모양으로 휘어 있습니다. 계산 간격을 아주 잘게 쪼개면 높이가 어떤 식이 되는지, 평균 속도로 구할 수 있습니다.

    속도가 일정하게 바뀔 때는 그동안의 평균 속도가 처음 속도와 나중 속도의 한가운데입니다. 8 에서 0 으로 고르게 줄었다면 평균은 4 입니다. 처음 0.8초 동안 속도는 8 에서 0 으로 줄었으니 평균 4 로 0.8초를 간 셈이고, 오른 높이는 4 × 0.8 = 3.2 입니다. 이것이 정확한 꼭대기입니다.

    아무 시각 t 초에 대해서도 같은 방법이 됩니다.

    단계 계산
    t 초 뒤 속도 8 − 10t
    그동안의 평균 속도 (8 + 8 − 10t) ÷ 2 = 8 − 5t
    t 초 동안 오른 높이 = 평균 속도 × t (8 − 5t) × t = 8t − 5t²

    t² 은 t × t 입니다(같은 수를 두 번 곱한 것, “t 의 제곱”). 이 식이 물리 교과서의 높이 = 처음 속도 × t − ½ × 중력 × t² 입니다. 5 가 ½ × 10 입니다.

    t 8t 5t² 높이
    0.2 1.6 0.2 1.4
    0.4 3.2 0.8 2.4
    0.8 6.4 3.2 3.2 (꼭대기)
    1.6 12.8 12.8 0 (바닥)

    8t 는 시간에 비례해 곧게 늘어나지만 5t² 은 시간이 2배면 4배로 늘어납니다. 처음엔 8t 가 이겨서 오르다가, 나중엔 5t² 이 따라잡아 끌어내립니다. 이렇게 t² 이 들어간 식의 그래프가 포물선입니다.

    계산이 정확한 값과 어긋나는 이유

    0.2초씩 계산한 꼭대기는 2.4 였고, 정확한 꼭대기는 3.2 입니다. 0.8 이 모자랍니다. 원인은 0.2초 동안 속도가 바뀌는데, 계산은 그 0.2초 내내 한 가지 속도만 쓴다는 데 있습니다. 첫 0.2초 동안 실제 속도는 8 에서 6 으로 서서히 줄어 평균 7 이지만, 계산은 줄어든 뒤의 6 으로 0.2초를 다 간 것으로 칩니다. 그래서 매번 조금씩 덜 오릅니다. 간격을 줄이면 오차도 줄어듭니다.

    계산 간격 꼭대기 높이 정확한 값과의 차이
    0.2초 2.4 −0.8
    0.05초 3.0 −0.2
    아주 잘게 3.2 0

    이렇게 짧은 간격마다 “바뀌는 양 × 간격” 을 더해 모아 가는 계산을 수치 적분이라고 부릅니다. 적분은 “잘게 나눠 더해 모은다” 는 뜻입니다. 위의 가장 단순한 방식은 수학자 오일러의 이름을 따 오일러 방법이라고 합니다.

    두 줄의 순서도 결과를 바꿉니다. 위치를 먼저 옮기면 줄기 전의 속도를 쓰게 됩니다.

    순서 이름 첫 0.2초에 쓰는 속도 0.2초 간격 꼭대기
    속도 먼저, 새 속도로 위치 반암시적(semi-implicit) 오일러 6 2.4 (0.8 낮음)
    위치 먼저, 옛 속도로 위치 명시적(explicit) 오일러 8 4.0 (0.8 높음)

    점프에서는 둘 다 0.8 씩 어긋나지만, 용수철처럼 되돌아오는 힘이 있으면 차이가 커집니다. 명시적 오일러는 흔들림이 갈수록 커지며 에너지가 불어나 터지고, 반암시적 오일러는 오래 돌려도 흔들림의 크기를 대체로 지킵니다. 그래서 게임 물리는 보통 속도를 먼저 바꾸고 그 속도로 위치를 옮기는 순서를 씁니다. Glenn Fiedler 의 「Integration Basics」 도 대부분의 상용 게임 물리 엔진이 이 방식을 쓴다고 정리합니다.

    이 오차는 게임에서 그대로 드러납니다. 아래 코드로 2m 점프를 만들면 꼭대기가 144fps 에서 약 1.98m, 60fps 에서 1.95m, 30fps 에서 1.90m 입니다. 프레임 시간이 긴 기계일수록 간격이 큰 계산이 되어 점프가 낮아집니다. 유니티의 물리 엔진이 화면 프레임과 따로 늘 같은 간격(기본 0.02초)으로 도는 이유 중 하나가 이것입니다. 간격이 같으면 기계가 달라도 같은 높이로 뜁니다.

    CharacterController 로 점프하기

    CharacterController 는 벽과 바닥에 막히며 움직이는 기능만 있고 중력은 없어서, 위의 두 줄을 직접 씁니다.

    public float gravity = -9.81f;
    public float jumpHeight = 2f;
    
    CharacterController controller;
    Vector3 velocity;   // 속도는 프레임을 넘어 쌓여야 하니 필드로 둔다
    
    void Start()
    {
        controller = GetComponent<CharacterController>();
    }
    
    void Update()
    {
        if (controller.isGrounded && velocity.y < 0f)
        {
            velocity.y = -1f;   // 땅에 있으면 떨어지는 속도가 끝없이 쌓이지 않게 작게 눌러 둔다
        }
    
        if (controller.isGrounded && Input.GetButtonDown("Jump"))
        {
            // 원하는 높이에 닿는 처음 속도. 다음 절의 식
            velocity.y = Mathf.Sqrt(2f * -gravity * jumpHeight);
        }
    
        velocity.y += gravity * Time.deltaTime;       // 속도 += 가속도 × dt
        controller.Move(velocity * Time.deltaTime);   // 위치 += 속도 × dt
    }

    점프 키는 속도를 한 번 바꿀 뿐이고, 그 뒤의 오르내림은 전부 중력이 속도를 바꾼 결과입니다. Rigidbody 를 쓰면 물리 엔진이 이 두 줄을 0.02초 간격으로 대신 계산합니다. 그때도 점프는 위치가 아니라 속도를 넣는 것이라, 위로 향한 속도를 같은 식으로 줍니다.

    원하는 높이로 뛰게 하는 처음 속도

    “2m 높이로 뛰게 하고 싶다” 가 보통의 요구입니다. 처음 속도를 몇으로 줘야 할까요. 꼭대기에서 속도가 0 이 된다는 것과 평균 속도를 씁니다.

    단계 식 v = 8, g = 10 일 때
    위로 v 로 출발해 1초에 g 씩 줄면, 0 이 될 때까지 시간 = v ÷ g 8 ÷ 10 = 0.8초
    그동안 평균 속도 (v 와 0 의 한가운데) v ÷ 2 4
    오른 높이 = 평균 속도 × 시간 (v ÷ 2) × (v ÷ g) = v² ÷ (2g) 4 × 0.8 = 3.2

    높이 h = v² ÷ (2g) 를 거꾸로 풀면 v² = 2 × g × h 입니다. v 는 “두 번 곱해서 2gh 가 되는 수” 이고, 이것을 제곱근이라 부르며 √ 로 씁니다. √64 = 8 입니다(8 × 8 = 64). 코드에서는 Mathf.Sqrt 입니다.

    처음 속도 v = √(2 × g × 높이)
    원하는 높이 중력 10 일 때 처음 속도 확인: v² ÷ 20
    3.2 √64 = 8 64 ÷ 20 = 3.2
    2 √40 ≈ 6.32 40 ÷ 20 = 2
    0.8 √16 = 4 16 ÷ 20 = 0.8

    이 식으로 처음 속도를 정하면 중력을 바꿔도 점프 높이는 그대로입니다. 중력을 키우면 같은 높이를 더 빨리 오르내려 묵직해지고, 줄이면 둥실 뜹니다. 높이와 느낌을 따로 조절할 수 있습니다.

    정리

    • 속도는 위치가 1초에 바뀌는 양, 가속도는 속도가 1초에 바뀌는 양입니다. 중력은 아래로 향한 가속도(유니티 기본 9.81)입니다
    • 중력은 위치가 아니라 속도를 바꿉니다. 매 프레임 속도 += 중력 × dt, 위치 += 속도 × dt 입니다
    • 속도가 일정하게 바뀌면 높이는 처음 속도 × t − ½ × 중력 × t² 이 되어 포물선을 그립니다. 평균 속도 × 시간으로 구할 수 있습니다
    • 계산 간격 동안 속도를 하나로 고정해서 오차가 생깁니다. 간격이 크면 꼭대기가 낮아지고, 그래서 fps 가 다르면 점프 높이도 다릅니다
    • 속도를 먼저 바꾸고 그 속도로 위치를 옮기는 반암시적 오일러가 게임 물리의 기본 순서입니다
    • 원하는 높이 h 로 뛰려면 처음 속도를 √(2 × g × h) 로 줍니다