유니티를 못 켜는 시간에 뭘 할 수 있을까
요즘 부업으로 방치형 게임을 만들고 있는데, 현실적인 문제가 하나 있어요. 본업이 따로 있다는 거예요. 더 솔직히 말하면 회사 자리에서 유니티 에디터를 띄워놓고 있기가 좀 그렇습니다.
점심시간이나 자투리 시간에 조금이라도 진척은 내고 싶은데, 화면에 게임 캐릭터가 둥둥 떠다니면 누가 봐도 “딴짓”이잖아요. 그래서 며칠 고민하다가 결론을 냈어요.
유니티 없이 할 수 있는 걸 먼저 하자.
그리고 지금 단계에서 유니티 없이 할 수 있으면서도 제일 가치 있는 작업이 하나 있었어요. 바로 밸런싱 시뮬레이터였습니다.
왜 시뮬레이터부터 만들었냐면
저는 10년 가까이 게임 서버를 만졌어요. 클라이언트는 솔직히 아직 1년차에 가까운데, 서버 개발하던 버릇은 그대로 남아 있습니다. 그중 하나가 이거예요.
게임 로직은 UI 없이도 돌아가야 한다.
전투, 진화, 장비 머지, 오프라인 보상 같은 핵심 로직이 전부 유니티 MonoBehaviour에 들러붙어 있으면 테스트도 어렵고, 밸런싱도 어렵고, 나중에 수정할 때도 지옥이 열려요. 그래서 코어 로직은 순수 C#으로 떼어두는 쪽으로 잡았습니다.
방치형 게임에서 제일 무서운 건 숫자가 틀린 채로 UI를 다 만들어버리는 거예요. 레벨 곡선, 골드 인플레이션, 진화 조건, 드롭률 같은 게 어긋나 있으면 유저는 D7도 안 돼서 떠날 수 있거든요.
근데 이걸 유니티에서 직접 플레이하면서 검증한다? 며칠 치 플레이를 실제로 기다려야 하니까 거의 미친 짓에 가깝습니다.
그래서 유저가 7일 동안 플레이한 걸 0.03초 만에 굴리는 콘솔 프로그램을 만들었어요. 유니티가 한 줄도 안 들어가니까 회사에서 봐도 그냥 C# 콘솔 앱입니다. 누가 봐도 업무용 코드처럼 생겼어요. 이거 중요합니다.
구조는 이렇게 쪼갰어요
솔루션은 크게 셋으로 나눴습니다.
MysteryCreature.Core/ # 순수 C# 게임 로직 (.NET Standard 2.1)
MysteryCreature.Simulator/ # 콘솔 앱, 가상 플레이어 실행
MysteryCreature.Core.Tests/ # xUnit, 곡선/결정성 회귀 테스트
Core를 .NET Standard 2.1로 맞춘 이유는 단순해요. 나중에 유니티가 이 프로젝트를 그대로 참조하게 하려고요. 같은 공식을 시뮬레이터용, 클라이언트용으로 두 번 짜면 그 순간부터 디버깅 지옥입니다.
그래서 곡선은 한 군데, Formulas에만 둡니다. 시뮬레이터도 테스트도, 나중에 유니티도 같은 함수를 부르게 만드는 게 목표예요.
설계하면서 제일 신경 쓴 건 엔진과 정책을 분리하는 거였어요.
- 엔진: 게임이 어떻게 반응하는가. 스테이지 클리어 시간, 진화 게이트, 장비 머지 같은 부분
- 정책: 유저가 하루를 어떻게 보내는가. 하루 25분, 5세션, 광고 시청률, 과금 여부 같은 부분
이 둘을 섞으면 무과금 유저, 가벼운 과금 유저, 고래 유저를 비교하기가 어려워져요. 정책만 갈아끼우면 같은 엔진 위에서 여러 유저 타입을 돌려볼 수 있어야 했습니다. 서버에서 부하 시나리오 짜는 느낌이랑 비슷했어요.
그리고 19억 골드가 터졌어요
스테이지 데이터, 진화 곡선, 드롭 테이블을 CSV로 넣고 무과금 유저 7일치를 돌려봤습니다. CSV도 손으로 300줄 치면 무조건 오타가 날 것 같아서 공식에서 자동 생성하게 만들었어요.
=== balance-A (F2P, 7일) ===
day stage 위치 Lv gold
1 0 2-22 63 72,552
4 1 3-30 871 137,849,962
7 4 4-20 2999 1,945,536,778
누적 골드 19억. 목표는 10만~50만이었어요. 대충 4,000배 정도 초과한 셈입니다.

