LYSC
Development Insight

새로운 도전: 3D 레이싱 게임 프로토타입 제작기

2021.02.14

2D 플랫포머를 넘어 3D 환경에서의 물리 엔진을 다루는 새로운 프로젝트의 첫 삽을 뜨며 겪은 축 설정 및 충돌 처리 이슈들.

1. 2D에서 3D로의 전환: 차원의 벽을 넘다

그동안 2D 플랫포머 게임 개발에 집중해왔던 저에게 3D 레이싱 게임은 완전히 다른 차원의 도전이었습니다. 2D에서는 X축과 Y축만 고려하면 되었지만, 3D에서는 Z축이 추가됨에 따라 공간을 인지하고 물리 로직을 구성하는 방식이 근본적으로 변해야 했습니다. 특히 레이싱 게임의 핵심인 '속도감'과 '접지력'을 구현하기 위해 유니티(Unity)의 내장 물리 엔진인 PhysX를 깊게 파고들기 시작했습니다.

2. 레이캐스트 기반 서스펜션 시스템의 설계

유니티에서 제공하는 기본 `WheelCollider`는 매우 강력하지만, 프로토타입 단계에서 세밀한 제어와 학습을 위해 직접 레이캐스트(Raycast) 기반의 서스펜션 시스템을 구현해보기로 했습니다. 차량의 각 바퀴 위치에서 아래 방향으로 레이를 쏘아 지면과의 거리를 측정하고, 이를 바탕으로 후크의 법칙(Hooke's Law)을 적용해 서스펜션 힘을 계산하는 방식입니다.

// 간단한 레이캐스트 서스펜션 로직 예시
void FixedUpdate() {
    foreach (var wheel in wheels) {
        RaycastHit hit;
        if (Physics.Raycast(wheel.position, -wheel.up, out hit, suspensionRestLength)) {
            // 서스펜션 압축 정도 계산
            float offset = suspensionRestLength - hit.distance;
            
            // 후크의 법칙: F = k * x (k: 스프링 상수, x: 변위)
            float springForce = offset * springStiffness;
            
            // 댐핑(Damping) 계산: 진동을 억제하기 위함
            float vel = Vector3.Dot(wheel.up, rb.GetPointVelocity(wheel.position));
            float dampForce = vel * damperStiffness;
            
            float totalForce = springForce - dampForce;
            rb.AddForceAtPosition(wheel.up * totalForce, wheel.position);
        }
    }
}

이 방식을 사용하면 서스펜션의 부드러움이나 반발력을 매우 정밀하게 조절할 수 있습니다. 하지만 문제는 지면이 평평하지 않을 때 발생했습니다. 레이캐스트가 지면의 법선(Normal) 벡터를 제대로 계산하지 못하면 차량이 공중에 뜨거나 땅에 박히는 현상이 발생했는데, 이를 해결하기 위해 레이캐스트의 결과값인 `hit.normal`을 활용해 타이어의 접지 평면을 실시간으로 업데이트하는 로직을 추가했습니다.

3. Blender와 Unity의 축 설정 잔혹사

3D 개발 초보자가 가장 많이 겪는 문제 중 하나가 바로 모델링 툴과 게임 엔진 간의 축(Axis) 불일치입니다. 블렌더(Blender)는 Z축이 위(Up)를 향하는 오른손 좌표계를 사용하는 반면, 유니티는 Y축이 위를 향하는 왼손 좌표계를 사용합니다. 이로 인해 모델을 임포트했을 때 차량이 옆으로 누워 있거나 앞뒤가 바뀌는 문제가 빈번했습니다.

특히 레이싱 게임에서는 전진 방향(Forward)이 매우 중요한데, 블렌더에서 -Z 방향을 앞으로 두고 작업한 뒤 유니티로 가져왔을 때 `transform.forward`가 예상과 다르게 작동하여 물리 계산이 엉망이 되곤 했습니다. 이를 해결하기 위해 블렌더의 Export 설정에서 'Apply Transform' 옵션을 체크하거나, 유니티 내부에서 빈 오브젝트(Parent)를 생성해 축을 보정하는 방식을 채택했습니다.

4. 마찰력과 핸들링: 슬립(Slip)의 미학

차량이 단순히 궤도를 따라가는 것이 아니라, 속도에 따라 미끄러지고 관성에 의해 밀려나는 느낌을 주는 것이 레이싱 게임의 묘미입니다. 저는 이를 위해 타이어의 가로 방향(Lateral) 속도를 측정하고, 그에 반하는 힘을 가해주는 방식으로 핸들링을 구현했습니다. 속도가 너무 빠르면 반대 방향의 힘이 한계를 초과하여 차가 미끄러지도록 설계했습니다.

// 가로 방향 마찰력(Side Friction) 적용 예시
Vector3 steeringDir = wheel.right;
Vector3 tireVel = rb.GetPointVelocity(wheel.position);

float steeringVel = Vector3.Dot(steeringDir, tireVel);
float desiredVelChange = -steeringVel * gripFactor;
float desiredAccel = desiredVelChange / Time.fixedDeltaTime;

rb.AddForceAtPosition(steeringDir * tireMass * desiredAccel, wheel.position);

이 수식에서 `gripFactor`를 조절함으로써 드리프트가 쉬운 아케이드 스타일의 조작감과 묵직한 시뮬레이션 스타일의 조작감을 넘나들 수 있었습니다. 수많은 테스트 끝에 제가 원하는 '약간의 미끄러짐이 허용되는 쫀득한 핸들링'을 찾아냈을 때의 쾌감은 이루 말할 수 없었습니다.

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

프로젝트의 성공은 기술력뿐만 아니라 팀 내 원활한 커뮤니케이션과 체계적인 파이프라인 구축에 달려 있습니다. 자동화된 빌드 시스템과 코드 리뷰 프로세스는 개발 속도를 비약적으로 높여줍니다. 1인 개발일지라도 스스로의 작업 규칙을 명확히 하는 것이 중요합니다.

기술적 구현의 디테일

저는 이번 개발 과정에서 모든 기능을 모듈화하여 독립적으로 테스트할 수 있는 환경을 구축했습니다. 이는 추후 기능 확장이나 버그 수정 시 발생할 수 있는 사이드 이펙트를 최소화하는 데 큰 역할을 했습니다. 또한 문서화를 병행하여 기술 부채가 쌓이는 것을 방지했습니다.

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

협업 툴 및 자동화 시스템 도입 이후 작업 히스토리 추적 시간이 50% 단축되었으며, 휴먼 에러로 인한 빌드 실패율이 눈에 띄게 줄어들었습니다. 이는 전체적인 개발 사이클을 20% 이상 단축시키는 결과를 가져왔습니다.

실무 적용 시 주의사항

완벽한 설계를 추구하기보다 빠르게 프로토타입을 만들고 피드백을 수용하는 애자일(Agile)한 자세가 특히 중요합니다. 기술에 매몰되기보다 유저가 실제로 느끼는 가치에 집중하는 균형 잡힌 시각을 유지하세요.

Drag to Rotate Cube

5. 결론 및 향후 계획

이번 프로토타입 제작을 통해 3D 물리 시스템의 기초를 탄탄히 다질 수 있었습니다. 단순히 컴포넌트를 붙이는 수준을 넘어, 물리 법칙을 코드로 직접 구현해보며 엔진의 작동 원리를 깊이 이해하게 된 값진 시간이었습니다. 다음 단계에서는 멀티플레이어 환경에서의 동기화 문제와 더 정교한 트랙 레이아웃 설계를 진행해볼 예정입니다.

작성자 프로필

LYSC Studio

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