dev
Sonic Frontier 모작


소닉 프론티어(Sonic Frontier)

embed

bookmark

작업기간: 2025-04-23 ~ 2025-07-19(약 12주)

기술 스택

  • C++
  • DirectX11(DX11)

외부 라이브러리

  1. PhysX
  2. FMOD Studio
  3. IMGUI
  4. ImGizmo
  5. simd-json
  6. ImFileDialog

개요

DirectX11을 이용한 모작 프로젝트로, 개인적으로 팬이였던 IP중에 최신 작을 골랐다. 애셋 추출 부터 사용된 기술에 대한 연구까지 동시에 진행했습니다.

프레임워크

Entity-Component-System(ECS) 에 가까운 프레임워크를 작성하여 사용했으며 필요한 시스템들을 싱글턴 으로 선언했고, Engine DLL을 따로 작성하여 세개의 프로젝트로 관리했다. Client, Editor, Engine 이렇게 세개의 프로젝트로 관리했다. 대부분의 기초 컴포넌트와 시스템들을 엔진에서 작성후 동적 라이브러리화 해서 클라이언트에서 접근 가능하게 했으며 모든 시스템을 가지고 있는 싱글턴 인스턴스 하나만 접근 가능하게 했다. DirectX11을 사용하기 때문에 고정 렌더링 파이프라인이 아닌 동적으로 렌더링 했습니다. 멀티 렌더 타겟을 이용해서 지연 렌더링을 했습니다. 충돌과 움직임은 PhysX라이브러리를 사용해서 구현했습니다. 툴용 GUI는 ImGui를 이용해 추가했습니다.

애셋 추출

소닉 프론티어는 자체 엔진 사용으로 Unity와 Unreal Engine처럼 정형화된 추출 툴은 없지만, 모더와 팬들이 제작해둔 툴이 있어 그것을 이용해서 애셋을 추출 할 수 있었다.

모델

추출한 모델과 애니메이션을 FBX로 변환해서 사용했다. 생성시간을 줄이기 위해서 Assimp 라이브러리로 읽고, 필요한 정보만 바이너리 변환을 통해 준비했다. 애니메이션이 있는 모델과 없는 모델의 정보가 다르므로 플래그를 만들어서 변환 과정에서 기록해 다시 읽을 때 문제가 없도록 처리 했다.

메쉬 인스턴싱

추출된 애셋에 정적인 오브젝트의 위치가 담긴 파일이 있어, 이를 블렌더 에서 변환 시킨다음 Python 스크립트를 통해 내 엔진에서 사용할 수 있는 json 형태로 트랜스폼을 저장해 메쉬 인스턴싱으로 렌더링 했다.

애니메이션

뼈를 이용한 애니메이션으로, 정점 쉐이더에서 뼈의 변화에 따른 정점의 위치를 변화 시켜 픽셀 쉐이더로 넘겼다. CAnimator::Play_Animation에서 이전·현재 애니메이션을 각각 샘플링하고, 뼈의 로컬 변환을 스케일·회전·이동으로 분해했습니다. 스케일과 이동은 Lerp, 회전은 Quaternion Slerp로 혼합한 뒤 행렬을 다시 구성해 상태 전환을 표현했습니다. 상태에 따라서 애니메이션을 바꾸고, 콤보 공격시 특정 시간내에 입력을 계속한다면 다음 공격이 나가게 했다.

게임 오브젝트

게임 오브젝트는 컴포넌트를 들고 있고 이 컴포넌트에 따라 기능이 구현 된다. 게임 오브젝트는 상속관계를 가지고 있으며, 기본적으로 트랜스폼을 들고있다. 자식인 게임 오브젝트는 부모의 트랜스폼에 위치, 크기, 회전, 을 각각 영향을 받을지 말지 결정 할 수 있다.

툴

