게임 데이터 분석의 심장: 로깅 시스템, 밑바닥부터 다시 만들기 (Unity + PlayFab)

오늘 하루 종일 '로그'와 씨름했습니다.

이전에도 LocalDebugLogger라는 이름으로 로깅 시스템을 갖추고는 있었습니다. 하지만 솔직히 말하면, 이건 '데이터 수집'이라기보단 '데이터 배설'에 가까웠죠. 모든 로그를 LogEntry라는 하나의 '만능 잡동사니 클래스'에 욱여넣다 보니, null 값이 절반인 지저분한 JSON 파일만 쌓여갔습니다.

"damage": null, "waveCount": null, "enemyType": "Ranger" "damage": 50, "waveCount": null, "enemyType": null

이런 식이었죠. 이걸 나중에 분석에 쓰려면... 생각만 해도 끔찍했습니다.

그래서 오늘, PlayFab을 이용한 제대로 된 데이터 분석을 목표로 로깅 시스템을 완전히 새로 뜯어고쳤습니다.


1. '만능 양식지'를 버리다: OOP의 축복, 다형성

가장 큰 변화는 설계 사상입니다. JsonUtility의 한계(다형성 미지원) 때문에 억지로 null 가득한 클래스 하나만 썼던 과거와 이별했습니다.

대신, 객체지향(OOP)의 상속을 활용했습니다.

  1. BaseLogEntry (뼈대): 모든 로그가 공통으로 가질 '뼈대'를 만들었습니다.
    • eventType (이벤트 이름)
    • timestamp (게임 시간)
    • playerID (이 세션의 고유 ID)
  2. '개별 양식지' (자식 클래스): 이벤트별로 딱 필요한 데이터만 가진 '전용 양식지'를 만들었습니다.
    • DamageLog는 damage, currentHp만 가집니다.
    • WaveStartLog는 waveCount만 가집니다.
    • EnemyKillLog는 enemyType만 가집니다.

이제 null 파티는 끝났습니다! 코드는 훨씬 깔끔해졌고, PlayFab이 받아볼 데이터도 명확해졌습니다.


2. '투 트랙' 전략: TXT와 PlayFab의 분리

로컬 .json 저장은 과감히 삭제했습니다. JsonUtility의 한계에 묶여있을 필요가 없으니까요. 이제 저희 로거는 두 가지 임무만 수행합니다.

  1. 로컬 .txt 파일 (실시간 디버깅용):
    • 이전과 동일하게 AppendLine을 통해 사람이 읽을 수 있는 로그를 실시간으로 기록합니다.
    • 파일 최상단에 playerID (세션 ID)를 기록해서, 나중에 PlayFab 데이터와 대조할 수 있게 했습니다.
  2. PlayFab (데이터 분석용):
    • Awake()에서 PlayFab 익명 로그인을 시도합니다.
    • 로그인 성공 시, '로그 전송 코루틴'을 시작합니다.

3. "장바구니" 시스템: 60초마다 모아서 전송 🛒

PlayFab에 이벤트가 발생할 때마다(예: 1초마다 위치 전송) API를 호출하는 건 엄청난 낭비입니다.

그래서 "장바구니" 시스템을 도입했습니다.

  • List<BaseLogEntry> logEntries: 모든 로그(위치, 데미지, 몬스터 킬...)는 일단 이 '장바구니' 리스트에 담깁니다.
  • SendLogsPeriodicallyCoroutine (전송 코루틴): 60초에 한 번씩 이 '장바구니'를 통째로 들고 PlayFabEventsAPI.WriteTelemetryEvents를 호출해 PlayFab으로 "일괄 결제(전송)"합니다.
  • HandleSessionEnd (게임 종료 시): 게임이 꺼질 때는 60초가 되지 않았어도 '장바구니'에 남은 로그를 싹 긁어모아 마지막으로 전송(Flush)합니다.

덕분에 API 호출은 최소화하면서, 데이터는 빠짐없이 수집할 수 있게 되었습니다.


4. 🔥 화룡점정: 모든 로그에 '현재 웨이브' 심기

오늘 작업 중 가장 중요했던 부분입니다.

"이 플레이어가 죽었을 때, 그게 몇 웨이브였지?" "코어가 가장 많은 데미지를 입은 건 몇 웨이브였을까?"

이전에는 timestamp만 있어서 이걸 분석하려면 시간을 일일이 대조해야 했습니다.

이 문제를 해결하기 위해, BaseLogEntry (뼈대)에 currentWave 필드를 추가했습니다!

C#
 
[Serializable]
public abstract class BaseLogEntry
{
    // ...
    public int currentWave; // ⭐️ 이 이벤트가 발생할 당시의 웨이브 번호

    public BaseLogEntry(..., int currentWave) // ⭐️
    {
        // ...
        this.currentWave = currentWave;
    }
}

그리고 LocalDebugLogger가 로그를 생성하는 모든 메서드 (CoreTakeDamageLog, PositionLog 등)에서 로거 자신이 가진 waveCount 변수를 생성자에 넘겨주도록 수정했습니다.

이제 PlayFab에 쌓이는 **모든 데이터(1초짜리 위치 로그 포함)**에는 currentWave 번호가 찍혀 나옵니다. 시간순 분석은 물론, 웨이브별 분석까지 가능해진 거죠. 🚀


