ALEngine
C++17과 Vulkan 기반 3D 게임 엔진

- →glTF 기반 스켈레탈 애니메이션 시스템 구현 (State Machine · Blending 포함)
- →Mono InternalCall 기반 C# 스크립팅 API 설계 및 게임 로직 검증
- →mono_gchandle 기반 GC Lifetime 관리로 Script Object 안정성 확보
- →ECS 위에서 Renderer · Physics · Scripting 시스템 통합
목차
개요
C++17과 Vulkan 기반으로 제작한 3D 게임 엔진 프로젝트입니다. 4인 팀 프로젝트로 약 5개월간 개발했으며, 스켈레탈 애니메이션 시스템과 Mono 기반 스크립팅 시스템을 중심으로 작업했습니다. 게임 클라이언트 개발자로서 엔진 내부 구조를 직접 구현하며 Runtime, Animation, ECS, Scripting 구조를 학습하는 것을 목표로 진행했습니다.
데모 씬 기준 약 60 FPS로 동작하며, 정점 3만 개 이상의 glTF 모델에서도 안정적으로 렌더링됩니다. 엔진 결과물 및 데모 영상은 상단 우측에 링크 되어 있습니다.
기여 범위
4인 팀 중 스켈레탈 애니메이션과 스크립팅 시스템을 중심으로 담당했습니다.
- 스켈레탈 애니메이션 시스템 — glTF 파싱부터 State Machine까지 전체 구현
- Mono 스크립팅 API — InternalCall 기반 API 확장, GC Lifetime 관리 구조 추가
- 모델하우스 씬 스크립트 — 모든 상호작용 스크립트 작성
- ECS 통합 — Renderer · Physics · Scripting 시스템 통합 과정 참여
Vulkan 렌더러와 PhysX 물리 시스템은 팀원이 담당했으며, ECS 위에서 각 시스템을 통합하는 작업은 팀 전체가 함께 진행했습니다.
주요 기술 구현
스켈레탈 애니메이션 시스템
glTF 기반 스켈레탈 애니메이션 시스템을 구현했습니다.
- Sampler / Channel 구조 파싱
- 키프레임 보간
- Quaternion slerp 기반 회전 보간
- Animation State Machine 구현
- Animation Blending
- 역재생 기반 상호작용 애니메이션 지원
애니메이션 상태 관리가 하드코딩 구조로 복잡해지는 문제를 해결하기 위해 별도의 AnimationStateManager를 설계했습니다.
C#에서 함수를 선언해 Transition 조건으로 등록하면, 매 프레임 루프에서 조건을 평가해 상태 요청 큐에 삽입하는 방식으로 동작합니다. 조건 판단이 코드 기반이기 때문에 복잡한 로직도 스크립트 레벨에서 자유롭게 표현할 수 있으며, 상태 수에 별도 제한을 두지 않았습니다.
구현 이유
초기에는 애니메이션 상태를 스크립트에서 직접 제어하는 구조를 고려했지만, 상태 수가 늘어날수록 전환 조건과 재생 흐름이 복잡해지는 문제가 있었습니다. 또한 프로젝트 규모에 비해 적합한 외부 상태 머신 라이브러리를 찾기 어려웠고, 필요한 기능 범위가 비교적 단순했기 때문에 직접 구현하는 방향을 선택했습니다.
상태 전환이 없다면 시체다 [링크 연결 필요]
Mono 기반 스크립트 런타임
Mono Runtime 기반 C# 스크립팅 시스템을 확장했습니다.
- InternalCall 기반 Native API 연결
- Quaternion / Animator API 구현
- Component 접근 API 확장
- Script Component 활성화 제어
스크립트 API를 실제 게임 로직에서 검증하기 위해 모델 하우스 씬의 상호작용 스크립트 전체를 작성했습니다.
구현 인터랙션 예시
- 문 열기 / 닫기
- 버튼 토글
- 카메라 이동
- 엔티티 추적
- 라이트 제어
C++에서 C#을 호출하는 방법 feat. Mono [링크 연결 필요]
문제 해결 경험
런타임 메모리 디버깅
플레이 중 특정 시점 이후 Script Object 내부 값이 비정상적으로 변경되는 문제가 발생했습니다.
초기에는 메모리 오염 문제로 추정했으나, 이후 Mono GC가 Native 영역에서 참조 중인 객체를 이동 및 수거하는 것이 원인임을 확인했습니다.
이를 해결하기 위해 mono_gchandle 기반 Lifetime 관리 구조를 추가하고, C++ Runtime에서 Script Instance 생명주기를 직접 관리하도록 수정했습니다.
해결 방식
- ScriptInstance 생성 시 GC Handle 등록
- Runtime에서 Script Object Lifetime 직접 관리
- 객체 접근 시 Handle 기반 접근으로 변경
- 소멸 시 GC Handle 해제
이후 GC 실행 이후에도 Script Object 상태가 안정적으로 유지되는 것을 확인했습니다. 카메라가 우주로 날아간 이야기
Animation Bone Matrix 전용 UBO 분리 실패 경험
스켈레탈 애니메이션의 Bone Matrix 데이터를 기존 UBO와 분리해 별도로 관리하려고 시도했습니다. Bone Matrix는 매 프레임 갱신되는 동적 데이터이기 때문에, 변하지 않는 셰이더 데이터와 분리해 관리하는 것이 목표였습니다.
하지만 Vulkan Descriptor Set Layout 구성, Binding 순서, Command Buffer 흐름 등을 수정하는 과정에서 안정적인 구조를 완성하지 못했습니다. 프로젝트 기간 내 해결이 어렵다고 판단해 최종적으로는 기존 UBO에 통합하는 방향으로 구조를 단순화했습니다.
이 과정에서 얻은 경험
- Vulkan Resource Binding 흐름 이해
- Descriptor Set Layout 구조 추적
- Buffer Binding 시점 디버깅
- Render Pipeline 데이터 흐름 분석
Camera Follow 구조와 짐벌락 문제
초기에는 Player Entity를 카메라가 따라가는 구조로 구현했습니다. 하지만 특정 회전 각도에서 카메라 방향이 비정상적으로 뒤집히는 문제가 발생했고, Euler Rotation 기반 회전 동기화의 한계를 경험했습니다. Quaternion 기반 회전 구조로 개선을 시도했지만, 기존 Transform이 Euler 기반으로 직렬화 및 동기화되어 있어 Quaternion 변환을 끼워넣으면 다른 시스템과의 정합성이 깨지는 문제가 있었습니다.
근본적인 해결은 Transform 전체를 Quaternion 기반으로 재설계하는 것이었지만, 프로젝트 마감 일정상 현실적이지 않다고 판단해 최종적으로는 카메라 직접 이동 방식으로 구조를 변경했습니다. 이 과정에서 회전 표현 방식과 Transform 구조 설계의 중요성을 체감할 수 있었습니다.
프로젝트를 통해 배운 점
- Runtime Ownership과 GC Lifetime 관리
- GPU 기반 Skeletal Animation 구조
- ECS 기반 시스템 통합 경험
- Animation Transition 구조 설계
- Vulkan Resource 관리 흐름
- 실제 엔진 통합 과정에서의 구조 충돌 경험
특히 팀원들이 각자 독립적으로 개발한 Renderer, Physics, Scripting 시스템을 하나의 ECS 위에서 통합하는 과정에서, 인터페이스 설계와 Runtime 구조 조율의 중요성을 크게 느낄 수 있었습니다. 직접 구현하며 쌓은 Animation, GC, ECS에 대한 구조적 이해는 이후 언리얼 엔진의 AnimInstance, UObject GC 구조를 파악하는 데 직접적인 기반이 됐습니다.
아쉬웠던 점과 이후 개선 방향
Polling 기반 이벤트 처리 구조
다시 만든다면 Collision, Interaction, State Change 같은 흐름을 별도의 Event Layer로 분리하고 싶습니다. 현재 구조에서는 Key Input은 이벤트로 처리되지만, Collision 처리는 Polling 기반 구조로 구현되어 있습니다. 작은 규모에서는 단순하고 빠르게 구현할 수 있었지만, 상호작용이 늘어날수록 컴포넌트가 서로의 상태를 직접 확인해야 하는 구조가 되었고 결합도가 높아지는 문제가 있었습니다.
Event Layer를 도입한다면 충돌이나 상호작용이 발생했을 때 필요한 시스템만 해당 이벤트를 구독해 처리할 수 있습니다. 이를 통해 Polling 기반 구조의 단순함은 유지하면서도, 시스템 간 직접 의존성을 줄이고 스크립팅 API를 더 유연하게 확장할 수 있을 것이라 생각합니다. 충돌 전수 조사 [링크 연결 필요]
Shared Model 구조 한계
동일한 모델을 여러 Entity가 공유할 때, 애니메이션 클립 데이터와 실행 상태가 명확히 분리되지 않는 문제가 있었습니다. 그 결과 같은 모델을 사용하는 여러 Entity가 서로 독립적으로 애니메이션을 재생하지 못하고, 동일한 재생 시간과 상태를 공유하는 문제가 발생했습니다.
Mesh, Material, Skeleton 같은 리소스는 여러 Entity가 공유해도 되지만, 현재 재생 시간, 반복 여부, 역재생 여부 같은 Runtime State는 Entity마다 독립적으로 관리되어야 했습니다. 당시 구조에서는 SkeletalAnimation 내부에 불변 클립 데이터와 가변 상태가 함께 존재해, 같은 모델을 사용하는 Entity들이 애니메이션 상태까지 공유할 수 있는 구조였습니다.
이를 우회하기 위해 SAComponent에서 Entity별 상태를 따로 저장하고, 매 프레임 공유 애니메이션 객체에 주입한 뒤 다시 회수하는 Save / Restore 방식으로 처리했습니다. 다시 설계한다면 SkeletalAnimation은 무상태 클립 데이터로 두고, 재생 상태와 Bone Matrix는 Entity별 AnimationInstance에서 관리하도록 분리하고 싶습니다.
고블린 세 마리가 같은 영혼을 공유한 이유 [링크 연결 필요]
참고 레퍼런스
- Game Engine Architecture - Jason Gregory
- https://github.com/beaumanvienna/vulkan/tree