핵심 역량 정리
팀의 생산성을 높이는 도구 개발부터, 게임의 성능을 책임지는 최적화까지 저의 핵심 역량을 소개합니다.

개발 편의 기능

개발 과정에서 발생하는 비효율적인 작업을 개선하고,
실무에 즉시 도입 가능한 기능을 구현하여 팀의 생산성을 끌어올릴 수 있습니다.

Attribute 기반의 자동 이벤트 바인딩 시스템

UIToolkit 환경에서, element class를 통해 이벤트를 자동으로 바인딩하는 기능을 구현하였습니다.
디자이너는 UI를 새로 만들거나 수정할 때, element class를 추가하면 즉시 기능을 적용할 수 있습니다.
프로그래머는 명시된 element class에 따라 Attribute를 선언하는 것으로, 추가적인 바인딩 작업을 진행하지 않아도 됩니다.

[ElementCommand("cmd--button--brush-tool")]
private static void SelectBrushTool(VisualElement target) => History.Log("브러쉬 도구를 선택하였음");

UQuery를 통해 가져온 VisualElement에 메서드를 등록하기 위해 Reflection을 사용하였으나,
초기화 시점에 단 한번 동작하여 UIBuilder 사용 시 성능 영향이 적도록 설계하였습니다.
추후 런타임 적용을 고려하여, Source Generator를 사용하는 방식으로 전환하더라도 복잡한 수정 과정 없이 사용할 수 있도록 설계하였습니다.
추가적인 메모리 할당을 방지할 수 있도록, 외부 변수 캡처 없이 static 메서드를 호출하는 static 람다 함수를 사용하는 방식으로 구현하였습니다.

CSV Utility

별도의 파싱 로직 작성 없이 CSV 데이터를 변환할 수 있는 유틸리티를 구현하였습니다.
이름 지정 방식과 열 인덱스 지정 방식을 지원하여 데이터 테이블의 헤더 수정에 유연하게 대응할 수 있도록 하였습니다.

public class Weapon
{
    // 열 지정 방식
    [CSVIndex(0)]
    public string ItemCode;

    // 이름 지정 방식
    [CSVHeader("wpn_author")]
    public string AuthorName;

    public List<string> EnchantList;
    public EquipSlotType SlotType; // enum
}

private UniTaskVoid Start()
{
    Dictionary<string, Weapon> resultDictionary = new();
    CSVUtility.TryFromText("text...", nameof(Weapon.ItemCode), out resultDictionary);

    string resultText = string.Empty;
    CSVUtility.TryToText(resultDictionary, out resultText);
}

기본 자료형(int, string) 외에도 사용자 정의 타입(클래스나 딕셔너리 리스트 같은 복합 자료형)을 처리하기 위해,
제네릭 기반의 추가 컨버터를 구현하여 사용할 수 있습니다.
동일한 타입이라도 상황에 따라 다른 파싱 규칙을 적용할 수 있습니다.

// 1. 사용자 정의 타입 처리를 위한 컨버터 구현
// CSVConverter<T>를 상속하여 구현
public class StatConverter : CSVConverter<StatInfo>
{
    // 입력값: "HP:100|MP:50"
    public override StatInfo Read(string value)
    {
        var stat = new StatInfo();
        var parts = value.Split('|'); // 구분자 정의
        stat.HP = int.Parse(parts[0].Split(':')[1]);
        stat.MP = int.Parse(parts[1].Split(':')[1]);
        return stat;
    }

    public override string Write(StatInfo value)
    {
        return $"HP:{value.HP}|MP:{value.MP}";
    }
}

// 2. 사용자 정의 타입 컨버터 사용
public class MonsterData
{    
    [CSVIndex(0)]
    public string ID;

    // Attribute로 컨버터를 주입
    [CSVConverter(typeof(StatConverter))]
    public StatInfo BaseStat; 
}

Naming Tool

지정된 네이밍 규칙에 따라 하이어라키와 프로젝트 에셋 파일명을 자동 변경해주는 에디터 기능을 구현하였습니다.
단순히 이름을 일괄 변경하는 것을 넘어, 에셋의 타입이나 컴포넌트 구성에 따라 우선순위를 지정하여
상황에 맞는 이름을 생성하도록 구현했습니다.

컴포넌트 기반 네이밍