5. 로그 청사진: 우린 무엇을, 왜, 어떻게 분석할 것인가?

로그를 쌓다 보면 "이걸 다 어디다 쓰지?" 싶을 때가 있습니다. 맞습니다. "일단 쌓아두자"는 '보험' 심리도 큽니다. "플레이어가 버그라고 하는데... CauseLog에 원인이 안 찍혔네? -> 버그 확정!" 같은 식으로요.

하지만 저희는 '보험'을 넘어, 이 데이터로 게임을 더 재미있게 만들 구체적인 분석 목표를 설정했습니다.

1. 플레이어 행동 패턴 분석 (히트맵 🗺️)

  • PositionLog (1초 단위 위치):
    • 목표: 플레이어 동선 히트맵(Heatmap) 생성.
    • 분석: 플레이어들이 맵의 어디에 주로 머무는지, 어떤 경로를 선호하는지, 어디가 '핫'한 교전 지역인지, 혹은 아무도 가지 않는 '죽은' 공간은 어디인지 시각화합니다.
    • 활용: "플레이어들이 특정 자원(광물) 근처에만 머문다" ➡️ 자원 밸런스나 맵의 반대편에 새로운 동기 부여 요소를 배치하는 등 레벨 디자인을 개선합니다.
  • DeathLog (사망 위치):
    • 목표: 사망 히트맵(Death Heatmap) 생성.
    • 분석: "플레이어들이 주로 어디서 죽는가?"를 시각화합니다.
    • 활용: PositionLog와 겹쳐봅니다. "많이 가는 곳이라 많이 죽는" 건 정상이지만, "잘 가지도 않는데 사망률만 높은" 지역이 있다면? ➡️ 해당 지역의 몬스터 스폰이나 지형이 불합리한지 검토하고 난이도를 수정합니다.

2. 게임 밸런스 및 난이도 곡선 검증 📈

  • DamageLog (코어/우주선 피해):
    • 목표: 웨이브별(currentWave) 평균/누적 피해량 분석.
    • 분석: "가장 아팠던 웨이브는 몇인가?", "플레이어가 아니라 코어가 집중 공격당하는 시점은?", "특정 웨이브(예: 5웨이브)에서 받는 피해량이 급증하는가?"
    • 활용: 기획 의도대로 난이도 곡선(Difficulty Curve)이 그려지는지 검증합니다. "3웨이브가 5웨이브보다 3배 더 아팠다" ➡️ 3웨이브 몬스터 조합을 너프하고 5웨이브를 버프하는 등 밸런스를 재조정합니다.
  • EnemyKillLog (몬스터 킬):
    • 목표: 웨이브별 몬스터 킬 통계.
    • 분석: "가장 많이 잡힌 몬스터는? (위협적 vs 잡기 쉬움)", "가장 적게 잡힌 몬스터는? (너무 강함 vs 스폰이 적음)"
    • 활용: 몬스터 스폰 테이블과 개별 몬스터의 스펙을 조절합니다.

3. 경제 및 플레이어 성장(Pacing) 분석 💰

  • OrePickLog / CoreGetOreLog (광물 획득/반납):
    • 목표: 웨이브별 자원 수급 속도(Pacing) 분석.
    • 분석: "플레이어가 한 웨이브 당 평균 몇 개의 광물을 획득/반납하는가?", "선호/기피하는 광물이 있는가?"
    • 활용: 자원 리스폰 속도, 업그레이드 비용(ForgeLog와 연계)과 비교하여 경제 밸런스를 조절합니다. "플레이어가 돈을 너무 빨리 벌어서 2웨이브 만에 풀업한다" ➡️ 업그레이드 비용을 높이거나 자원량을 조절합니다.
  • ForgeLog (업그레이드):
    • 목표: 선호 업그레이드 테크 트리 분석.
    • 분석: "모든 플레이어가 '쉴드 업그레이드'만 찍는가?", "아무도 '터렛 공격 속도' 업그레이드를 찍지 않는가?"
    • 활용: 인기 없는 업그레이드는 버프하고, 너무 사기적인(OP) 업그레이드는 너프하여 다양한 플레이 스타일을 유도합니다.

4. 시스템 검증 및 버그 추적 (보험 🛡️)

  • BlockLog, TileLog, CauseLog:
    • 목표: 이벤트 발생의 원인과 결과 추적.
    • 분석: "플레이어가 'Kamikaze_Explosion'에 죽었다" (CauseLog) vs "플레이어가 원인 불명으로 죽었다" (CauseLog가 누락됨).
    • 활용: 이게 바로 '보험'입니다. 원인 불명의 데미지, 좌표 밖에서 죽는 현상 등 온갖 버그를 추적하고 시스템이 정직하게 작동하는지 검증하는 데 사용됩니다.

결론

이제 저희 로거는 null 범벅이던 과거를 청산하고, PlayFab 분석에 최적화된 깔끔하고 강력한 데이터를 60초마다 안정적으로 전송하는 시스템으로 다시 태어났습니다.

1초마다 쌓일 PositionLog로 플레이어들의 동선 히트맵을 그려볼 생각, 그리고 DamageLog를 currentWave별로 그룹화해서 웨이브별 난이도를 분석해 볼 생각에 벌써부터 설레네요.

이제 정말, 데이터를 볼 시간입니다.