물고기가 집을 벗어나 층 전체를 헤엄치도록
집 둘레를 타원으로 돌던 것을 걷어냈다. 관계는 분명했지만 꾸미기 게임에는 어울리지 않았다 -- 말뚝에 묶인 것처럼 보였고, 집을 여러 채 지으면 화면이 회전목마 여러 대가 된다. 집은 정원을 세는 단위로만 남고, 헤엄치는 범위는 그 층의 격자 전체다. 제 집 앞에서 시작해 거기서부터 어디로든 간다. 알고리즘은 방향 흔들기다. 목표 지점을 따로 두지 않고 진행 각도만 매 틀 조금씩 흔든다. 그것만으로 충분히 어슬렁거리는 것처럼 보이고 목표를 찍는 방식보다 싸다. 테두리는 닿기 전에 미리 각도를 반사시킨다. 종류마다 다니는 방식을 갈랐다. 같은 식으로 움직이면 여러 마리가 한 화면에 있을 때 움직임이 무늬가 된다. - 물고기: 빠르고 크게 돌아다닌다 - 게: 세로 성분을 12%로 눌러 바닥을 짚고 걷는다 - 해마: 가로 성분을 35%로 눌러 거의 제자리에서 오르내린다 - 새우: 1~3초마다 0.35초 동안 3.2배로 튄다 - 해파리: 물살에 밀리듯 아주 천천히 화면 위쪽(뒷줄)일수록 작고 흐리게 그려 앞뒤를 읽게 했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,7 +67,8 @@ tests/smoke.tscn/.gd 경제 로직 스모크 테스트
|
||||
|
||||
1. 바다에 있는 **생물**이 시간에 따라 알아서 **거품**을 모은다. 클릭으로 버는 길은 없다.
|
||||
그래서 새로 시작하면 **항아리 집 한 채와 거품돌·구피 한 마리씩**이 이미 있고, 거품 20을 쥐고 시작한다.
|
||||
2. 생물은 **집**에 산다. 집에는 두 갈래가 있다 — 돌·해초·산호처럼 **자연 서식지**(싸고 적게 들인다)와
|
||||
2. 생물은 **집**에 산다. 다만 집은 몇 마리를 들일 수 있는지 세는 단위일 뿐,
|
||||
헤엄치는 자리는 **층 전체**다. 집에는 두 갈래가 있다 — 돌·해초·산호처럼 **자연 서식지**(싸고 적게 들인다)와
|
||||
항아리·난파선·석주처럼 **지어 올린 것**(비싸고 정원이 넓다). 더 키우려면 먼저 집을 지어야 한다.
|
||||
집은 칸을 차지하고 거품을 벌지 않는다. 자리와 **아름다움**을 준다.
|
||||
3. 생물을 들이면 초당 생산량과 아름다움이 오른다. 비싼 생물일수록 한 마리가 더 번다.
|
||||
|
||||
@@ -417,3 +417,32 @@ F11이나 메뉴로 창을 화면 전체로 키운다. 그냥 창만 키우면
|
||||
또 `_make_fish()`가 `fish`와 `glow`만 궤도에 올리고 있었다. 고착생물이 생물 쪽에
|
||||
있던 시절의 가드인데, 새로 넣은 게·해마·새우가 여기 걸려 제자리에 굳어 있었다.
|
||||
고착생물이 전부 집으로 갔으므로 가드 자체를 걷어냈다.
|
||||
|
||||
## 생물의 움직임
|
||||
|
||||
처음에는 생물이 제 집 둘레를 타원으로 돌았다. 집과 생물의 관계는 분명해졌지만
|
||||
꾸미기 게임에는 어울리지 않았다 — 물고기가 말뚝에 묶인 것처럼 보였고,
|
||||
집을 여러 채 지으면 화면이 회전목마 여러 대가 된다.
|
||||
|
||||
지금은 **층 전체를 자유롭게 돌아다닌다.** 집은 정원을 세는 단위로만 남고,
|
||||
헤엄치는 범위는 그 층의 격자 전체다(`_roam_bounds`). 제 집 앞에서 시작해
|
||||
거기서부터 어디로든 간다.
|
||||
|
||||
**알고리즘은 방향 흔들기(wander)다.** 목표 지점을 따로 두지 않고 진행 각도만
|
||||
매 틀 조금씩 흔든다. 그것만으로 충분히 어슬렁거리는 것처럼 보이고,
|
||||
목표를 찍는 방식보다 훨씬 싸다. 테두리는 닿기 전에 미리 각도를 반사시킨다 --
|
||||
벽에 붙어 비비는 것보다 자연스럽다.
|
||||
|
||||
**종류마다 다니는 방식이 다르다.** 같은 식으로 움직이면 여러 마리가 한 화면에
|
||||
있을 때 움직임이 무늬가 되어 버린다.
|
||||
|
||||
| | 속도 | 노는 높이 | 특징 |
|
||||
| --- | --- | --- | --- |
|
||||
| 물고기 | 빠름 | 물속 전체 | 크게 돌아다닌다 |
|
||||
| 게 | 보통 | 바닥께 | 세로 성분을 12%로 눌러 바닥을 짚고 걷는다 |
|
||||
| 해마 | 느림 | 위아래 넓게 | 가로 성분을 35%로 눌러 거의 제자리에서 오르내린다 |
|
||||
| 새우 | 느림 + 돌진 | 바닥께 | 1~3초마다 0.35초 동안 3.2배로 튄다 |
|
||||
| 해파리 | 아주 느림 | 위쪽 | 물살에 밀리듯 |
|
||||
|
||||
**앞뒤 단서**: 화면 위쪽(뒷줄)일수록 조금 작고 흐리게 그린다(`_apply_depth`).
|
||||
3/4 시점에서 깊이를 읽게 해주는 유일한 신호라 없으면 평면처럼 보인다.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+118
-32
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user