레거시 비주얼 노벨 시스템 분석과 확장

한눈에 보기

기존 데이터 기반 비주얼 노벨 시스템을 인수받아 실행 흐름을 분석하고, 선택지 분기와 저장·복구 기능을 확장했습니다. 전체 시스템을 새로 만든 작업이 아니라, 이미 서비스 규모로 커진 구조에서 기존 동작을 보존하며 상태 전이를 안전하게 추가한 경험입니다.

항목내용
프로젝트3D 캐릭터 기반 비주얼 노벨 게임
기간2025.08 ~ 2026.07
역할Unity 클라이언트 개발
기술Unity, C#, UGUI, Animator, CSV 기반 시나리오 데이터
직접 기여구조 분석, 선택지 분리, 저장 상태 복구, 이어하기 개선

기여 범위

CSV 파싱과 기본 시나리오 진행 구조는 기존에 존재했습니다. 저는 해당 구조를 인수받아 분석한 뒤 선택지 진행 정책, 로드 시 연출 상태 복구, 이어하기 흐름과 콘텐츠 제작 보조 도구를 구현·개선했습니다.


1. 먼저 시스템을 이해해야 했다

프로젝트에 합류했을 때 시나리오 진행, 캐릭터 연출, 이벤트 실행과 저장 기능이 여러 Manager와 Controller에 나뉘어 있었습니다. 기능을 바로 추가하면 기존 자동 재생, 스킵, 저장과 장면 전환 중 하나를 쉽게 깨뜨릴 수 있는 상태였습니다.

그래서 구현보다 먼저 시나리오와 캐릭터를 중심으로 클래스 다이어그램을 작성했습니다.

flowchart LR
    A["시나리오 CSV"] --> B["ScenarioManager<br/>파싱·데이터 보관"]
    B --> C["ScenarioController<br/>페이지 진행"]
    C --> D["CharacterController<br/>캐릭터 연출"]
    C --> E["EventController<br/>배경·효과·음악"]
    C --> F["DataManager<br/>진행 상태 저장"]

이 과정에서 ScenarioController가 단순히 다음 대사를 출력하는 역할을 넘어 선택지, 자동 재생, 스킵과 연출 실행까지 함께 조정한다는 점을 확인했습니다. 선택지를 조건문으로 계속 추가하면 페이지 이동 규칙이 더 복잡해지고 기존 진행 경로에도 영향을 줄 수 있었습니다.

증빙

기존 클래스 다이어그램을 공개용으로 단순화한 이미지를 이 위치에 삽입합니다. 사용하지 않는 클래스와 회사 내부 명칭은 제거합니다.


2. 선택지 분기를 기존 진행 로직에서 분리하기

문제

일반 대사는 현재 페이지의 다음 행으로 이동하면 되지만, 선택지 구간에서는 다음 페이지의 의미가 달라집니다.

  • 선택지 시작 명령을 만나면 입력을 기다려야 합니다.
  • 선택한 분기의 시작 위치로 이동해야 합니다.
  • 선택하지 않은 분기 구간은 건너뛰어야 합니다.
  • 분기가 끝나면 일반 진행 규칙으로 돌아와야 합니다.
  • 자동 재생과 스킵은 선택지 입력을 무시해서는 안 됩니다.

이 규칙을 ScenarioController의 페이지 증가 코드 안에 직접 섞으면 모든 진행 모드가 선택지 구현에 의존하게 됩니다.

선택

페이지를 어떻게 증가시킬지 결정하는 함수를 교체할 수 있도록 만들었습니다. 평상시에는 기본 증가 함수를 사용하고, 선택지 상태에 진입하면 분기 전용 함수를 주입합니다.

private int SetDefaultNextPage()
{
    return pageCount + 1;
}
 
public void SetNextPageAction(Func<int> nextPagePolicy = null)
{
    onSetNextPage = nextPagePolicy ?? SetDefaultNextPage;
}

ScenarioSelectState는 선택지 데이터의 시작과 끝 인덱스를 파싱하고, 사용자가 선택한 분기의 다음 페이지를 계산합니다. 선택 구간이 끝나면 기본 정책을 다시 등록합니다.

stateDiagram-v2
    [*] --> Normal
    Normal --> Selecting: "Select 명령 감지"
    Selecting --> Branch: "사용자 선택"
    Branch --> Normal: "SelectEnd 도달"
    Normal --> [*]: "시나리오 종료"

구현에서 중요했던 점

단순히 클래스를 나눈 것보다 기존 진행 규칙을 복구하는 시점이 중요했습니다. 분기 종료 후 정책을 되돌리지 않으면 이후 일반 대사도 선택지 인덱스 계산을 사용합니다. 또한 선택지 UI가 열려 있는 동안 자동 재생과 스킵이 다음 페이지를 요청하지 못하도록 입력 상태를 함께 조정했습니다.