사실 이 순간이 시뮬레이터를 만든 보람이었어요. 만약 유니티에서 UI 다 붙이고, 며칠 플레이해보고 나서 발견했으면 골드 표시 자릿수부터 경제 시스템까지 다 뜯어고쳤을 거예요.
근데 콘솔에서는 0.03초 만에 알려줍니다. “야 이거 골드 터졌어.”
처음에는 목표가 틀린 줄 알았어요
처음에는 이렇게 생각했어요. 오프라인 보상이 16시간에 효율 60%로 들어오고, 골드가 지수로 불어나니까 목표 범위 자체가 모델이랑 안 맞는 거 아닌가?
반은 맞고 반은 틀렸습니다.
며칠 더 들여다보니까 진짜 원인은 다른 데 있었어요. 골드 보상이 난이도랑 같은 1.10 지수를 공유하고 있던 것이 문제였습니다.
스테이지가 어려워지는 속도, 그러니까 DPS 요구치랑 골드 보상이 같은 속도로 불어나니까 플레이어가 챕터 4까지 가는 순간 골드도 같이 폭발한 거예요. 목표가 모순인 게 아니라, 제가 곡선을 잘못 묶어둔 거였습니다.
그래서 골드 인플레이션을 난이도에서 떼어냈어요.
- 난이도 곡선: 1.10 유지
- 골드 보상 곡선: 1.035로 완화
수정 후에는 D7 누적 골드가 465,055로 내려왔습니다. 목표 범위 100K~500K 안에 들어왔어요. PASS가 떴습니다.
좋아할 뻔했는데, 서버 개발자 감으로 뭔가 불안하더라고요. 465K면 상한선 500K에 너무 가까웠거든요.
단일 시드 PASS는 믿으면 안 되더라고요
그래서 바로 시드 8개로 강건성 체크를 붙였습니다. 같은 정책이어도 랜덤 드롭 운이 달라지면 결과가 얼마나 흔들리는지 보려고요.
[강건성 8시드]
골드 228K~444K / 진화 2~4 / PASS 4/8
역시나였습니다. 골드는 범위 안으로 들어왔는데, 진화 단계가 2~4로 들쭉날쭉했어요. 어떤 시드는 D7에 진화 4단계까지 가고, 어떤 시드는 2단계에서 멈췄습니다.

원인은 진화 조건이었습니다. 조건이 “장비 6슬롯 전부 일정 등급 이상”이었는데, 드롭은 랜덤 슬롯으로 떨어지고 있었어요. 운이 나쁘면 특정 슬롯만 계속 안 나와서 진화 게이트를 못 넘는 구조였던 거죠.
현실적인 랜덤이긴 한데, 방치형 초반 7일 구간에서는 분산이 너무 컸습니다.
해결은 의외로 흔한 방식으로 했어요. 스마트 루트입니다. 드롭을 완전 랜덤 슬롯에 넣는 게 아니라, 지금 제일 약한 슬롯을 우선해서 보정하는 방식이에요.
그랬더니 결과가 이렇게 바뀌었습니다.
[강건성 8시드]
골드 246K~474K / 진화 4~4 / PASS 8/8
이제야 진짜로 안심할 수 있었어요. 진화는 전부 4단계, 골드는 전부 목표 범위 안. 단일 시드가 아니라 여러 시드에서 같은 방향으로 통과한 거니까요.
유니티를 안 켜고도 꽤 많이 진행됐어요
이번 작업으로 하루 만에 시뮬레이터 코어를 세우고, 무과금 D7 밸런스를 통과시키고, 그 과정에서 곡선 버그 하나랑 진화 분산 버그 하나를 잡았습니다. 유니티는 한 번도 안 켰어요.
테스트도 63개까지 붙였고 전부 통과했습니다. 같은 시드와 같은 정책이면 결과가 똑같이 나오게 결정성도 잡아뒀어요. 그래서 앞으로 곡선 하나를 바꾸면 어디가 깨지는지 바로 알 수 있습니다.
다음은 과금 유저 시나리오를 돌려볼 생각이에요. 가벼운 과금 유저랑 고래 유저를 나눠서, IAP가 너무 P2W로 새지 않는지 확인해야 합니다. 이것도 유니티 없이 할 수 있어요.
유니티는 숫자가 검증되고 나서 껍데기를 씌울 때 켜도 늦지 않다고 봅니다.
방치형 게임 만드는 분들한테 지금 단계에서 하고 싶은 말은 하나예요. 숫자부터 헤드리스로 굴려보세요. UI는 거짓말을 잘 못 잡아내는데, 콘솔은 0.03초 만에 빨간 글씨로 말해줍니다.