바탕화면 바닷속 방치형 위젯 첫 커밋
화면 아래 가장자리에 가로로 깔리는 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>
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
# 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`의 배열에 딕셔너리 한 줄을 넣으면 상점 목록, 수조 렌더링,
|
||||
저장 형식이 전부 따라온다. 다른 파일은 건드릴 필요가 없다.
|
||||
|
||||
```gdscript
|
||||
{
|
||||
"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()`.
|
||||
빈 배열이면 통과 해제, 폴리곤을 주면 **그 안쪽만** 입력을 받는다.
|
||||
통과를 켜도 오른쪽 아래 모서리를 입력 영역으로 남겨 두어 되돌아올 길을 만든다.
|
||||
|
||||
## 아직 안 만든 것 (우선순위 순)
|
||||
|
||||
1. **꾸미기 배치 자유도** — 지금은 산 물건이 자기 구역 안 랜덤 위치에 놓인다.
|
||||
드래그로 옮기고 위치를 저장하면 "내 바다"라는 느낌이 훨씬 강해진다.
|
||||
`DecorSprite`에 위치를 저장하고 `GameState.producers`를 개수 대신
|
||||
인스턴스 배열로 바꾸는 작업이 필요하다.
|
||||
2. **두 번째 화폐(진주)** — 아름다움에 비례해 아주 천천히 쌓이고,
|
||||
특별한 장식만 살 수 있게. 거품 인플레이션과 분리된 장기 목표가 생긴다.
|
||||
3. **환생/이사** — 더 큰 수조로 옮기면서 영구 배율을 얻는 장르 표준 루프.
|
||||
4. **소리** — 물소리와 거품 소리. 위젯이라 기본은 음소거가 맞을 듯.
|
||||
5. **트레이 아이콘** — Godot 기본 기능에 없어서 GDExtension이나 별도 런처가 필요하다.
|
||||
당장은 작업표시줄 창으로 충분.
|
||||
6. **폭을 화면 일부만 쓰기** — 지금은 항상 폭 전체다. 왼쪽/가운데/오른쪽 정렬과
|
||||
폭 비율 선택이 있으면 넓은 모니터에서 덜 부담스럽다.
|
||||
|
||||
## 한글 문장
|
||||
|
||||
구역 이름을 문장에 끼워 넣는 곳이 여럿이라 조사가 어긋나기 쉽다
|
||||
("켈프 숲를 더 꾸며야 해요"). `scripts/korean.gd`가 마지막 글자의 받침을 보고
|
||||
을/를·이/가·은/는을 골라준다. 새 문장을 쓸 때도 이걸 거쳐야 한다.
|
||||
|
||||
## 저장 파일 보호
|
||||
|
||||
`SaveManager.persistence_enabled`를 끄면 저장/자동저장/종료 시 저장이 모두 멎는다.
|
||||
스모크 테스트가 이걸 끈다. 끄지 않으면 테스트가 끝나면서 초기화된 상태를
|
||||
실제 저장 파일에 덮어써 진행도가 날아간다.
|
||||
Reference in New Issue
Block a user