그동안 다양한 분야를 탐색하며 여러 직군을 경험해왔다.
게임 원화가를 꿈꾸며 시각디자인학과에 입학했고, 대학 시절에는 인디게임 개발 팀을 꾸려 팀장이자 기획자로 게임 제작에 참여했다.
이후 게임 회사에 기획자로 입사하여 1년간 근무하면서 PM 업무를 일부 맡았고, 지표 분석과 스프린트 운영 방법에 대한 이해를 넓혔다.
그러다 개발에 대한 관심이 커져 네이버 부스트캠프에 참가해 웹 프론트엔드 과정을 수료했다. 그 결과 디자인, 개발, 기획을 모두 경험할 수 있게 되었다.
이런 배경을 바탕으로 PM 취업을 목표로 준비하던 중, 직접 혼자서 게임을 만들어보자는 결심을 했다. 게임을 기획하고 제작하는 과정에서 게임 개발 전 과정에 대한 이해도를 높이고, 마케팅과 지표 분석, 운영까지 전 과정을 경험하면서 실제 유입을 만들어낼 수 있다면, 단순한 이력 이상의 배움과 통찰을 얻을 수 있을 것이라 생각했다. 결국 이러한 생각이 행동으로 이어졌고, 지금의 도전을 시작하게 되었다.
게임 기획
이전에 Despotism 3K라는 게임을 인상깊게 했었다. 플레이어가 로봇이 되어서 인간공장을 운영하는 게임이었다. (스팀 링크: https://store.steampowered.com/app/699920/Despotism_3k/)
해당 게임의 대략적인 시스템은 다음과 같다:
- 20초마다 필요한 전력량이 존재
- 이 전력량을 채우기 위해 인간을 시설에 배치해야 함
- 인구는 체력이 있고 체력이 다 닳으면 죽어버림
- 휴게실: 인구가 생성되는 곳 & 인구 체력을 채우는 곳 (식량 소비)
- 전력 생산실: 인구가 쳇바퀴를 돌며 체력을 소비하고 전력을 생산
- 식량 생산실: 인구가 체력을 소비하고 식량을 생산
- 인구 융해: 인구를 녹여서 죽이고 에너지 획득 가능
- 중간중간 이벤트가 발생 -> 선택에 따른 다양한 상황 발생
이 게임이 재밌긴 한데 19금 묘사에 잔인하고 고어한 장면이 있어서 전 연령층이 즐기기에는 한계가 있었다. 그래서 동일한 시스템과 구조를 유지하되, 캐주얼하게 재해석을 해보면 재밌겠다는 생각이 들었다.
플레이어는 고대 여신이 되어 신도들(인간)을 관리한다. 신도들은 시설에서 노동하며 여신의 힘인 신성을 만들어낸다. 게임의 목표는 가능한 한 오랫동안 문명을 존속시키는 것이고, 게임 오버 조건은 인구가 0명이 되는 것이다.즉 착취와 생존 요소를 유지하되 여신과 신도라는 관계로 변형시킨 것이다.
시설은 다음과 같다.
- 밭: 식량 생산 / 배치된 인구 체력 감소
- 신전: 신성 생산 / 배치된 인구 체력 감소
- 식당: 인구 체력 회복 / 식량 소모
- 거주지: 특정 시간마다 인구 생산
Despotism 3K처럼 고대 여신과 어울리는 이벤트를 30초마다 발생시켜서 선택에 따라 보상이나 처벌을 획득할 수 있게 했다. 이 이벤트는 게임 플레이 중간에 변수를 주어 게임에 변수가 되어 반복적인 게임 플레이를 유도하는 중요 장치였다.
상세 기획은 다음에서 확인할수 있다. (기획서 링크: https://www.notion.so/225ab3ee1cc1802cb31fc69bd2ecaa69)
개발 과정
MVP 제작에는 한 달 반 정도의 시간이 걸렸다.
- 7월 3일 ~ 8월 15일: MVP 개발
- 8월 16일 ~ 9월 1일: 내부테스트, 출시 준비, 수익화 추가, 광고 소스 만들기
- 9월 2일 ~: 게임 재미 개선, 광고 집행 시작
진행상황은 노션에 보드를 만들어서 시작전 → 진행중 → 완료 → 아카이브 형태로 나눴고, 진행중에 오늘 작업할 카드를 배치해서 스프린트를 진행했다. 스프린트 단위는 1주일이었다.
(노션 링크: https://www.notion.so/225ab3ee1cc180cd9f26d8720fbf8f8e)
스프린트 요약:
- 스프린트 1: 게임 및 이벤트 기획
- 스프린트 2: 파라미터 시스템, 이벤트 시스템, 시설 개발, 사람 유닛 개발
- 스프린트 3: UI 디자인, 유닛 디자인, 여신 디자인
- 스프린트 4: 디자인 적용, 업그레이드 시스템, 여신 선택 시스템, 상태 시스템 개발, 밸런싱
- 스프린트 5: 이벤트 추가, 사운드 적용, 온보딩(튜토리얼) 추가, 게임 로그 추가, 다국어 지원, 설정 추가
- 스프린트 6: 광고 지면 추가, 로그 설계 및 추가, GA 연결
- 스프린트 7: 리워드 광고 추가, 내부 테스트, 광고 영상 찍기
개발
엔진은 Godot 엔진을 사용했다. 오픈소스에다가 파이썬 문법을 기반으로 된 엔진이라고 해서 호기심에 사용해보았다. Godot 엔진에 대한 경험이 없었던 지라 간단한 Godot 강의 영상 몇 개를 보고 바로 시작했다. (Claude Code의 도움을 크게 받았다.) Godot 경험이 전무했지만, 직접 만들어가며 배우니 훨씬 빠르고 재미있었다. 아래는 공부한 내용들을 정리해 보았다.
학습1: Godot 엔진의 구조 이해
Godot의 구조를 이해하지 못하고 짜다 보니 반복적으로 등장하는 get_tree()가 무엇인지 궁금해졌다.
Godot 엔진의 구조:
- 저수준 API: OS 클래스 및 서버, 드라이버가 존재
- MainLoop: 미들웨어 저수준과 고수준을 연결하는 역할
- 게임 시작할 때 초기화를 진행
- 유휴 콜백: 매 프레임마다 “지금 뭐 할까?” 묻기
- 고정 콜백: 물리 계산을 위해 정해진 간격으로 “물리 업데이트!”
- 입력 처리
- 게임엔진
SceneTree→ 우리가 조작하는 것들
SceneTree 구조:
씬 트리 하위에는 루트 뷰포트가 있고 하위에 게임 씬들이 붙는다. 루트 뷰포트는 모든 화면의 최상위 부모다:
get_tree().root를 통해 접근 가능- 사용자가 직접 만들 수 없고, 씬 트리가 자동으로 만들어줌
- 여기에 연결된 것들만 화면에 보임, 현재 실행중인 씬만 루트 뷰포트에 붙는다
SceneTree
└──루트 뷰포트
└── Game 씬 (게임플레이)
├── Player
├── Enemies
├── UI
└── ...노드 생명주기:
_init(): 노드 인스턴스가 생성될 때(생성자 역할) 호출_enter_tree(): 노드가 씬 트리에 추가되는 즉시(부모→자식 순서로) 호출_ready(): 씬 트리 구조상 모든 자식 노드까지 추가가 완료된 뒤(자식→부모 순서로) 호출_process(delta)/_physics_process(delta): 씬 트리에 있는 동안, 게임이 실행되면 매 프레임마다 계속 호출_exit_tree(): 노드가 씬 트리에서 제거될 때 자동 호출
_enter_tree는 트리 상위에서 아래로 내려가고, _ready는 반대로 아래 → 위로 올라간다. 부모가 자식을 호출하려면 자식이 미리 준비되어있어야 하기 때문
씬을 전환할 때는 _exit_tree를 통해 기존 씬을 제거한 뒤 위 과정을 반복한다. 새 씬이 로딩되는 동안 게임이 완전히 멈추기 때문에 주의해야 한다.
스크립트는 노드가 생성되어 씬 트리에 추가된 이후에 부착된다.
스크립트가 부착된다는 의미는 해당 크립트가 메모리에 로드(컴파일) 된다는 뜻이다.
이후 위에 라이프 사이클에 맞는 메서드가 호출된다.
실제로 개발하면서는 주로 _ready만 활용했다. 초기화 및 시그널 연결을 담당했고, 이후 이벤트 기반으로 로직을 처리했다. _process를 쓸 정도로 연속 계산이 필요한 경우는 거의 없었다.
학습2: 시그널
시그널은 이벤트 시스템으로 동작한다: 발신자 → 시그널(이벤트) → 수신자
시그널은 메모리에 테이블 형식으로 저장되어 있다. (시그널-수신자)
이벤트가 발생시 모든 수신자에게 전달한다.
이후 독립적으로 이벤트를 처리한다.
시그널을 사용하니 기존 코드 수정없이 기존 이벤트를 구독하기만 하면 되서 너무 편리했다.
- _ready(): 시그널 연결
- _on_이벤트(): 이벤트 처리
- emit(): 이벤트 발생
- connect(): 이벤트 구독
# ParameterManager.gd - 이벤트를 던지는 쪽
signal population_increased(amount: int)
signal population_decreased(amount: int)
func change_population(amount: int) -> void:
var old_population = population
if famine and amount > 0:
LogManager.display(tr("parametermanater_log1"))
return
population = max(0, population + amount)
parameter_changed.emit("population", population)
if amount > 0: population_increased.emit(amount)
if amount < 0: population_decreased.emit(amount)
check_game_over()# HumanManager.gd - 이벤트를 받는 쪽
extends Node
func _ready():
ParameterManager.population_increased.connect(spawn_humans)
ParameterManager.population_decreased.connect(remove_humans)
call_deferred("initialize_population")
func spawn_humans(amount: int):
var residence = get_residence_facility()
for i in amount:
if residence:
residence.spawn_human()
else:
print("[HUMAN_MANAGER] Residence를 찾을 수 없습니다.")
func remove_humans(amount: int):
remove_weakest_humans(-amount)하지만 문제도 있었다.
게임 플레이 타임이 길어질수록 씬 변경이 너무너무 오래 걸리는 것이다…
바로 시그널 중첩 연결로 인한 메모리 누수 문제였다.
시스템 중 파라미터 매니저는 각 자원을 모두 관리하는 만큼 중앙에 위치하는 스크립트다. 다른 스크립트에서 자원을 변동시킬 때마다 시그널을 받아와서 check_game_over(), check_status_conditions() 등으로 플레이어 상황을 업데이트 해놨다.
signal parameter_changed(param_name: String, new_value: int)
signal population_increased(amount: int)
signal population_decreased(amount: int)
signal year_changed(new_year: int)
signal status_changed(status_name: String, active: bool)
signal game_over(reason: String)이후 시그널을 정리하지 않은채로 게임을 계속해서 반복 플레이를 하니 게임 시작버튼을 누를때 로딩이 너무나도 오래 걸렸다. 알고보니 시그널을 해지하지 않으면 계속 생겨서 그런 것이었다.
똑같은 시그널이 계속 생기고 또 생겨서 메모리 누수가 생긴 것이다.
자동으로 게임 내 모든 시그널을 해지하는 방법이 없었을까 고민했는데 찾아도 나오지 않아 아래처럼 일일이 시그널 연결을 해제해줬다.
func reset_all_signals():
disconnect_all_connections(parameter_changed)
disconnect_all_connections(population_increased)
disconnect_all_connections(year_changed)
disconnect_all_connections(status_changed)
disconnect_all_connections(game_over)
func disconnect_all_connections(signal_obj: Signal):
for connection in signal_obj.get_connections():
signal_obj.disconnect(connection.callable)나중에 알아보니 Godot은 queue_free()나 free()로 노드를 제거할 경우, 그 노드와 관련된 시그널 연결도 자동으로 정리한다. 문제는 게임 오브젝트가 남아있는데 연결만 계속 추가되는 경우인데, 이건 엔진이 자동으로 정리하지 않는다. 이유는 같은 시그널을 여러 군데서 동시에 쓸 수도 있기 때문이다.
학습3: GDScript의 런타임 동적 변경
흥미로웠던 점은 GDScript가 인터프리터 언어라 런타임 중에도 스크립트를 교체할 수 있다는 것이었다. Unity에서는 불가능한 일이다. 이번 프로젝트에서는 실제로 활용하지는 않았지만, 이런 유연함은 Godot의 큰 장점 중 하나라 생각된다.
# AIBehaviorManager.gd
extends Node
func change_ai_behavior(ai_node: Node, behavior_type: String):
var behavior_script = load("res://ai_behaviors/" + behavior_type + ".gd")
ai_node.set_script(behavior_script)
print("AI 행동 패턴 변경: ", behavior_type)
# 사용 예시
func _on_difficulty_changed(new_difficulty: String):
var all_enemies = get_tree().get_nodes_in_group("enemies")
for enemy in all_enemies:
match new_difficulty:
"easy": change_ai_behavior(enemy, "PassiveAI")
"hard": change_ai_behavior(enemy, "AggressiveAI")
"nightmare": change_ai_behavior(enemy, "SmartAI")하여튼 Godot으로 개발하는 건 정말 재밌었다. 다음 개발 때는 Unity를 사용해보려고 한다. 실제로 두 엔진을 모두 써봐야 각각의 장단점을 제대로 비교할 수 있을 것 같아서다.
디자인
디자인은 피그마로 진행했다. 참고한 게임 디자인은 레전드 오브 슬라임과 수확의 정석이다.
아트 스타일은 레전드 오브 슬라임을 많이 참고했고, 수확의 정석에서 UI배치를 참고했다.
UI와 캐릭터는 모두 벡터로 제작했으며, 작업물은 링크에서 확인할 수 있다. (피그마 링크: https://www.figma.com/design/5o3iQfNFI5FkFIFY70cCwT/Become_goddness?node-id=0-1&p=f&t=IflVjDRoGdmJr8su-0)
애니메이션은 Rive를 사용했다. 포토샵 외에는 툴 경험이 없었지만, Rive가 직관적이어서 금세 적응할 수 있었다.
게임 디자인을 하면서 배웠던 점은 다음과 같다:
- 에셋을 내보낼 때 같은 크기의 마스크(캔버스) 안에 정리하지 않으면, 엔진으로 가져왔을 때 크기가 들쭉날쭉해져서 정렬/배치가 깨진다는 점을 깨달았다. 마스크를 꼭 해야한다..
- 처음에는 디자인 적용 먼저 하지 않고 대충 개발부터 하고 이후 디자인한 에셋으로 바꿨는데 같은 일을 2번 하게 된다는 것을 깨달았다. 역시 기획 → 디자인 → 개발 순으로 하는 게 맞다. 같은 일을 두 번 하고 싶지 않다면 말이다.
로그 설계와 데이터 분석
MVP가 어느정도 마무리 되고 나서는 로그를 추가했다. 퍼널을 통해 유저 행동을 파악하기 위함이다.
나는 Google Analytics를 사용하기로 했다.
GA는 이벤트 명 {}키값 형태로 파라미터를 보내면 된다. 웹 스트림과 앱 스트림이 있는데 둘 다 시도해봤는데 웹은 성공했는데 앱은 실패해서 웹스트림으로 했다. (웹 스트림으로 하니 국가 아이디가 추적이 안 되는 것 같기도 하고… 아직 파악중이다)
게임의 사용자 플로우에 따른 로그
- 게임 시작 (first_visit, appstart)
- 여신 선택 (goddess_chosen)
- 게임 시작버튼 클릭 (game_start)
- 튜토리얼 시작 (tutorial_start)
- 튜토리얼 진행 (tutorial_progress)
- 튜토리얼 종료 (tutorial_complete)
- 게임 진행 (yearly_status_str)
- 게임 종료 (game_over)
yearly_status_str는 유저들이 실제로 어느 자원에서 막히는지, 몇 년까지 버티는지 등을 파악하기 위함이다. 이를 보고 밸런싱을 추후 진행할 예정이다.
| 카테고리 | 트리거 시점 | 로그 키 예시 (add_design_event) | 목적 / 설명 |
|---|---|---|---|
| 유입 | 게임을 최초 실행했을 때(유저 기준) | first_visit{user_id, play_count, session_id, ga_session_id, ga_session_number, engagement_time_msec} | |
| 앱 실행 시마다 발생(중복 가능) | app_start{user_id, play_count, session_id, ga_session_id, ga_session_number, engagement_time_msec} | ||
| 여신 선택 | 구매 버튼 클릭 시 | goddess_purchase{goddness_name} | 구매 전환률 분석 |
| 게임 시작 시 선택된 여신 | goddess_chosen{goddness_name} | 실제 사용 여신 통계 | |
| 게임 시작 | 게임 시작 버튼 클릭 시 | game_start{play_count} | 세션당 플레이 횟수 |
| 튜토리얼 | 튜토리얼 시작 | tutorial_start | 시작률 파악 |
| 튜토리얼 각 스텝 완료 | tutorial_progress{step} | 중도 이탈률 파악 | |
| 튜토리얼 완료 시 | tutorial_complete | 완주율 분석 | |
| 자원 현황 | 매해 자원 기록 | yearly_status_str{year, faith, food, population} | 자원 밸런싱 |
| 게임 종료 | 종료 시 자원 상태 | game_over{faith, food, population, year} | 종료 상황 분석 |
이렇게 추가한 이벤트를 기반으로 GA로 보고서를 만들어서 튜토리얼 퍼널 이탈률을 분석해보기도 했다.
튜토리얼 단계별로 보고서를 만들었는데 2 & 4 단계에서 많은 이탈이 일어난다는 것을 발견했다.
- 튜토리얼 2단계
- 진행 조건: 화면에 표시된 말풍선을 반드시 클릭해야 다음 단계로 넘어갈 수 있었음.
- 문제: 일부 유저가 이 조건을 인지하지 못해 진행이 막히고 이탈 발생.
- 이탈률: 48%.
- 튜토리얼 4단계
- 진행 조건: 드래그 조작을 처음 배우는 단계.
- 문제: 어떤 대상을 드래그해야 하는지가 불명확해 유저 혼란 발생.
- 이탈률: 53%.
튜토리얼 2단계는 클릭하지 않아도 3초 뒤에 자동으로 다음 단계로 넘어가도록 수정했으며, 4단계는 드래그할 대상 위에 손 모양 아이콘 애니메이션을 추가하여 직관적으로 안내했다.
그 결과 튜토리얼 2단계 이탈률이 48% -> 17%로 감소했고, 4단계는 53% -> 0%로 해소되었따.
작은 UX 장벽을 제거하고 보완하여 이탈률이 크게 줄어들었다.
출시
이후 완성된 MVP를 스토어에 업로드하였다. 구글 플레이스토어, 앱스토어 두개 다 고려해봤을때 앱스토어 비용이 다소 부담스러워서 구글 플레이 스토어 부터 업로드를 했다. 이후 수익이 나오면 앱스토어 업로드를 고려해볼 생각이다.
구글 플레이스토어는 비공개 테스트를 거쳐야만 스토어에 등록할 수 있다. 14일간 12대를 매일 실행해야하는건데 이게 여간 까다로운게 아니다.
지인을 동원해서 하기에는 너무 어렵다고 느껴져서 크몽 전문가에게 맡겼다. 걱정을 많이 했는데 다행히 한번만에 승인이 되었다.
지인 테스트 및 재미 보강
스토어에 올린 뒤에 주변 지인에게 테스트를 부탁했다.
평가는 냉정했다 대체로 재미가 없다고 말했다. 그리고 게임 목표가 뭐냐고 묻는 사람이 많았다.
목표를 1차원적으로 바로 인지하지 못하는 게 문제였다. 왜 그럴까 생각하며 모티브로 삼은 Despotism 3K를 다시 살펴보니 왼쪽에 큰 게이지로 상시 전력량을 보여주며 다음에 닳을 전력량을 계속 보여주고 있었다. 이에 게이머는 상시 내가 얼마나 더 많은 전력을 생산해야 하는지 즉각적으로 파악이 가능했다.
해당 게임과 달리 내 게임의 플랫폼은 모바일이고 이미 다양한 UI로 인해 복잡한 상황이었다. 그래서 게이지나 다른 시각적인 표현 없이 단순한 박스로 표기했다(수확의 정석을 참고했다).
이게 있고 없고의 차이가 생각보다 컸다. 결국 매세기(20초)마다 신성이 줄어들고 신성이 없으면 게임이 오버가 된다는 새로운 제약이자 단기적인 목표를 추가하자 긴장감과 동기부여가 올라갔다.
수익화와 마케팅
MVP 단계라서 인터스티셜 광고, 자원 리워드 광고, 배너 광고만 추가했다.
- 인터스티셜 광고: 2판째부터 게임 오버 → 로비로 가는 사이에 삽입
- 리워드 광고: 게임 자원이 부족해지면 옆에 팝업 형태로 띄움 → 리워드 시청 이후에 자원 제공
3.8달러라는 귀여운 수익이 생겼는데 바로 광고 게재 제한되었다. 테스트할때도 테스트 id를 이용해 광고를 확인했는데 억울할 뿐이다. 알아보니 빈번하게 발생하는 일이라고 한다.
기다리면 30일 내에 자동으로 풀린다고 하여 막연히 기다리고 있다. 언제 풀릴런지…
광고 게재 제한 먹기 전에는 마케팅을 진행했었다. 게임 플레이 영상을 토대로 소재를 하나만 제작했다. 인구 유닛이 점점 폭발적으로 증가하는 영상이고 상단에 “너는 얼마나 인구를 만들 수 있냐?”라는 식의 도발 문구를 넣었다. 인구 애니메이션에서 나오는 소리로 ASMR 효과를 노렸다.
처음에는 메타 광고를 돌리려고 했는데 뭣 모르고 계정 열자마자 광고 올렸다가 영구 제재 당해버렸다. 알고 보니 페북 페이지도 만들고 계속 게시글도 쓰면서 어느 정도 신뢰를 쌓고 했어야 했다고 한다…
그래서 구글 애즈로 시작하기로 했다.
Google Ads 결과
- 비용: 12,041원
- 평균 CPC: 317원
- 노출수: 1,008
- 클릭수: 38
- 다운로드: 8
- 클릭률(CTR): 3.52%
미국, 캐나다, 호주, 스위스 등 광고 단가가 높은 나라들로만 돌렸다. 클릭수 최소 100은 되어야 지표를 볼 만하다는데 광고 게재 제한을 먹어버려서 일단은 일시정지 상태다. 게재 풀리면 계속 진행해볼 예정이다.
앞으로 방향성
광고 게재 제한이 풀리면 다시 광고를 열어서 클릭이 100이 될 때까지 기다려볼 예정이다.
그 이후 지표를 보고 액션을 취할 예정이다.
노출 → 클릭이 낮으면 → 광고 개선
- 광고 그룹 추가해보기
- 다양한 광고 소재 만들어서 추가하기
- 다른 지역도 추가 (한국)
클릭 → 설치가 낮으면 → 스토어 전환율 개선 - 리뷰 요청 팝업 추가: 유저 긍정 경험 구간 찾아서 추가
- 다양한 앱 아이콘, 플레이 이미지로 A/B 테스트 → 스토어 최적화하기
둘 다 높으면 → 광고 계속하면서 인게임 지표를 보면서 개선
회고
이번에 처음으로 게임 프로젝트를 처음부터 끝까지 진행하면서 부족한 점을 많이 느낄 수 있었다.
잘한점과 아쉬웠던 점을 정리해보면 다음과 같다.
잘한 점
- 체계적인 프로젝트 관리: 스프린트 방식으로 진행하여 목표와 일정을 명확히 관리
- 데이터 기반 개선: GA를 활용한 튜토리얼 퍼널 분석으로 실질적인 개선 달성
- 전 과정 경험: 기획부터 개발, 디자인, 마케팅까지 게임 개발의 전체 파이프라인 경험
- 기술 학습: Godot 엔진의 구조와 시그널 시스템을 깊이 있게 이해
아쉬운 점
- 워크플로우 비효율: 디자인 먼저 하지 않고 개발부터 시작해 같은 작업을 두 번 함
- 마케팅 준비 부족: 메타 광고 계정 제재, 애드몹 게재 제한 등 사전 지식 부족
- 게임 재미 요소: 지인 테스트에서 재미 부족 피드백, 목표 명확성 부족
배운 것들
- 기획 → 디자인 → 개발 순서의 중요성
- 시그널 시스템의 메모리 관리 필요성
- 사용자 퍼널 분석을 통한 UX 개선의 효과
- 광고 플랫폼별 특성과 진입 장벽
- 모바일 게임에서의 긴장감과 목표 명확성의 중요성
다음 프로젝트에서는 이번 경험을 바탕으로 더 체계적이고 효율적인 개발을 진행할 수 있을 것이라 생각한다. 또한 Unity 엔진으로 전환하여 Godot과 비교하면서 두 엔진의 장단점을 직접 체험해보는 것도 좋은 학습이 될 것이다.