화면 아래 가장자리에 가로로 깔리는 Godot 4.7 위젯. 거품을 모아 바다를 꾸미고, 꾸밀수록 거품이 더 빨리 모인다. - 도킹 창: 투명·테두리 없음, 화면 폭 전체, 위/아래 가장자리 스냅, 높이 프리셋, 클릭 통과(모서리로 복귀), DPI 배율 보정 - 3/4 부감 격자: 구역당 9x6, 전체 45x6 = 270칸. 바닥은 위에서, 물체는 서 있는 모습으로 그리고 Y 정렬로 앞뒤를 가린다 - 구역 5개(얕은 바다 -> 열수구): 앞 구역을 꾸며야 다음이 열린다 - 칸이 곧 한계인 경제: 배치/이동/치우기(절반 환불), 여러 칸짜리 장식 - 저장: 30초 자동 저장, 백업 한 세대, 8시간 상한 오프라인 보상, 격자 이전 형식 저장본 자동 이전 - 스모크 테스트 60여 개 (test.ps1) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.7 KiB
Blub Blub 설계 메모
한 줄 요약
화면 아래 가장자리에 깔려 조용히 돌아가는 바닷속 스트립. 다른 창을 쓰는 동안 시선의 가장자리에 있고, 꾸미면 예뻐지고, 예뻐지면 더 빨리 자란다. 러스티의 농장/몬스터 빅팜 같은 "일하면서 곁눈질하는" 형태를 따른다.
핵심 설계 판단
칸이 곧 한계다. 클리커 장르는 보통 한 종류를 무한히 사서 가격 곡선으로만 속도를 조절한다. 그러면 배치는 장식이 되고, 꾸미기는 성장과 무관한 곁가지가 된다. 여기서는 격자 칸이 보유 개수를 직접 제한한다. "무엇을 어디에 놓을까"가 매 순간 진짜 고민이 되고, 자리를 늘리는 유일한 길이 다음 구역을 여는 것이라 꾸미기·성장·진행이 한 줄로 이어진다.
직교 격자 + 3/4 부감 시점을 쓴다. 아이소메트릭은 가로·세로 두 축이 모두 화면 높이를 먹기 때문에(높이 = (열+행) × 타일높이 / 2) 220px 스트립에서는 격자가 10×3으로 쪼그라든다. 직교 격자는 세로가 행 수만큼만 필요해서 45×6 = 270칸이 나온다.
그 위에 바닥은 위에서, 물체는 옆에서 그리는 고전적인 2D 관례를 얹었다. 러스티의 농장·스타듀밸리가 쓰는 방식이다. 완전한 탑다운은 물고기를 등에서만 보게 되고 구조물이 납작한 얼룩으로 보여서, 무엇인지 알아볼 수가 없었다.
구현은 단순하다. 노드의 원점을 발자국의 아래 가운데(물체가 바닥에 닿는 지점)에 두고
그림을 위(-Y)로 그린다. 그러면 Y 정렬만으로 앞줄이 뒷줄을 가리는 순서가 저절로 맞는다.
height(칸 높이의 배수)가 1을 넘으면 뒷줄을 가리므로, 이 값이 곧 배치의 무게가 된다.
구역은 왼쪽에서 오른쪽으로 깊어진다. 스트립이 가로로 길다는 형태를 그대로 진행 방향으로 썼다. 왼쪽 끝이 수면에 가까운 얕은 바다, 오른쪽 끝이 열수구다. 한 화면에 지나온 길과 앞으로 갈 길이 같이 보이기 때문에, 위젯을 흘깃 볼 때마다 "어디까지 왔는지"가 설명 없이 읽힌다.
다음 구역을 열려면 앞 구역을 꾸며야 한다. 해금 조건을 거품만으로 두면 플레이어는 앞 구역을 최소한만 건드리고 계속 오른쪽으로 달린다. 앞 구역의 아름다움을 조건으로 걸어서, 진도를 빼려면 반드시 지금 있는 바다를 가꾸게 했다.
꾸미기와 성장을 하나로 묶었다. 방치형 게임에서 꾸미기는 보통 "번 돈을 쓰는 곳"에 그치고, 그러면 꾸미기가 성장의 경쟁 상대가 되어 아무도 꾸미지 않는다. 그래서 모든 구매물이 아름다움(beauty) 수치를 가지고, 아름다움이 다시 전체 생산량에 곱해지도록 했다. 꾸미는 것과 최적으로 플레이하는 것이 같은 방향을 가리킨다.
아름다움 배율 = 1 + sqrt(아름다움) × 0.02
제곱근이라 초반에는 한 개 사는 것이 체감되고(아름다움 100 → 1.20배), 후반에는 완만해져서(10,000 → 3.0배) 인플레이션을 막는다.
수치
| 항목 | 값 | 위치 |
|---|---|---|
| 생물 가격 성장률 | ×1.15 (장르 표준) | Catalog.COST_GROWTH |
| 아름다움 배율 계수 | 0.02 | Catalog.BEAUTY_MULT_SCALE |
| 오프라인 상한 | 8시간 | SaveManager.OFFLINE_CAP_SECONDS |
| 오프라인 효율 | 50% | SaveManager.OFFLINE_RATE |
| 자동 저장 | 30초 | SaveManager.AUTOSAVE_INTERVAL |
| 상점 공개 조건 | 해금된 구역 + 가격의 50%를 벌어본 적 있음 | GameState.is_revealed() |
| 구역 수 | 5 (구역당 생물 2 + 장식 2) | Catalog.ZONES |
| 구역 해금 비용 | 30.0K / 3.00M / 400M / 40.0B | Catalog.ZONES[i].unlock_cost |
| 구역 해금 아름다움 | 앞 구역에 90 / 500 / 2.20K / 8.00K | Catalog.ZONES[i].require_beauty |
| 격자 | 구역당 9열 × 6행 (총 45×6 = 270칸) | Catalog.ZONE_COLUMNS / GRID_ROWS |
| 물체 높이 | 칸 높이의 0.75~1.95배 | 각 아이템의 height |
| 치울 때 환불 | 50% | Catalog.REFUND_RATE |
| 정보 바 자리 | 위쪽 30px는 격자에서 뺌 | Aquarium.GRID_TOP |
| 스트립 높이 | 160 / 220 / 300px | Main.HEIGHTS |
생물 10종·장식 10종을 구역당 2+2로 나눠 두었다. 한 구역을 70%쯤 채우면
다음 구역 해금 비용이 대략 20~90분치 생산량이 되도록 잡아 두었지만,
칸 제한이 들어오면서 곡선이 크게 달라졌으므로 실제로 플레이해 보며 다시 맞춰야 한다.
현재 밸런스는 첫 몇 시간을 촘촘하게 만드는 데 맞춰져 있고, 실제로 플레이해 보며
Catalog의 base_cost / bps / beauty 세 숫자만 조정하면 된다.
데이터 추가하는 법
autoload/catalog.gd의 배열에 딕셔너리 한 줄을 넣으면 상점 목록, 수조 렌더링,
저장 형식이 전부 따라온다. 다른 파일은 건드릴 필요가 없다.
{
"id": "shrimp", "name": "청소새우", "shape": "fish",
"desc": "바닥을 훑고 다닌다.",
"color": Color(0.9, 0.4, 0.4), "base_cost": 400.0, "bps": 3.0, "beauty": 5.0,
},
shape은 plant / rock(바닥 고정) / fish / glow(제자리 위를 떠다님) 중 하나이고,
scenes/aquarium/tile_item.gd가 형태별로 서 있는 모습을 직접 그린다.
size는 바닥에서 차지하는 칸(기본 1×1), height는 칸 높이의 몇 배로 서는지다.
물빛 오버레이가 덮이면 형태가 바닥에 묻히므로, 모든 면에 어두운 테두리를 두른다
(_fill()). 테두리를 빼면 무엇인지 읽히지 않는다.
아트
지금은 스프라이트 없이 _draw()로 도형을 그린다. 색과 형태만으로 구분되는 자리 표시용이다.
실제 아트가 생기면 assets/art/에 넣고 tile_item.gd를
Sprite2D 기반으로 바꾸면 된다(카탈로그에 texture 항목 추가).
바닥은 seabed.gd가 칸마다 그리고, 그 위에 water.gdshader가 물빛과
일렁이는 빛 그물을 덮는다. 깊이감은 이 오버레이의 murk가 만든다.
창 동작 (Windows)
- 투명·테두리 없음:
project.godot의per_pixel_transparency/allowed+size/transparent - 도킹:
screen_get_usable_rect()로 작업표시줄을 뺀 영역을 구해 폭 전체로 늘리고 위/아래 가장자리에 붙인다. 창을 끌어 옮기는 기능은 없다 — 위치는 계산되는 값이다. - 스트레치 모드는
disabled. 화면 폭이 얼마든 1:1 픽셀로 그려야 글씨가 흐려지지 않는다. - 셰이더는 화면에 닿는 쪽 모서리를 각지게, 바깥쪽만 둥글게 깎는다. 바탕화면에서 자라난 것처럼 보이게 하려는 것.
- 클릭 통과:
DisplayServer.window_set_mouse_passthrough(). 빈 배열이면 통과 해제, 폴리곤을 주면 그 안쪽만 입력을 받는다. 통과를 켜도 오른쪽 아래 모서리를 입력 영역으로 남겨 두어 되돌아올 길을 만든다.
아직 안 만든 것 (우선순위 순)
- 꾸미기 배치 자유도 — 지금은 산 물건이 자기 구역 안 랜덤 위치에 놓인다.
드래그로 옮기고 위치를 저장하면 "내 바다"라는 느낌이 훨씬 강해진다.
DecorSprite에 위치를 저장하고GameState.producers를 개수 대신 인스턴스 배열로 바꾸는 작업이 필요하다. - 두 번째 화폐(진주) — 아름다움에 비례해 아주 천천히 쌓이고, 특별한 장식만 살 수 있게. 거품 인플레이션과 분리된 장기 목표가 생긴다.
- 환생/이사 — 더 큰 수조로 옮기면서 영구 배율을 얻는 장르 표준 루프.
- 소리 — 물소리와 거품 소리. 위젯이라 기본은 음소거가 맞을 듯.
- 트레이 아이콘 — Godot 기본 기능에 없어서 GDExtension이나 별도 런처가 필요하다. 당장은 작업표시줄 창으로 충분.
- 폭을 화면 일부만 쓰기 — 지금은 항상 폭 전체다. 왼쪽/가운데/오른쪽 정렬과 폭 비율 선택이 있으면 넓은 모니터에서 덜 부담스럽다.
한글 문장
구역 이름을 문장에 끼워 넣는 곳이 여럿이라 조사가 어긋나기 쉽다
("켈프 숲를 더 꾸며야 해요"). scripts/korean.gd가 마지막 글자의 받침을 보고
을/를·이/가·은/는을 골라준다. 새 문장을 쓸 때도 이걸 거쳐야 한다.
저장 파일 보호
SaveManager.persistence_enabled를 끄면 저장/자동저장/종료 시 저장이 모두 멎는다.
스모크 테스트가 이걸 끈다. 끄지 않으면 테스트가 끝나면서 초기화된 상태를
실제 저장 파일에 덮어써 진행도가 날아간다.