From 375b340c475e3e6f5dfd61f715c630e5eec1936a Mon Sep 17 00:00:00 2001 From: ttipo Date: Wed, 2 Sep 2026 14:49:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AC=BC=EA=B3=A0=EA=B8=B0=EA=B0=80=20?= =?UTF-8?q?=EC=A7=91=EC=9D=84=20=EB=B2=97=EC=96=B4=EB=82=98=20=EC=B8=B5=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EB=A5=BC=20=ED=97=A4=EC=97=84=EC=B9=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 집 둘레를 타원으로 돌던 것을 걷어냈다. 관계는 분명했지만 꾸미기 게임에는 어울리지 않았다 -- 말뚝에 묶인 것처럼 보였고, 집을 여러 채 지으면 화면이 회전목마 여러 대가 된다. 집은 정원을 세는 단위로만 남고, 헤엄치는 범위는 그 층의 격자 전체다. 제 집 앞에서 시작해 거기서부터 어디로든 간다. 알고리즘은 방향 흔들기다. 목표 지점을 따로 두지 않고 진행 각도만 매 틀 조금씩 흔든다. 그것만으로 충분히 어슬렁거리는 것처럼 보이고 목표를 찍는 방식보다 싸다. 테두리는 닿기 전에 미리 각도를 반사시킨다. 종류마다 다니는 방식을 갈랐다. 같은 식으로 움직이면 여러 마리가 한 화면에 있을 때 움직임이 무늬가 된다. - 물고기: 빠르고 크게 돌아다닌다 - 게: 세로 성분을 12%로 눌러 바닥을 짚고 걷는다 - 해마: 가로 성분을 35%로 눌러 거의 제자리에서 오르내린다 - 새우: 1~3초마다 0.35초 동안 3.2배로 튄다 - 해파리: 물살에 밀리듯 아주 천천히 화면 위쪽(뒷줄)일수록 작고 흐리게 그려 앞뒤를 읽게 했다. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 3 +- docs/DESIGN.md | 29 +++++++ scenes/aquarium/aquarium.gd | 20 +++-- scenes/aquarium/tile_item.gd | 150 +++++++++++++++++++++++++++-------- 4 files changed, 158 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index a78815a..1dc2f26 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ tests/smoke.tscn/.gd 경제 로직 스모크 테스트 1. 바다에 있는 **생물**이 시간에 따라 알아서 **거품**을 모은다. 클릭으로 버는 길은 없다. 그래서 새로 시작하면 **항아리 집 한 채와 거품돌·구피 한 마리씩**이 이미 있고, 거품 20을 쥐고 시작한다. -2. 생물은 **집**에 산다. 집에는 두 갈래가 있다 — 돌·해초·산호처럼 **자연 서식지**(싸고 적게 들인다)와 +2. 생물은 **집**에 산다. 다만 집은 몇 마리를 들일 수 있는지 세는 단위일 뿐, + 헤엄치는 자리는 **층 전체**다. 집에는 두 갈래가 있다 — 돌·해초·산호처럼 **자연 서식지**(싸고 적게 들인다)와 항아리·난파선·석주처럼 **지어 올린 것**(비싸고 정원이 넓다). 더 키우려면 먼저 집을 지어야 한다. 집은 칸을 차지하고 거품을 벌지 않는다. 자리와 **아름다움**을 준다. 3. 생물을 들이면 초당 생산량과 아름다움이 오른다. 비싼 생물일수록 한 마리가 더 번다. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 64f2c32..f64cd08 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -417,3 +417,32 @@ F11이나 메뉴로 창을 화면 전체로 키운다. 그냥 창만 키우면 또 `_make_fish()`가 `fish`와 `glow`만 궤도에 올리고 있었다. 고착생물이 생물 쪽에 있던 시절의 가드인데, 새로 넣은 게·해마·새우가 여기 걸려 제자리에 굳어 있었다. 고착생물이 전부 집으로 갔으므로 가드 자체를 걷어냈다. + +## 생물의 움직임 + +처음에는 생물이 제 집 둘레를 타원으로 돌았다. 집과 생물의 관계는 분명해졌지만 +꾸미기 게임에는 어울리지 않았다 — 물고기가 말뚝에 묶인 것처럼 보였고, +집을 여러 채 지으면 화면이 회전목마 여러 대가 된다. + +지금은 **층 전체를 자유롭게 돌아다닌다.** 집은 정원을 세는 단위로만 남고, +헤엄치는 범위는 그 층의 격자 전체다(`_roam_bounds`). 제 집 앞에서 시작해 +거기서부터 어디로든 간다. + +**알고리즘은 방향 흔들기(wander)다.** 목표 지점을 따로 두지 않고 진행 각도만 +매 틀 조금씩 흔든다. 그것만으로 충분히 어슬렁거리는 것처럼 보이고, +목표를 찍는 방식보다 훨씬 싸다. 테두리는 닿기 전에 미리 각도를 반사시킨다 -- +벽에 붙어 비비는 것보다 자연스럽다. + +**종류마다 다니는 방식이 다르다.** 같은 식으로 움직이면 여러 마리가 한 화면에 +있을 때 움직임이 무늬가 되어 버린다. + +| | 속도 | 노는 높이 | 특징 | +| --- | --- | --- | --- | +| 물고기 | 빠름 | 물속 전체 | 크게 돌아다닌다 | +| 게 | 보통 | 바닥께 | 세로 성분을 12%로 눌러 바닥을 짚고 걷는다 | +| 해마 | 느림 | 위아래 넓게 | 가로 성분을 35%로 눌러 거의 제자리에서 오르내린다 | +| 새우 | 느림 + 돌진 | 바닥께 | 1~3초마다 0.35초 동안 3.2배로 튄다 | +| 해파리 | 아주 느림 | 위쪽 | 물살에 밀리듯 | + +**앞뒤 단서**: 화면 위쪽(뒷줄)일수록 조금 작고 흐리게 그린다(`_apply_depth`). +3/4 시점에서 깊이를 읽게 해주는 유일한 신호라 없으면 평면처럼 보인다. diff --git a/scenes/aquarium/aquarium.gd b/scenes/aquarium/aquarium.gd index 6dc3f5c..5e8a649 100644 --- a/scenes/aquarium/aquarium.gd +++ b/scenes/aquarium/aquarium.gd @@ -55,8 +55,6 @@ const LAYER_SLIDE := 0.32 ## 높이를 전부 격자에 주면 칸만 거대해지고 보이는 열은 오히려 줄어든다. const SHOWCASE_GRID_RATIO := 0.45 const DRAG_THRESHOLD := 5.0 -## 물고기가 집을 한 바퀴 도는 속도(라디안/초). 위젯이라 눈에 거슬리지 않게 느리다. -const ORBIT_SPEED := 0.5 var _rng := RandomNumberGenerator.new() var _lock_reasons := {} ## 구역 인덱스 -> 사유 Label @@ -463,14 +461,9 @@ func _make_fish(entry: Dictionary) -> void: items.add_child(node) node.setup(d, "producer", -1, _fish_area(home, cell, slot), cell, _rng) - # 생물은 모두 집 둘레를 돈다. 고착생물(해초·거품돌)은 이제 집 쪽으로 옮겼으므로 - # 여기 들어오는 것은 전부 헤엄치거나 걷는 해양동물이다. - var house_index := int(entry.house) - var cap: int = maxi(Catalog.capacity_of(String(GameState.houses[house_index].id)), 1) - # 집마다 도는 방향을 바꿔 여러 채가 한 화면에 있어도 단조롭지 않게 한다. - var dir := 1.0 if house_index % 2 == 0 else -1.0 - node.set_orbit(_orbit_center(home, cell), _orbit_radius(home, cell), - TAU * float(slot) / float(cap), ORBIT_SPEED * dir) + # 집은 몇 마리를 들일 수 있는지 세는 단위일 뿐, 헤엄치는 자리는 층 전체다. + # 제 집 앞에서 시작해 거기서부터 어디로든 돌아다닌다. + node.set_roam(_roam_bounds(home, cell), _rng) ## 어느 생물이 어느 집에 사는지 그릴 때마다 정한다. 집은 정원만 주므로 배정을 @@ -530,6 +523,11 @@ func _orbit_center(home: Rect2, cell: Vector2) -> Vector2: g.position.y + cell.y * 0.9 + r.y, g.end.y - r.y)) +## 그 집이 속한 층에서 헤엄칠 수 있는 범위. 층 전체를 쓰되 가장자리는 조금 남긴다. +func _roam_bounds(home: Rect2, cell: Vector2) -> Rect2: + return _home_bounds(home, cell).grow(-cell.x * 0.3) + + ## 그 집이 속한 층의 테두리. 생물을 여기 안에 가둔다. ## 예전에는 화면(grid_rect)에 맞춰 잘랐는데, 층이 옆으로 늘어선 지금 그렇게 하면 ## 뒤쪽 층의 생물이 전부 첫 층 오른쪽 끝으로 끌려와 제 층에서 사라진다. @@ -755,7 +753,7 @@ func _begin_drag() -> void: _drag_node = _node_for(_drag_ref) # 끄는 동안에는 궤도를 멈춘다. 안 그러면 손을 따라오지 않고 제 길을 간다. if _drag_node: - _drag_node.orbit_enabled = false + _drag_node.roam_enabled = false _press_was_drag = true drag_changed.emit(true) diff --git a/scenes/aquarium/tile_item.gd b/scenes/aquarium/tile_item.gd index 23908a1..bc8a269 100644 --- a/scenes/aquarium/tile_item.gd +++ b/scenes/aquarium/tile_item.gd @@ -25,12 +25,19 @@ var body_height := 32.0 ## 바닥에서 얼마나 떠 있는지(픽셀). 헤엄치는 것만 0보다 크다. var hover := 0.0 -## 집 둘레를 도는 것(fish/glow)만 궤도를 갖는다. 끌고 있는 동안에는 멈춘다. -var orbit_enabled := true -var _orbit_center := Vector2.ZERO -var _orbit_radius := Vector2.ZERO -var _orbit_speed := 0.0 -var _orbit_phase := 0.0 +## 헤엄치는 것만 돌아다닌다. 끌고 있는 동안에는 멈춘다. +var roam_enabled := true +var _roaming := false +var _roam := Rect2() +var _speed := 0.0 +var _turn := 0.0 +## 헤엄치는 세로 범위를 층 높이의 비율로. (위, 아래) +var _band := Vector2(0.1, 0.95) +var _heading := 0.0 +## 0보다 크면 가끔 튀어 나간다(새우). +var _dart := 0.0 +var _dart_timer := 0.0 +var _dart_left := 0.0 var _t := 0.0 var _phase := 0.0 @@ -75,35 +82,114 @@ func set_area(area: Rect2, cell: Vector2, height_tiles: float) -> void: queue_redraw() -## 집 둘레를 도는 궤도에 올린다. -## 원점이 궤도를 따라 움직이므로 Y 정렬이 알아서 앞뒤를 바꿔준다 — -## 뒤쪽으로 돌면 집에 가려 "들어간" 것처럼 보이고, 앞으로 나오면 다시 드러난다. -func set_orbit(center: Vector2, radius: Vector2, phase: float, speed: float) -> void: - _orbit_center = center - _orbit_radius = radius - _orbit_phase = phase - _orbit_speed = speed - # 궤도가 큰 움직임을 맡으므로 제자리 흔들림은 줄인다. 안 그러면 서로 싸운다. - _swim_range *= 0.35 - _apply_orbit(0.0) +## 층 전체를 자유롭게 돌아다니게 한다. +## 집은 정원을 세는 단위일 뿐이고, 헤엄치는 자리는 층 전체다. +## +## 종류마다 다니는 방식이 다르다. 물고기는 크게 돌고, 게는 바닥을 짚고 걷고, +## 해마는 거의 제자리에서 오르내리고, 새우는 튀었다 멈춘다. +## 같은 식으로 움직이면 여러 마리가 한 화면에 있을 때 무늬가 되어 버린다. +func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: + _roam = area + _roaming = true + # 종류별 성격. speed는 초당 픽셀, turn은 초당 라디안. + match shape: + "crab": + # 바닥을 짚고 좌우로만. 위아래로는 거의 안 움직인다. + _speed = _roam.size.y * 0.22 + _turn = 0.5 + _band = Vector2(0.62, 1.0) + _dart = 0.0 + "seahorse": + # 꼬리를 감고 선 채로 조금씩. 오르내림이 주된 움직임이다. + _speed = _roam.size.y * 0.10 + _turn = 1.6 + _band = Vector2(0.15, 0.9) + _dart = 0.0 + "shrimp": + # 튀었다 멈춘다. 평소 속도는 느리고 가끔 확 나아간다. + _speed = _roam.size.y * 0.16 + _turn = 1.1 + _band = Vector2(0.5, 1.0) + _dart = 1.0 + "glow": + # 해파리는 물살에 밀리듯 아주 천천히. + _speed = _roam.size.y * 0.13 + _turn = 0.35 + _band = Vector2(0.05, 0.75) + _dart = 0.0 + _: + _speed = _roam.size.y * 0.38 + _turn = 0.8 + _band = Vector2(0.1, 0.95) + _dart = 0.0 + + # 돌아다니는 것이 큰 움직임을 맡으므로 제자리 흔들림은 줄인다. 안 그러면 서로 싸운다. + _swim_range *= 0.3 + _speed *= rng.randf_range(0.8, 1.25) + _heading = rng.randf() * TAU + _dart_timer = rng.randf_range(0.5, 3.0) + _apply_depth() -func is_orbiting() -> bool: - return orbit_enabled and _orbit_radius != Vector2.ZERO +func is_roaming() -> bool: + return _roaming and roam_enabled and _roam.size.x > 1.0 -func _apply_orbit(t: float) -> void: - var angle := _orbit_phase + t * _orbit_speed - var next := _orbit_center + Vector2( - cos(angle) * _orbit_radius.x, sin(angle) * _orbit_radius.y) - # 나아가는 쪽으로 몸을 돌린다. - if absf(next.x - position.x) > 0.01: +## 헤엄칠 수 있는 세로 범위. 종류마다 물속에서 노는 높이가 다르다. +func _band_range() -> Vector2: + return Vector2( + _roam.position.y + _roam.size.y * _band.x, + _roam.position.y + _roam.size.y * _band.y) + + +func _roam_step(delta: float) -> void: + # 방향을 조금씩 튼다. 목표를 따로 두지 않고 각도만 흔들어도 + # 충분히 어슬렁거리는 것처럼 보인다. + _heading += (randf() - 0.5) * _turn * delta * 6.0 + + # 튀는 종류는 가끔 확 나아간다. + var boost := 1.0 + if _dart > 0.0: + _dart_timer -= delta + if _dart_timer <= 0.0: + _dart_timer = randf_range(1.2, 3.5) + _dart_left = 0.35 + if _dart_left > 0.0: + _dart_left -= delta + boost = 3.2 + + var dir := Vector2.RIGHT.rotated(_heading) + # 게는 좌우로만 걷는다. 위아래로 미끄러지면 헤엄치는 것처럼 보인다. + if shape == "crab": + dir.y *= 0.12 + elif shape == "seahorse": + dir.x *= 0.35 + var next := position + dir * _speed * boost * delta + + # 테두리에 닿기 전에 미리 돌아선다. 벽에 붙어 비비는 것보다 자연스럽다. + var margin := _roam.size.y * 0.12 + var band := _band_range() + if next.x < _roam.position.x + margin or next.x > _roam.end.x - margin: + _heading = PI - _heading + next.x = clampf(next.x, _roam.position.x + margin, _roam.end.x - margin) + if next.y < band.x or next.y > band.y: + _heading = -_heading + next.y = clampf(next.y, band.x, band.y) + + if absf(next.x - position.x) > 0.02: _facing = signf(next.x - position.x) position = next - # 뒤(위쪽)로 돌수록 작고 흐려져 집 안으로 들어간 것처럼 보인다. - var depth := (sin(angle) + 1.0) * 0.5 - scale = Vector2.ONE * lerpf(0.72, 1.0, depth) - modulate.a = lerpf(0.45, 1.0, depth) + _apply_depth() + + +## 뒷줄(화면 위쪽)일수록 멀리 있으므로 조금 작고 흐리게. +## 3/4 시점에서 앞뒤를 읽게 해주는 유일한 단서다. +func _apply_depth() -> void: + if _roam.size.y <= 1.0: + return + var f := clampf((position.y - _roam.position.y) / _roam.size.y, 0.0, 1.0) + scale = Vector2.ONE * lerpf(0.78, 1.06, f) + modulate.a = lerpf(0.62, 1.0, f) ## 그려지는 대략의 범위. 클릭으로 집을 때 쓴다. @@ -116,12 +202,12 @@ func visual_rect() -> Rect2: func _process(delta: float) -> void: _t += delta - if is_orbiting(): - _apply_orbit(_t) + if is_roaming(): + _roam_step(delta) if _swim_range != Vector2.ZERO: var next_x := sin(_t * 0.55 + _phase) * _swim_range.x # 방향을 튼 순간 몸도 돌아본다. 궤도를 돌 때는 궤도가 방향을 정한다. - if not is_orbiting() and absf(next_x - _swim.x) > 0.001: + if not is_roaming() and absf(next_x - _swim.x) > 0.001: _facing = signf(next_x - _swim.x) _swim = Vector2(next_x, sin(_t * 0.9 + _phase * 1.7) * _swim_range.y) queue_redraw()