IMGUI를 통해 뷰포트를 만들었으며, Window 창의 크기에따라서 비율에 맞게 조정될 수 있도록 만들었으며, 그에 따른 UV나 피킹위치에 대해서 보정을 해줬다. 멀티 렌더 타겟을 사용 하고 있기 때문에 원하는 렌더타겟을 뷰포트에 띄워주기 위해 IMGUI에 이미지 복사를 통해 보여줬다. 피킹 렌더 타깃의 RGB 값을 객체 ID로 해석하고, ID에 대응하는 객체를 찾아 선택 대상으로 지정했습니다. 선택한 객체는 ImGuizmo 기즈모 또는 수치 입력으로 위치, 회전, 크기를 수정할 수 있도록 했습니다.

CPicking_System은 읽기 요청 시 Staging Texture를 생성하고 CopyResource와 Map으로 CPU에서 픽셀을 읽습니다. 이는 GPU ID 피킹의 구현 경로이며, 비동기 읽기나 비용 최적화까지 완료한 방식으로 설명할 수는 없습니다. 읽기 빈도와 GPU·CPU 동기화 비용은 별도 측정이 필요합니다.

UI 툴

텍스쳐 아틀라스를 그대로 사용하기 위해서 텍스쳐의 크기와 선택영역에서 UV를 계산해 쉐이더로 넘겨서 렌더링 했다. 현재 등록된 이미지 파일 중에 선택해서 툴 상에서 파일을 바꾸고 저장할 수 있도록 만들었다.

맵 툴

지형 컴포넌트에서 Height Map 파일을 불러와 실시간으로 평면에 높이를 적용 시켜서 표현할 수 있게 만들었다. 만약 같은 폴더 안에 Splat Map 파일이 있다면 폴더안에 있는 텍스처들을 순회해서 등록해 Splat Map을 통해 지형을 렌더링 했다. 지형 컴포넌트에서 형성된 지형의 정점버퍼와 인덱스 버퍼를 토대로 충돌체 컴포넌트에서 삼각형 메쉬를 만들어서 지형의 과의 충돌을 구현했다. 픽셀 피킹을 통해 위치를 결정하고 IMGUIZMO라는 라이브러를 사용해 기즈모를 사용하거나 숫자를 입력해서 상태를 조정 했다.

스플라인

CSpline_Component에서 네 제어점을 이용한 Catmull-Rom 보간으로 경로를 샘플링했습니다. 샘플 사이의 접선과 노멀·바이노멀을 계산하고, 누적 길이를 기록한 경로 프레임을 구성했습니다. Find_Closest_Param은 위치를 각 경로 구간에 투영해 가장 가까운 지점의 누적 길이를 전체 길이로 정규화합니다.

제어점이 변경되어 Dirty 상태가 되면 CGameObject_Spline에서 경로와 렌더 버퍼를 갱신하고, CRigidBody::Create_Colliders_Along_Spline을 호출해 PhysX 충돌체를 다시 생성합니다. 경로 편집과 충돌체 배치를 연결한 구현입니다.


경로의 충돌체와 레일 외형은 별도로 다뤘습니다. 레일 모델을 셰이더에서 스플라인에 맞춰 변형하는 방식도 시도했지만 완성하지 못했고, 급격히 꺾이는 구간의 모델 문제도 작업 기록에 남아 있습니다.

코드 근거와 분석 범위

GitHub 저장소의 main 커밋 13def1f 기준입니다.

코드의 구현 경로를 확인한 범위입니다. 빌드·플레이와 로딩 시간·GPU 비용의 재측정은 수행하지 않았습니다.

아쉬운 점

  1. 몬스터에 대한 구현을 못했다.
  2. 상호작용에서 일어나는 버그들
  3. 레일 모델을 쉐이더를 통해서 스플라인에 따라서 늘려서 표현하려고 했는데 실패했다.
  4. 중간보스인 아슈라의 충돌로직을 구현하다가 프레임드랍과 오류를 잡지못해 폐기