diff --git a/docs/DESIGN.md b/docs/DESIGN.md index af9461e..bfea24f 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -469,3 +469,26 @@ F11이나 메뉴로 창을 화면 전체로 키운다. 그냥 창만 키우면 사이에서 뽑되 짧은 쪽으로 치우치게 해서(`pow(randf(), 1.6)`), 몇 칸만 가는 일이 잦고 화면을 가로지르는 일은 가끔 있게 했다. 방향도 좌우 어느 쪽인지 매번 새로 던진다 -- 가던 쪽을 이어 가게 했더니 오히려 한 방향으로 흐르는 것처럼 보였다. + +### 물기둥 높이 (hover) + +바닥은 위에서 내려다보고 물고기는 옆에서 본 모습으로 그리다 보니, 물고기가 모래 +위를 기어다니는 것처럼 보였다. 원인은 물고기를 설치물과 똑같은 규칙으로 -- 원점을 +바닥에 닿는 지점에 두고 -- 놓았기 때문이다. 바닥에 붙어 있으면 바닥에 붙은 것으로 +읽히는 게 당연하다. + +그래서 헤엄치는 것에는 높이를 하나 더 뒀다. `position`은 여전히 "모래 위 어디"를 +가리키고 그림자도 거기 남지만, 몸은 `hover`만큼 위에 그린다. 이 간격이 벌어질수록 +물속에 떠 있는 것으로 읽힌다. 물고기는 칸 높이의 1~2.9배를 뜨고, 이동할 때마다 그 +높이를 새로 뽑아 천천히 오르내린다. 게와 새우는 거의 뜨지 않고 해파리는 가장 높이 +뜬다. 뜬 높이만큼 그림자는 넓고 옅게 번진다. + +뜰 자리를 확보하려고 헤엄치는 것들의 바닥 범위를 앞쪽(화면 아래)으로 옮겼다. +뒷줄에 있으면 화면 위 여유가 없어 높이 뜰 수 없는데, 이건 원근으로도 맞는 얘기라 +`_hover_ceiling()`으로 자리에 따라 알아서 낮아지게 뒀다. + +**주의**: `draw_set_transform()`은 쌓이지 않고 통째로 갈아치운다. 형태 함수 안에서 +좌우를 뒤집을 때 `Vector2.ZERO`로 다시 부르면 `hover` 오프셋이 통째로 날아간다. +실제로 이 때문에 물고기·게·해마·새우가 전부 바닥에 붙어 있었다. 그래서 오프셋을 +`_body_offset`에 담아 두고 `_xform()`을 거치도록 했다 -- 형태 함수에서 직접 +`draw_set_transform`을 부르면 안 된다. diff --git a/scenes/aquarium/tile_item.gd b/scenes/aquarium/tile_item.gd index 3834bd2..d346123 100644 --- a/scenes/aquarium/tile_item.gd +++ b/scenes/aquarium/tile_item.gd @@ -5,6 +5,11 @@ class_name TileItem ## 바닥은 위에서 내려다보지만 물건은 "서 있는" 모습으로 그린다. ## 노드의 원점은 발이 바닥에 닿는 지점(발자국 아래 가운데)이고, ## 그림은 거기서 위(-Y)로 자란다. 그래서 키가 큰 것은 뒷줄을 가린다. +## +## 헤엄치는 것은 여기에 hover가 하나 더 붙는다. 원점은 여전히 "모래 위 어디"를 +## 가리키고 그림자도 거기 남지만, 몸은 hover만큼 위에 그린다. 이 둘을 나누지 +## 않으면 -- 즉 물고기를 설치물과 똑같이 바닥에 붙여 놓으면 -- 모래 위를 기어 +## 다니는 것처럼 보인다. 뜬 높이만큼 몸과 그림자가 벌어져야 물속으로 읽힌다. ## 아직 아트 에셋이 없어서 형태별로 직접 그린다. ## assets/art/ 에 스프라이트가 생기면 Sprite2D + offset 기반으로 바꾸면 된다. @@ -23,7 +28,16 @@ var span := Vector2(34, 32) ## 서 있는 높이(픽셀). var body_height := 32.0 ## 바닥에서 얼마나 떠 있는지(픽셀). 헤엄치는 것만 0보다 크다. +## 바닥 위치(position)와는 따로 논다 -- position은 "모래 위 어디"이고 +## hover는 "그 자리에서 물기둥 얼마나 위". 그림자는 바닥에, 몸은 hover만큼 위에 +## 그려지므로, 둘 사이가 벌어질수록 물속에 떠 있는 것으로 읽힌다. var hover := 0.0 +var _cell_h := 26.0 +## 떠오를 높이의 (최소, 최대). 칸 높이의 배수. 헤엄치는 것만 0이 아니다. +var _hover_range := Vector2.ZERO +var _hover_goal := 0.0 +## 이번 틀에 몸을 그릴 기준 오프셋. _xform()이 여기에 얹는다. +var _body_offset := Vector2.ZERO ## 헤엄치는 것만 돌아다닌다. 끌고 있는 동안에는 멈춘다. var roam_enabled := true @@ -83,6 +97,7 @@ func setup(item_data: Dictionary, kind: String, index: int, area: Rect2, cell: V func set_area(area: Rect2, cell: Vector2, height_tiles: float) -> void: span = area.size var cell_h := maxf(cell.y, 1.0) + _cell_h = cell_h body_height = cell_h * height_tiles position = Vector2(area.position.x + area.size.x * 0.5, area.end.y) @@ -132,6 +147,7 @@ func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: _turn_rate = 1.6 _accel = 3.0 _band = Vector2(0.62, 1.0) + _hover_range = Vector2(0.0, 0.05) "seahorse": # 꼬리를 감고 선 채로 아주 천천히 오르내린다. _speed = h * 0.07 @@ -143,7 +159,8 @@ func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: _rest_odds = 0.75 _turn_rate = 1.0 _accel = 1.5 - _band = Vector2(0.15, 0.92) + _band = Vector2(0.55, 0.96) + _hover_range = Vector2(0.5, 1.7) "shrimp": # 거의 멈춰 있다가 짧게 튄다. 튀는 순간만 빠르다. _speed = h * 0.35 @@ -155,7 +172,8 @@ func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: _rest_odds = 0.9 _turn_rate = 4.0 _accel = 10.0 - _band = Vector2(0.5, 1.0) + _band = Vector2(0.55, 1.0) + _hover_range = Vector2(0.0, 0.35) "glow": # 해파리는 물살에 밀리듯 길게 흐르고 가끔 멎는다. _speed = h * 0.07 @@ -167,7 +185,8 @@ func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: _rest_odds = 0.45 _turn_rate = 0.6 _accel = 0.9 - _band = Vector2(0.05, 0.78) + _band = Vector2(0.42, 0.86) + _hover_range = Vector2(1.8, 3.6) _: # 물고기는 거의 좌우로만 다닌다. spread가 0.1이면 6도쯤 -- # 한 번 나아가는 동안 수심이 눈에 띄게 바뀌지 않을 만큼이다. @@ -180,7 +199,8 @@ func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: _rest_odds = 0.6 _turn_rate = 1.2 _accel = 1.4 - _band = Vector2(0.1, 0.95) + _band = Vector2(0.45, 0.98) + _hover_range = Vector2(1.0, 2.9) # 돌아다니는 것이 큰 움직임을 맡으므로 제자리 흔들림은 줄인다. _swim_range *= 0.22 @@ -195,6 +215,12 @@ func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void: else: _start_leg() _travelled = _leg_dist * rng.randf() + + # 집 앞에서 시작하므로 제 바닥 범위 밖일 수 있다. + var band := _band_range() + position.y = clampf(position.y, band.x, band.y) + _pick_hover() + hover = _hover_goal _apply_depth() @@ -230,6 +256,21 @@ func _start_leg() -> void: _leg_dist = _roam.size.y * lerpf(_leg.x, _leg.y, t) _leg_speed = randf_range(0.6, 1.5) _desired = _new_heading() + _pick_hover() + + +## 이 자리에서 띄울 수 있는 한계. 층 위로 솟구쳐 나가면 안 된다. +## 뒷줄에 있을수록 화면 위 여유가 적으니 덜 뜬다 -- 멀리 있는 물고기가 +## 낮게 떠 있는 것은 원근으로도 맞다. +func _hover_ceiling() -> float: + return maxf(position.y - _roam.position.y - _cell_h * 0.35, 0.0) + + +func _pick_hover() -> void: + if _hover_range == Vector2.ZERO: + return + _hover_goal = minf(_cell_h * randf_range(_hover_range.x, _hover_range.y), + _hover_ceiling()) func _start_rest() -> void: @@ -281,6 +322,12 @@ func _roam_step(delta: float) -> void: # 벽을 따라 하염없이 미끄러지지 않도록 이번 이동을 곧 끝낸다. _leg_dist = minf(_leg_dist, _travelled + _roam.size.y * 0.8) + # 물기둥에서 오르내리는 것은 바닥 위치와 따로 간다. 아주 천천히 움직여야 + # 헤엄쳐 올라가는 것처럼 보인다. + if _hover_range != Vector2.ZERO: + _hover_goal = minf(_hover_goal, _hover_ceiling()) + hover = move_toward(hover, _hover_goal, _cell_h * 0.55 * delta) + # 목표 각도로 부드럽게 돌아서고, 속도는 서서히 붙고 서서히 죽는다. _heading = rotate_toward(_heading, _desired, _turn_rate * delta) _throttle = move_toward(_throttle, 0.0 if _resting else 1.0, _accel * delta) @@ -330,17 +377,28 @@ func _process(delta: float) -> void: queue_redraw() -func _draw() -> void: - # 바닥 그림자. 3/4 시점에서 물건이 땅에 붙어 있다고 알려주는 신호다. - # 떠 있을수록 넓고 옅어진다. - var lift := clampf(hover / maxf(span.y, 1.0), 0.0, 1.0) - draw_set_transform(Vector2(0, -span.y * 0.12), 0.0, Vector2(1.0, 0.34)) - draw_circle(Vector2.ZERO, span.x * (0.30 + lift * 0.10), - Color(0, 0, 0, 0.20 - lift * 0.09)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) +## 몸을 그릴 기준 변환. draw_set_transform은 쌓이지 않고 통째로 갈아치우므로, +## 형태 함수 안에서 좌우를 뒤집거나 윗면을 눕힐 때도 반드시 _body_offset을 다시 +## 실어야 한다. 빠뜨리면 hover가 날아가 헤엄치는 것이 모래 위에 붙어 버린다. +func _xform(offset := Vector2.ZERO, scale := Vector2.ONE) -> void: + draw_set_transform(_body_offset + offset, 0.0, scale) - # 헤엄치는 것은 제 칸 위를 오간다. - draw_set_transform(Vector2(_swim.x, -hover + _swim.y), 0.0, Vector2.ONE) + +func _draw() -> void: + # 몸을 얼마나 띄워 그릴지. 형태 함수들이 좌우를 뒤집을 때도 이 값을 다시 + # 실어야 하므로 멤버에 담아 둔다. + _body_offset = Vector2(_swim.x, -hover + _swim.y) + + # 바닥 그림자. 3/4 시점에서 이것이 바닥 어디에 있는지 알려주는 유일한 신호다. + # 헤엄치는 것은 그림자만 모래에 남고 몸은 hover만큼 위로 떠오른다. + # 이 간격이 "바닥을 기어간다"와 "물속에 떠 있다"를 가른다. + # 높이 뜰수록 그림자는 넓고 옅게 번진다. + var lift := clampf(hover / maxf(_cell_h * 3.0, 1.0), 0.0, 1.0) + draw_set_transform(Vector2(0, -span.y * 0.12), 0.0, Vector2(1.0, 0.34)) + draw_circle(Vector2.ZERO, span.x * (0.30 + lift * 0.34), + Color(0, 0, 0, 0.22 - lift * 0.13)) + + _xform() match shape: "plant": _draw_plant() "fish": _draw_fish() @@ -387,7 +445,7 @@ func _draw_plant() -> void: func _draw_fish() -> void: var l := span.x * 0.34 var wag := sin(_t * 4.0 + _phase) * l * 0.30 - draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + _xform(Vector2.ZERO, Vector2(_facing, 1.0)) # 꼬리지느러미. _fill(PackedVector2Array([ @@ -421,7 +479,7 @@ func _draw_fish() -> void: ]), color.lightened(0.28)) draw_circle(Vector2(l * 0.60, -l * 0.14), maxf(l * 0.11, 1.2), Color(0.06, 0.07, 0.09)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform() ## 스스로 빛나며 떠 있는 것. 후광과 아래로 늘어진 촉수. @@ -487,9 +545,9 @@ func _draw_rock() -> void: # 꼭대기 면. 부감이라 윗면이 살짝 보이지만, 몸통보다 넓으면 안 된다. var top_w := w * _rock_profile(1.0, 0.0) - draw_set_transform(Vector2(0, -h), 0.0, Vector2(1.0, 0.34)) + _xform(Vector2(0, -h), Vector2(1.0, 0.34)) draw_circle(Vector2.ZERO, top_w * 0.92, color.lightened(0.16)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform() ## 바위의 높이 f(0=밑동, 1=꼭대기)에서 옆으로 벌어지는 정도. @@ -543,9 +601,9 @@ func _draw_vessel() -> void: # 입구. 물고기가 드나드는 곳이라 확실히 어둡게 파 둔다. var mouth := w * _vessel_profile(1.0) - draw_set_transform(Vector2(0, -h), 0.0, Vector2(1.0, 0.38)) + _xform(Vector2(0, -h), Vector2(1.0, 0.38)) draw_circle(Vector2.ZERO, mouth * 1.05, color.darkened(0.72)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform() ## 밑동은 좁고, 중간이 부르고, 목에서 다시 좁아진다. @@ -619,9 +677,9 @@ func _draw_pillar() -> void: draw_line(Vector2(-half, y), Vector2(half, y), seam, maxf(span.x * 0.028, 1.0)) # 꼭대기 면. 부감이라 윗면이 살짝 보인다. - draw_set_transform(Vector2(0, -h), 0.0, Vector2(1.0, 0.34)) + _xform(Vector2(0, -h), Vector2(1.0, 0.34)) draw_circle(Vector2.ZERO, w * 1.02, color.lightened(0.18)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform() ## 바닥을 걷는 게. 옆으로 벌어진 몸에 다리가 뻗고 집게가 앞을 짚는다. @@ -629,7 +687,7 @@ func _draw_pillar() -> void: func _draw_crab() -> void: var r := span.x * 0.26 var step := sin(_t * 3.0 + _phase) - draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + _xform(Vector2.ZERO, Vector2(_facing, 1.0)) # 다리. 좌우 세 쌍이 번갈아 짚는다. for side: float in [-1.0, 1.0]: @@ -655,14 +713,14 @@ func _draw_crab() -> void: ]), color.lightened(0.1)) # 등딱지. 가로로 넓은 타원. - draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing * 1.35, 0.92)) + _xform(Vector2.ZERO, Vector2(_facing * 1.35, 0.92)) draw_circle(Vector2.ZERO, r, color) - draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + _xform(Vector2.ZERO, Vector2(_facing, 1.0)) draw_circle(Vector2(-r * 0.1, -r * 0.22), r * 0.55, color.lightened(0.16)) for side: float in [-1.0, 1.0]: draw_circle(Vector2(r * 0.5, side * r * 0.34), maxf(r * 0.12, 1.2), Color(0.06, 0.07, 0.09)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform() ## 선 채로 흔들리는 해마. 세로로 긴 S자가 곧 실루엣이다. @@ -670,7 +728,7 @@ func _draw_seahorse() -> void: var h := body_height * 0.9 var w := span.x * 0.16 var sway := sin(_t * 1.3 + _phase) * 0.22 - draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + _xform(Vector2.ZERO, Vector2(_facing, 1.0)) # 몸통. 아래로 갈수록 가늘어지며 꼬리가 안으로 말린다. var left := PackedVector2Array() @@ -703,14 +761,14 @@ func _draw_seahorse() -> void: Vector2(bend - w * 2.0, -h * (f + 0.06)), Vector2(bend - w * 0.8, -h * (f + 0.12)), ]), color.darkened(0.3)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform() ## 새우. 등이 굽고 더듬이가 길다. 꼬리를 튕겨 조금씩 물러난다. func _draw_shrimp() -> void: var l := span.x * 0.3 var flick := sin(_t * 4.5 + _phase) - draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + _xform(Vector2.ZERO, Vector2(_facing, 1.0)) # 더듬이. 실루엣의 절반을 차지한다. for side: float in [-0.35, 0.15]: @@ -742,4 +800,4 @@ func _draw_shrimp() -> void: tail + Vector2(-l * 0.35, l * 0.35), ]), color.darkened(0.25)) draw_circle(Vector2(l * 0.68, -l * 0.12), maxf(l * 0.1, 1.2), Color(0.06, 0.07, 0.09)) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + _xform()