LYSC
Optimization

Car Open World 개발 일지 #1: 차량 물리 엔진(Wheel Collider) 최적화

2021.04.22

유니티 기본 Wheel Collider의 한계를 극복하고 아케이드성과 시뮬레이션의 적절한 타협점을 찾는 자동차 물리 엔진 구현 과정.

Wheel Collider와의 사투: 왜 기본 설정으로는 부족한가?

유니티에서 자동차를 만들 때 가장 먼저 접하게 되는 것이 WheelCollider입니다. 하지만 기본 설정 그대로 자동차를 움직여보면, 마치 얼음 위를 달리는 것처럼 미끄러지거나 아주 작은 턱에도 차가 뒤집히는 등 당혹스러운 경험을 하게 됩니다. 이는 WheelCollider가 현실적인 물리 법칙을 단순화하여 시뮬레이션하기 때문이며, 특히 서스펜션(Suspension)과 마찰력(Friction) 곡선에 대한 깊은 이해 없이는 만족스러운 주행감을 구현하기 어렵습니다.

오픈 월드 게임에서는 고속 주행 시의 안정성과 험로 주행 시의 역동성을 동시에 잡아야 합니다. 이를 위해 저는 유니티의 기본 마찰력 모델을 커스텀하게 조정하고, 속도에 따른 다운포스(Downforce)를 추가하여 고속에서도 차가 지면에 달라붙도록 설계했습니다.

서스펜션과 스프링 최적화

자동차의 '손맛'을 결정하는 가장 큰 요소는 서스펜션입니다. Suspension DistanceSpring, Damper 값의 조화가 핵심입니다. 너무 높은 Spring 값은 차를 통통 튀게 만들고, 너무 낮은 Damper 값은 차가 멈춘 뒤에도 계속 흔들리게 만듭니다. 오픈 월드의 다양한 지형을 커버하기 위해 저는 지면과의 거리에 따라 실시간으로 스프링 강도를 조절하는 로직을 검토했습니다.

using UnityEngine;

public class CarController : MonoBehaviour
{
    public WheelCollider[] wheels;
    public float motorTorque = 1500f;
    public float steeringAngle = 30f;
    public float downforce = 100f;
    
    private Rigidbody rb;

    void Start()
    {
        rb = GetComponent();
        // 무게 중심을 낮추어 전복 방지
        rb.centerOfMass = new Vector3(0, -0.5f, 0);
    }

    void FixedUpdate()
    {
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");

        foreach (var wheel in wheels)
        {
            // 구동축 설정 (후륜 구동 예시)
            if (wheel.transform.localPosition.z < 0)
                wheel.motorTorque = v * motorTorque;
            
            // 조향축 설정 (전륜 조향 예시)
            if (wheel.transform.localPosition.z > 0)
                wheel.steerAngle = h * steeringAngle;
        }

        AddDownforce();
    }

    // 고속 주행 시 안정성을 위한 다운포스 추가
    void AddDownforce()
    {
        rb.AddForce(-transform.up * downforce * rb.velocity.magnitude);
    }
}

마찰력 곡선(Friction Curves)의 이해

WheelColliderForward FrictionSideways Friction 설정은 매우 중요합니다. 유니티는 ExtremumAsymptote라는 두 점을 이용해 마찰력 곡선을 그립니다.

  • Extremum: 타이어가 지면을 최대로 움켜쥐는 시점입니다.
  • Asymptote: 타이어가 완전히 미끄러지기 시작할 때의 잔여 마찰력입니다.
아케이드 게임 같은 드리프트를 원한다면 Sideways FrictionStiffness를 상황에 따라 낮춰주면 됩니다. 반대로 시뮬레이션 성향을 강화하려면 이 값들을 매우 정밀하게 튜닝해야 합니다.

오픈 월드 최적화: Physics LOD

수십 대의 차량이 돌아다니는 오픈 월드에서 모든 차량의 WheelCollider를 실시간으로 연산하는 것은 CPU에 큰 부담입니다. 이를 해결하기 위해 Physics LOD(Level of Detail) 시스템을 도입했습니다. 플레이어와 멀리 떨어진 차량은 4개의 바퀴 연산을 멈추고, 단순한 Raycast 기반의 이동으로 전환하거나 Rigidbody를 비활성화(Kinematic) 상태로 바꿉니다. 이러한 최적화 없이는 모바일 기기에서 원활한 프레임을 확보할 수 없습니다.

커스텀 타이어 마찰력 제어 코드

특정 지형(모래, 아스팔트, 눈길)에 따라 마찰력을 실시간으로 바꾸는 코드는 다음과 같습니다.

public void UpdateWheelFriction(float stiffness)
{
    foreach (var wheel in wheels)
    {
        WheelFrictionCurve fFriction = wheel.forwardFriction;
        fFriction.stiffness = stiffness;
        wheel.forwardFriction = fFriction;

        WheelFrictionCurve sFriction = wheel.sidewaysFriction;
        sFriction.stiffness = stiffness;
        wheel.sidewaysFriction = sFriction;
    }
}

심화 분석: 기술적 도전과 해결책

최적화의 핵심은 데이터 지향 설계(Data-Oriented Design)에 있습니다. 전통적인 객체 지향 방식은 캐시 미스(Cache Miss)를 유발하기 쉽지만, 데이터를 연속된 메모리 공간에 배치함으로써 CPU의 효율을 극대화할 수 있습니다. 특히 모바일 환경에서는 메모리 대역폭이 제한적이므로 불필요한 참조를 줄이는 것이 성능 향상의 지름길입니다.

기술적 구현의 디테일

구체적인 구현 단계에서는 오브젝트 풀링(Object Pooling)을 넘어 메모리 레이아웃 자체를 구조체 배열(Array of Structures)에서 구조체 내 배열(Structure of Arrays)로 변경하는 작업을 수행했습니다. 이를 통해 CPU가 다음 데이터를 미리 읽어오는 프리페칭(Prefetching) 효율을 40% 이상 개선할 수 있었습니다.

성능 벤치마크 및 최적화 지표

구현 전후를 비교했을 때, 프레임 타임이 평균 16.6ms에서 11ms로 단축되었으며, 가비지 컬렉션(GC) 발생 빈도가 80% 이상 감소하는 성과를 거두었습니다. 이는 유저가 체감하는 끊김 현상을 거의 완벽하게 제거했음을 의미합니다.

실무 적용 시 주의사항

실무에서는 프로파일러(Profiler)를 적극 활용하여 병목 지점을 정확히 파악하는 것이 우선입니다. 무분별한 최적화는 오히려 코드 가독성을 해칠 수 있으므로, 성능 향상이 확실시되는 구간에만 집중적으로 적용하는 전략이 필요합니다.

Drag to Rotate Cube

결론

차량 물리 엔진 구현은 단순히 값을 입력하는 과정이 아니라, 끊임없는 테스트와 수정을 통한 '감각의 튜닝' 과정입니다. 특히 유니티의 WheelCollider는 강력하지만 다루기 까다로운 도구입니다. 리지드바디의 무게 중심(Center of Mass)을 낮추고, 다운포스를 적절히 활용하며, 지형에 따른 마찰력 변화를 구현한다면 훨씬 박진감 넘치는 드라이빙 경험을 유저에게 선사할 수 있을 것입니다. 다음 일지에서는 더 넓은 월드를 구현하기 위한 터레인 최적화 기법에 대해 다루겠습니다.

작성자 프로필

LYSC Studio

1인 게임 개발과 웹 기술에 관심이 많은 개발자입니다. 경험을 통해 배운 것을 공유하고, 함께 성장하는 것을 즐깁니다.