Naninovel 비주얼 노벨 엔진과 방치형 RPG 시스템을 통합한 게임입니다. 플레이어는 기사를 조작해 무한 스테이지의 몬스터를 자동 전투로 처치하며 골드를 획득하고, 골드로 스탯을 강화해 더 높은 스테이지에 도전합니다. 3명의 동료 캐릭터(Ariel, Elena, Lynn)를 해금하고 호감도를 쌓으면 개별 스토리가 진행되는 구조입니다.
| 이름 | 역할 | 담당 업무 |
|---|---|---|
| 김경학 | 프로그래머, 기획, 아트 | 프로젝트 전체 개발 (시스템 설계, 동료 시스템, 장비 제작 시스템, Naninovel 연동, Claude Skill 기반 시나리오 자동 생성, 일러스트 제작) |
이 프로젝트에서는 Claude Code의 커스텀 스킬(nani-scenario-generator)을 개발해, 자연어 시나리오를 Naninovel 스크립트(.nani)로 자동 변환하는 워크플로우를 구축했습니다.
캐릭터 일러스트는 니지저니(Nijiourney)로 제작하고, 캐릭터 설정과 스토리 초안은 제미나이(Gemini)가 작성합니다. 작성된 시나리오를 Claude Skill에 전달하면 Naninovel 규칙에 맞춰 스크립트가 자동 완성됩니다.
[시나리오 생성 워크플로우]
1. 니지저니(Nijiourney) → 캐릭터 일러스트 제작
2. 제미나이(Gemini) → 캐릭터 설정, 스토리 초안 작성
3. Claude Skill(nani-scenario-generator) → Naninovel 스크립트 자동 변환
스킬은 .claude/skills/nani-scenario-generator/SKILL.md에 정의되어 있으며, Naninovel의 전체 명령어 문법을 포함합니다. 자연어 입력에서 캐릭터, 배경, 선택지, 효과음, 카메라 연출 등을 추출해 .nani 파일로 변환합니다.
; --- 스킬이 지원하는 Naninovel 명령어 ---
@back BackgroundName time:1.0 ; 배경 전환
@char CharacterName.pose pose:center ; 캐릭터 표정/위치
@hide CharacterName ; 캐릭터 숨기기
@hideAll time:1 ; 전체 숨기기
@choice "선택지" goto:.LabelName ; 분기 선택지
@goto .LabelName ; 라벨 이동
@bgm BGMName volume:0.3 ; 배경음악
@sfx SFXName ; 효과음
@camera zoom:0.3 ; 카메라 줌
@shake MainBackground power:0.2 ; 화면 흔들기
@spawn SpawnName pos:55,45 ; 이펙트 생성
@showUI / @hideUI ; UI 표시/숨기기
@wait / @delay ; 대기
@end ; 스크립트 종료
[입력] "Scene starts in a dark dungeon.
Elena is scared, cornered by slimes.
She screams for help.
The player can choose to help her immediately or watch first."
; Elena story 1
@back Dungeon0 time:1
@showUI time:1
@bgm Action2 volume:.3
@char Elena.scared pose:center
Unknown: 으악! 안 돼! 이건 졸업 기념으로 맞춘 특제 로브란 말이야!
@char Slime pose:left
Unknown: 저리 가! 이렇게 붙으면 마법을 못쓴다고!
@choice "구해준다." goto:.Rescue
@choice "잠깐 지켜본다." goto:.Watch
@stop
# Rescue
나는 검을 뽑아 들고 슬라임 사이로 뛰어들었다.
@hide Elena
@char Slime pose:center
@camera zoom:.3
@delay .5
@spawn SwordSlashThinBlue pos:55,45
@sfx Skill_Knife_Throw_B
@delay 1.2
@hide Slime lazy:true
@sfx Weapon_Impact_Blood
이 워크플로우를 통해 3개 캐릭터 × 5개 에피소드, 총 16개의 시나리오 스크립트를 효율적으로 생성했습니다.
| 항목 | 수치 |
|---|---|
| 시나리오 스크립트 | 16개 (.nani 파일) |
| 총 선택지(Choice) 지점 | 39개 |
| 분기 라벨(Label) | 60개 이상 |
| 사용 배경(Background) | 12종 (던전, 숲, 동굴, 비, 모닥불, 레이저 복도 등) |
| 캐릭터 표정(Pose) | 14종 (default, happy, angry, scared, nervous, love, sad, shy, embarrassed, tired, disgusted, thinking, worried, determined) |
| 효과음/배경음 | 20종 이상 (전투, 마법, 환경음, 타격음) |
| 카메라 연출 | 줌, 흔들기, 페이드 |
각 캐릭터는 프롤로그 1개 + 에피소드 5개로 구성되며, 선택지에 따라 대사가 분기합니다.
에피소드 1에서 던전에서 슬라임에게 쫓기는 모습으로 첫 등장합니다. 플레이어가 구해주거나 지켜보는 선택지로 시작해, 마법 실전 강의(에피소드 2), 수정 동굴 사고와 스킨십(에피소드 3), 모닥불 야간 장면(에피소드 4), 최종 전투와 평생 계약(에피소드 5)으로 이어집니다.
선택지는 전투 방식(직접 구출 vs 관전), 대화 태도(위로 vs 놀림), 관계 발전(수락 vs 거절) 등 상황마다 2~3개의 분기를 제공합니다. 에피소드 5에서는 최종 선택지로 "평생 함께하자"와 "보수는 너로 충분해" 두 가지 결말이 있습니다.
에피소드 1에서 던전에서 몬스터와 교전 중인 용병으로 만납니다. 왕립 기사냐는 질문, 협력 제안, 전투 돕기 등의 선택지로 관계가 시작됩니다. 레이저 트랩에서 함께 넘어지는 해프닝(에피소드 2), 불침번 교대(에피소드 3), 전투 전 손잡기(에피소드 4), 최종 전투 후 새로운 계약(에피소드 5)으로 진행됩니다.
Lynn의 스토리는 전투와 전문성이 핵심 테마입니다. 선택지는 작전 수락, 걱정 표현, 로맨스 모멘트 등으로 구성되고, 최종적으로 플레이어와 "새로운 계약"을 맺으며 용병 생활을 마감합니다.
에피소드 1에서 숲 던전에서 활을 겨누며 경계하는 엘프로 등장합니다. 길을 잃었다고 설명하거나 항복하는 선택지로 시작합니다. 흔들리는 다리 공포(에피소드 2), 비 피하며 동굴에서 대화(에피소드 3), 괴물 습격 시 아리엘의 희생(에피소드 4), 모닥불 앞 맹세(에피오드 5)로 이어집니다.
에피소드 3의 비 피하기 씬에서는 일러스트 CG(ArielCG0)가 등장하고, 에피소드 4에서는 아리엘이 플레이어를 구하기 위해 몸을 던지는 연출로 감정적 몰입을 높입니다. 최종 선택지에서 숲을 함께 떠나거나 영원히 곁에 있겠다는 약속으로 마무리됩니다.
3명의 동료는 각각 고유한 CompanionData ScriptableObject로 스탯, 애니메이션, 해금 비용을 관리합니다. 한 번에 1명만 활성화할 수 있으며, CompanionSystem 싱글톤이 해금, 호감도, 리스폰을 관리합니다.
호감도는 활성화된 동료와 함께 전투하는 동안 20초마다 1씩 상승하며 최대 100까지 쌓입니다. 스토리 에피소드는 호감도 기준(예: 에피소드 1은 호감도 10, 에피소드 5는 호감도 80)으로 해금되어, 전투와 스토리가 자연스럽게 연결되는 루프를 구성합니다.
동료의 레벨은 플레이어 레벨에 동기화됩니다. 플레이어가 레벨업하면 OnLevelUp 이벤트를 통해 동료 스탯도 갱신됩니다. 동료 AI는 플레이어가 공격 중인 적에게 2.5배 거리 페널티를 적용해 적을 분산시키는 전략을 사용합니다.
한국어 조사 처리를 위해 @jong 커스텀 명령어를 구현했습니다. 문자열의 마지막 글자 받침 여부를 판별해 올바른 조사(은/는, 이/가 등)를 선택합니다.
[CommandAlias("jong")]
public class JongsungCheck : Command
{
[ParameterAlias("text")] public StringParameter SourceText;
[ParameterAlias("with")] public StringParameter WithBatchim; // 받침 있을 때
[ParameterAlias("without")] public StringParameter WithoutBatchim; // 받침 없을 때
[ParameterAlias("set")] public StringParameter TargetVariable;
[ParameterAlias("append")] public BooleanParameter AppendToSource;
private static bool HasFinalConsonant(string text)
{
var lastChar = text[text.Length - 1];
const int hangulBase = 0xAC00;
const int hangulLast = 0xD7A3;
if (lastChar < hangulBase || lastChar > hangulLast) return false;
var code = lastChar - hangulBase;
var jong = code % 28;
return jong != 0;
}
}
시나리오에서의 사용 예시:
; 플레이어 이름의 받침 여부에 따라 조사 선택
@jong text:{PlayerName} with:이 without:"" append:true set:name
@choice "난 {name}야. 잘 부탁해." goto:.Accept
스토리 종료 시 @end 커스텀 명령어가 메인 게임 씬으로 전환합니다.
[CommandAlias("end")]
public class SwitchToAdventureMode : Command
{
public override async UniTask Execute(AsyncToken asyncToken)
{
var opMain = SceneManager.LoadSceneAsync("main");
await UniTask.WaitUntil(() => opMain.isDone);
}
}
DialogueManager는 스토리 재생 시 SceneFadeManager로 페이드 효과와 함께 스토리 씬으로 전환하고, Naninovel 엔진 초기화 후 플레이어 이름을 커스텀 변수로 설정합니다.
캐릭터와 적의 스탯은 지수 함수 기반으로 성장합니다. CharacterStatsData ScriptableObject에 기본값과 성장 계수를 정의합니다.
Stat(n) = baseStat × growthRate^(n-1)
// 예: HP의 경우 baseMaxHealth=150, growthHealth=1.12
// 레벨 10 → 150 × 1.12^9 ≈ 415
private int CalculateStat(int baseValue, float growth, int level)
{
if (level <= 1) return baseValue;
double result = baseValue * System.Math.Pow(growth, level - 1);
if (result > int.MaxValue) return int.MaxValue; // 오버플로우 방지
return (int)result;
}
플레이어는 업그레이드 레벨을 골드로 올려 스탯을 성장시키고, 장비 보너스가 추가로 더해집니다. 골드는 DarkNaku.Number 타입으로 대수 표기(1.5K, 2.3M 등)를 지원합니다.
적이 플레이어와 동료 모두를 공격할 수 있도록 ICombatTarget 인터페이스를 도입했습니다. PlayerStats와 CompanionController 모두 이 인터페이스를 구현합니다.
public interface ICombatTarget
{
Transform GetTransform();
bool IsAlive();
void TakeDamage(int damage);
int GetCurrentHealth();
int GetMaxHealth();
string GetName();
}
적은 플레이어와 모든 활성 동료 중 가장 가까운 대상을 타겟으로 삼습니다. 타겟 유효성 검사에서 null 체크, GameObject 파괴 여부, IsAlive까지 확인하고 MissingReferenceException도 캐치합니다.
EquipmentCraftingSystem은 포인트 기반으로 랜덤 장비를 생성합니다. 최대 5포인트를 보유하고 60초마다 1포인트가 회복됩니다. 앱을 종료했다가 돌아오면 오프라인 시간만큼 포인트를 자동 회복합니다.
생성된 장비는 희귀도(일반 50%, 고급 75%, 희귀 90%, 영웅 98%, 전설 2%)에 따라 1~4개의 랜덤 스탯이 부여되고, 희귀도 배율이 스탯에 곱해집니다.
GameSaveSystem은 JSON 파일로 버전 관리(saveVersion = 5)하며 저장합니다. 업그레이드 레벨만 저장하고 기본 스탯과 성장 계수는 ScriptableObject에서 불러오는 방식으로, 밸런스 수정 시 기존 세이브에 영향을 주지 않습니다.
30초 간격 자동 저장, 앱 일시정지/종료 시에도 저장을 수행합니다. 장비 제작 포인트는 오프라인 시간을 계산해 복귀 시 자동 회복합니다.
비주얼 노벨 엔진과 방치형 RPG를 통합해 스토리와 전투가 연결되는 게임을 만들었습니다. Claude Code 커스텀 스킬을 개발해 시나리오 작업을 자동화하고, 니지저니·제미나이와 결합한 AI 워크플로우로 콘텐츠 제작 효율을 높였습니다. 동료 호감도 시스템이 스토리 진행의 핵심 동기가 되고, 전투로 얻은 자원으로 성장하는 루프를 구성했습니다.