결과

  • 일반 진행과 선택지 진행의 페이지 계산 책임이 분리됐습니다.
  • 선택지 구간 밖의 기존 코드를 크게 바꾸지 않고 기능을 확장했습니다.
  • 분기 종료 시 기본 진행으로 복구되는 경계가 코드에 명시됐습니다.

증빙

선택 전 → 분기 대사 → 공통 대사 복귀가 한 번에 보이는 20~30초 영상을 추가합니다. 가능하면 동일한 데이터 행을 함께 보여줍니다.


3. 저장된 페이지 번호만으로는 화면을 복구할 수 없었다

문제

시나리오 데이터는 매 행마다 전체 화면 상태를 기록하지 않습니다. 배경이나 BGM이 바뀌는 행에만 새 키가 들어 있고, 이후 대사 행은 빈 값을 유지합니다.

예를 들어 120번째 대사에서 저장했더라도 마지막 배경 변경은 97번째 행, 마지막 BGM 변경은 83번째 행일 수 있습니다. 저장된 페이지로 바로 이동하면 대사는 맞지만 배경·효과·음악이 기본 상태로 남아, 정상 진행으로 도달했을 때와 다른 화면이 만들어집니다.

분석

이 문제는 단순한 파일 로드 오류가 아니라 명령의 실행 결과로 만들어진 상태를 어떻게 재구성할 것인가의 문제였습니다.

검토할 수 있는 선택지는 다음과 같았습니다.

  1. 저장할 때 모든 연출 상태를 함께 직렬화한다.
  2. 처음부터 저장 지점까지 모든 명령을 다시 실행한다.
  3. 복구가 필요한 각 상태의 마지막 유효 명령을 찾아 적용한다.

현재 저장 데이터 구조와 변경 범위를 고려해 세 번째 방법을 선택했습니다. 저장된 페이지에서 뒤로 이동하며 BG, BG Effect와 BGM의 마지막 유효 키를 각각 찾았습니다.

int bgPage = savedPage;
while (string.IsNullOrEmpty(scenario[bgPage].backgroundKey) && bgPage > 0)
{
    bgPage--;
}
 
ApplyBackground(scenario[bgPage].backgroundKey);

실제 구현에서는 배경, 배경 효과와 BGM마다 유효 값의 조건이 달라 각 항목을 독립적으로 복구했습니다. 페이드처럼 중간 진행률을 그대로 되살리기 어려운 효과는 완료 상태를 기준으로 안전하게 초기화했습니다.

선택지 구간의 저장

선택지 분기 내부의 페이지를 그대로 저장하면 로드 시 어느 선택지에서 진입했는지 알기 어렵습니다. 선택지 진행 중에는 분기 시작 이전의 기준 페이지를 저장하도록 처리해, 복구 시 불완전한 선택 상태에서 시작하지 않도록 했습니다.

이어하기 흐름

게임 시작 시 퀵세이브가 있으면 이를 우선하고, 없으면 일반 저장 데이터 중 가장 최근 항목을 찾아 이어하기 선택지를 제공했습니다. 저장 데이터가 전혀 없을 때만 새 게임을 시작하도록 분기했습니다.

결과

  • 저장 위치의 대사뿐 아니라 배경·효과·음악 상태를 함께 복구했습니다.
  • 선택지 진행 중 만들어질 수 있는 불완전한 저장 지점을 피했습니다.
  • 퀵세이브 유무에 따라 이어하기가 끊기던 흐름을 보완했습니다.

남은 한계

뒤로 탐색하는 방식은 이해하기 쉽고 기존 구조를 적게 변경하지만, 저장 지점과 데이터 길이에 따라 탐색량이 늘어납니다. 상태 종류가 계속 증가하면 복구 규칙도 흩어질 수 있습니다.

다시 설계한다면 시나리오를 로드할 때 각 페이지별 유효 상태 스냅샷을 미리 계산하거나, 저장 가능한 상태를 하나의 명시적인 모델로 모으는 방식을 검토하겠습니다.

증빙

  1. 특정 BG·BGM 상태에서 저장
  2. 다른 장면으로 진행
  3. 로드
  4. 대사와 BG·BGM·Effect가 함께 복구되는 장면

위 과정을 같은 영상 안에서 보여줍니다.


4. 이 경험에서 배운 것

  • 레거시 확장에서 중요한 것은 새 구조의 모양보다 기존 상태 전이의 경계를 정확히 파악하는 일입니다.
  • 저장·로드는 데이터 직렬화만이 아니라, 실행 결과로 만들어진 게임 상태를 동일하게 재구성하는 기능입니다.
  • 콘텐츠 제작자가 반복적으로 조정하는 값은 런타임 코드에만 두지 않고 에디터 도구로 노출할 가치가 있습니다.
  • 현재 요구사항에 적합한 작은 변경을 선택하되, 상태 종류가 증가할 때의 확장 한계를 함께 기록해야 합니다.

관련 연출 도구 사례

기존 애니메이션을 정지 포즈로 재활용하고 커스텀 인스펙터에서 포즈를 선택한 과정은 별도 사례로 정리했습니다.


← 포트폴리오 홈으로