게임 오브젝트에 부착된 컴포넌트를 분석하여 이름을 지정합니다.
단순히 컴포넌트명만 사용하는 것이 아니라, 내부 멤버 변수(참조 중인 메쉬, 재질 등)를 읽어와 이름에 반영할 수 있도록 구현하였습니다.

  • 예) MeshRenderer 감지 → MeshFilter의 메쉬 이름 + Material 이름 조합 자동 생성

넘버링 시스템

유니티 엔진이 중복된 이름에 대해 부여하는 기본 넘버링 패턴을 감지하고, 이를 프로젝트 규약인 포맷으로 보정하는 기능을 구현했습니다.
또한 계층 구조 내의 자식 객체들을 대상으로 넘버링을 적용하는 기능을 추가하여, 복잡한 하이어라키 뷰의 시각적인 정돈을 도모하였습니다.

에셋 타입 기반 네이밍

선택된 에셋의 타입(Texture, AudioClip, Material 등)을 식별하여, 사전에 정의된 접두사 규칙을 자동으로 적용합니다. 정규표현식을 통해 공백이나 특수문자 등 사전 정의한 규칙에 어긋나는 문자가 포함된 경우 자동으로 제거하거나 지정된 문자로 치환하는 유효성 검사 로직이 있습니다.

확장을 고려한 구조

네이밍 로직을 추상 클래스로 정의하고 각 타입별(Light, Texture 등)로 상속받아 구현했습니다.
새로운 컴포넌트나 에셋 타입이 추가되더라도, 기존 코드를 수정할 필요 없이 새로운 네이밍 클래스를 추가하는 것으로 대응 가능한 구조를 설계했습니다.

Unity 에디터 확장 기능

프로젝트 특성에 맞는 에디터 확장 기능을 구현하여,
팀 내에 보다 편안한 유니티 작업 환경을 제공할 수 있습니다.

레벨 에디팅 툴 및 베이킹 툴

주행 경로 에디터

원하는 경로로 차량이 이동할 수 있도록 자율 주행 경로를 제작할 수 있는 경로 제작 기능을 구현하였습니다.
경로 에디터 윈도우에 UIToolkit을 사용하여 노드의 추가 및 삭제 버튼, 선택된 노드의 정보를 표시하였으며,
씬 뷰에서 Gizmos와 Handles를 사용하여 각 노드의 위치와 각도를 수정할 수 있도록 하였습니다.

주행 경로 베이커

NavMeshAsset을 생성하는 대신 별도의 ScriptableObject를 통해 NavMesh 폴리곤 좌표와 커스텀한 주행 경로를 저장하는 베이킹 기능을 구현하였습니다.
기존의 Navigation 윈도우를 사용하는 대신 각 씬마다 설정이 필요한 정보만 노출하는 전용 에디터 윈도우를 구현하였습니다.

장비 아이템 착용 위치 에디터

차량의 모자 액세서리 착용 위치를 설정하는 기능을 구현하였습니다.
바디 쉘에 해당하는 메쉬에서 가장 높은 위치의 폴리곤을 탐색하여 기울기를 적용하여 오브젝트의 Hat Socket으로 설정합니다.
Gizmos를 통해 Hat Socket의 위치에 장착될 모자 메쉬를 표시하여 각 아이템이 정상 착용 되는지 확인하는 미리보기 기능을 구현하였습니다.

Addressables 시스템 확장

AssetReference 커스텀

프로젝트의 에셋 사용 방식에 따라, Address·Label을 사용하는 커스텀 AssetReference class를 구현하였습니다.
인스펙터에서 에셋을 할당하면 직접 참조하는 대신, 해당 에셋에 지정된 Address·Label을 참조하여
암묵적 종속성을 발생시키지 않는 구조를 설계하였습니다.
또한, 에셋의 타입에 따라 Texture, Material과 같은 의존성있는 하위 자산의 Address·Label를 포함하여 사용할 수 있는 기능을 구현하였습니다.

에셋 Lifecycle 관리

프로젝트의 씬 사용 방식에 따라 OperationHandle을 상시 상주하는 Global Scope와
스테이지 단위로 상주하는 Stage Scope로 나누어 관리하는 구조를 설계하였습니다.
종속된 에셋을 사용할 때 발생하는 에셋 번들의 로드/언로드로 인한 부하(Asset Churn)를 방지하기 위해,
IResourceLocation을 통해 에셋을 로드하여 번들 정보를 수집하고, OperationHandle의 재사용을 통해 참조 카운트를 통제하였습니다.

게임 로직 및 시스템

게임 플레이에 필요한 로직과 다양한 기능 요구사항을 구현할 수 있습니다.

절차적 레벨 생성

