프로토타입 지형 생성 로직을 상용 수준으로 리팩토링
재미 검증용으로 작성된 프로토타입 코드의 구조적 한계를 파악하고 병목 원인을 분석하여 상용 수준의 퍼포먼스를 확보하였습니다.
문제 상황
테스트 기기 환경에서 거대한 레벨을 생성할 경우, 300초 이상의 긴 시간이 소모되는 현상이 발생하였습니다.
원인 분석
Profiler를 사용하여 분석한 결과, 심각한 GC 오버헤드와 비효율적인 연산이 핵심 원인으로 파악되었습니다.
- GC 오버헤드
- class 기반 좌표 클래스의 연산자 오버로딩으로 인해 매 연산마다 발생하는 힙 할당
- ToList, ToDictionary 등의 잦은 LINQ 호출로 인한 리사이징과 박싱
- 잦은 Coroutine 호출
- 비효율적 알고리즘
- 오버랩 체크 시 타일 개수만큼 객체를 생성하거나 전체를 순회하는 $O(N^2)$ 로직
- 동일한 결과를 반환하는 조건 연산의 중첩 사용
해결 과정
1. GC 오버헤드 제거
좌표 클래스를 구조체로 변경하는 동시에 지형 정보를 담고 있던 string 등 참조 타입의 변수를 제거하여 GC.Alloc이 완전히 일어나지 않도록 하였습니다.
또한 LINQ 사용으로 인해 발생하는 문제점을 해결하기 위해 아래와 같이 수정하였습니다.
- 콜렉션을 미리 생성함과 동시에 Capacity를 계산하여, 함수 내에서의 생성과 Resizing을 최소화하였습니다.
- Memory Profiler를 통해 예약된 메모리와 콜렉션의 최종 크기가 동일함을 검증했습니다.
- 쿼리 함수를 for/while를 사용한 순회로 변경하여 새로운 콜렉션을 생성하지 않도록 하였습니다.
- List 순회 대신 HashSet과 Dictionary을 사용하여 평균 데이터 접근 속도를 개선하였습니다
마지막으로 코루틴은 컴파일 단계에서 클래스로 선언되어 작동하기 때문에 GC.Alloc이 일어나게 되므로,
이를 방지하기 위해 구조체 기반의 UniTask를 도입하였습니다.
2. 청크 시스템 적용
청크 시스템의 Spatial Hash Grid를 적용하여 좌표 단위로 실행하는 동작을 영역 단위로 실행하도록 수정하였습니다.
오버랩 체크, 영역 회전, 확장 등 관련 함수의 호출 횟수와 평균 데이터 접근 속도를 개선하였습니다.
3. CPU Friendly
개선된 구조에서도 일부 함수는 최악의 경우 수십만번 이상 호출되어 실행 시간에 영향을 주는 것을 확인하였습니다.
더 이상 호출 횟수를 줄이는 것은 어려웠기 때문에, 함수 자체의 동작 시간을 줄이기 위해 아래와 같은 방법을 통해 CPU의 처리 시간 감소를 도모하였습니다.
- 좌표 구조체의 크기를 조절하여 메모리 접근 횟수를 줄였습니다.
- 불필요한 중간 객체를 제거하고 함수 파라미터에 수식을 직접 넣어 전달하였습니다.
- 연산자 오버로딩을 제거하고 멤버의 값을 수정하는 연산 함수로 변경하였습니다.
결과
최악의 케이스(모든 청크 검사 실패, 기획상 최대 지형 크기 적용 등)를 가정한 테스트 환경에서,
평균 생성 시간이 380초 → 14초로 단축되었습니다.
또한 핵심 생성 로직에서 필수적인 할당을 제외한 Zero Allocation을 달성하여, GC Spike 현상을 방지할 수 있었습니다.
수정 불가능한 씬 환경에서 NavMesh 데이터를 통한 물리 주행 구현
NavMesh를 사용하는 월드에서 작동하는 WheelCollider를 이용한 주행 기능을 구현하였습니다.
문제 상황
개발 도중 플레이어의 이동 방식이 NavMeshAgent에서, 물리가 적용된 WheelCollider로 변경되었습니다.
하지만 기존 스테이지는 NavMesh 규격으로 이미 제작되어 수정이 불가능한 상황이었으며,
단순히 NavMeshAgent를 사용할 경우 물리적 상호작용(충돌, 서스펜션 등)을 구현하는 데 한계가 있었습니다.
따라서 기존 NavMesh 데이터를 활용하면서도, 물리 엔진 기반의 주행이 가능한 시스템을 새롭게 구축해야 했습니다.
해결 과정
1. NavMesh 데이터 추출
Unity AI Navigation Package의 Baking 함수를 추적하여, 베이킹 시 생성되는 NavMesh 폴리곤의 좌표 데이터를 추출하는 기능을 구현했습니다.
추출된 데이터를 별도의 ScriptableObject로 저장하여, NavMeshAsset 없이 런타임에 경로 데이터를 참조할 수 있도록 구조화했습니다.
2. 주행 경로 수정
NavMesh가 지원하는 경로 따라가기 함수 사용 시 폴리곤의 꼭짓점 부분을 이용하여 이동하기 때문에 시각적으로 부자연스러운 문제가 발생하였습니다.
이를 해결하기 위해 삼각형의 무게중심을 선형 보간한 경로를 사용하였으나, 삼각형 형태에 따라 불필요한 움직임이 발생했습니다.
최종적으로는 인접한 폴리곤이 맞닿는 모서리의 중점을 연결하여 경로를 수정하는 로직을 적용하여, 차량이 도로의 중앙을 따라 부드럽게 이동할 수 있도록 구현했습니다.
3. 장애물 회피 로직 구현
NavMeshAgent의 회피 기능을 대체하기 위해, Raycast를 사용하여 전방의 오브젝트를 감지하고 정지/후진하는 로직을 추가했습니다.
박스나 캡슐 콜라이더를 가진 고정 장애물 감지 시, 콜라이더의 좌표와 회전값을 분석하여 우회 경로를 생성합니다.
현재 속도를 기반으로 예상 도착 시간을 계산하고, 지연이 심할 경우 경로를 재탐색하도록 설계했습니다.
결과
기존 스테이지 리소스를 수정하지 않고도 WheelCollider 기반의 물리 주행 시스템을 성공적으로 도입했습니다.
자체적인 경로 보정 알고리즘을 적용하여 자연스러운 차량 주행감을 구현할 수 있었습니다.