Project PZ

  • Wang Tile의 Edge Matching 규칙을 응용한 레벨 생성 로직 구현
  • 퀘스트나 아이템 상태에 따라 생성 규칙이 동적으로 변하는 시스템 설계

물리 기반 자율 주행 및 파츠 시스템

Project C

  • WheelCollider로 이동하는 물리 기반 자율 주행 AI 구현
  • 주행 중 실시간으로 파츠(바퀴, 차체) 교체 시, 무게 중심과 회전 반경을 재계산하여 주행 물리에 즉각 반영되는 시스템 구현

산업 데이터 기반 게이미피케이션

Project C

  • 실제 공장 작업자의 조립 데이터를 실시간으로 수신하여 게임 내 피버 타임, 스테이지 변화 등을 제어하는 자동화 플레이 로직 구현
  • 데이터 송수신과 게임 로직을 분리하여 네트워크 지연 시에도 게임 흐름이 끊기지 않도록 설계

몬스터 및 보스 패턴 시스템

  • 데이터 테이블을 통해 상태와 전이 조건을 정의하는 몬스터 패턴 시스템 구현 (Project C)
  • StateMachineBehaviour를 활용하여 애니메이션 상태와 로직을 1:1로 매핑한 보스 패턴 시스템 구현 (Project I)

Troubleshooting

개발 중 발생하는 예상치 못한 스펙 변경이나 성능 이슈를 최선의 방법으로 해결하기 위해 노력합니다.

프로토타입 지형 생성 로직을 상용 수준으로 리팩토링

재미 검증용으로 작성된 프로토타입 코드의 구조적 한계를 파악하고 병목 원인을 분석하여 상용 수준의 퍼포먼스를 확보하였습니다.

문제 상황

테스트 기기 환경에서 거대한 레벨을 생성할 경우, 300초 이상의 긴 시간이 소모되는 현상이 발생하였습니다.

원인 분석

Profiler를 사용하여 분석한 결과, 심각한 GC 오버헤드와 비효율적인 연산이 핵심 원인으로 파악되었습니다.

  1. GC 오버헤드
    • class 기반 좌표 클래스의 연산자 오버로딩으로 인해 매 연산마다 발생하는 힙 할당
    • ToList, ToDictionary 등의 잦은 LINQ 호출로 인한 리사이징과 박싱
    • 잦은 Coroutine 호출
  2. 비효율적 알고리즘
    • 오버랩 체크 시 타일 개수만큼 객체를 생성하거나 전체를 순회하는 $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 데이터를 활용하면서도, 물리 엔진 기반의 주행이 가능한 시스템을 새롭게 구축해야 했습니다.

해결 과정

Unity AI Navigation Package의 Baking 함수를 추적하여, 베이킹 시 생성되는 NavMesh 폴리곤의 좌표 데이터를 추출하는 기능을 구현했습니다.
추출된 데이터를 별도의 ScriptableObject로 저장하여, NavMeshAsset 없이 런타임에 경로 데이터를 참조할 수 있도록 구조화했습니다.

2. 주행 경로 수정

NavMesh가 지원하는 경로 따라가기 함수 사용 시 폴리곤의 꼭짓점 부분을 이용하여 이동하기 때문에 시각적으로 부자연스러운 문제가 발생하였습니다.
이를 해결하기 위해 삼각형의 무게중심을 선형 보간한 경로를 사용하였으나, 삼각형 형태에 따라 불필요한 움직임이 발생했습니다.
최종적으로는 인접한 폴리곤이 맞닿는 모서리의 중점을 연결하여 경로를 수정하는 로직을 적용하여, 차량이 도로의 중앙을 따라 부드럽게 이동할 수 있도록 구현했습니다.

3. 장애물 회피 로직 구현

NavMeshAgent의 회피 기능을 대체하기 위해, Raycast를 사용하여 전방의 오브젝트를 감지하고 정지/후진하는 로직을 추가했습니다.
박스나 캡슐 콜라이더를 가진 고정 장애물 감지 시, 콜라이더의 좌표와 회전값을 분석하여 우회 경로를 생성합니다.
현재 속도를 기반으로 예상 도착 시간을 계산하고, 지연이 심할 경우 경로를 재탐색하도록 설계했습니다.

결과

기존 스테이지 리소스를 수정하지 않고도 WheelCollider 기반의 물리 주행 시스템을 성공적으로 도입했습니다.
자체적인 경로 보정 알고리즘을 적용하여 자연스러운 차량 주행감을 구현할 수 있었습니다.