헤엄치는 움직임에 쉬는 틈과 방향 전환을

매 틀 각도를 무작위로 흔들던 것을 나아감/멈춤 두 상태로 바꿨다. 속도가 늘
일정해 쉬는 틈이 없었고, 층이 가로로 길고 세로로 짧아 무작위 방향이 위아래
벽에 계속 부딪히며 튀었다.

- 종류마다 기본 축(물고기·게는 좌우, 해마·해파리는 위아래)에서 조금만 벗어나게
- 나아갈 때마다 목표 각도를 정하고 rotate_toward로 부드럽게 돌아서게
- 속도를 서서히 붙이고 죽여서 멈추고 출발하는 것이 기계처럼 보이지 않게
- 벽 앞에서 튕기는 대신 미리 안쪽으로 돌아서게
- 새우의 튀어 나가는 동작은 짧은 CRUISE + 긴 REST로 흡수

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 14:59:26 +09:00
parent 375b340c47
commit 9eaaabd3b3
2 changed files with 152 additions and 67 deletions
+11
View File
@@ -446,3 +446,14 @@ F11이나 메뉴로 창을 화면 전체로 키운다. 그냥 창만 키우면
**앞뒤 단서**: 화면 위쪽(뒷줄)일수록 조금 작고 흐리게 그린다(`_apply_depth`).
3/4 시점에서 깊이를 읽게 해주는 유일한 신호라 없으면 평면처럼 보인다.
### 헤엄치는 리듬
돌아다니는 생물은 두 상태를 오간다 — 한동안 나아가다가(CRUISE) 잠시 멈춘다(REST).
방향은 매 틀 흔들지 않고, 나아갈 때마다 목표 각도를 하나 정해 그쪽으로 부드럽게 돌아선다.
매 틀 각도를 무작위로 흔드는 방식을 먼저 써 봤는데 두 가지가 어색했다. 하나는
속도가 늘 일정해서 쉬는 틈이 없었고, 다른 하나는 층이 가로로 아주 길고 세로로
짧다 보니 무작위 방향이 위아래 벽에 계속 부딪혀 통통 튀는 것처럼 보였다.
그래서 종류마다 기본으로 삼는 축(물고기·게는 좌우, 해마·해파리는 위아래)을 두고
거기서 조금만 벗어나게 했고, 벽 앞에서는 튕기는 대신 미리 안쪽으로 돌아서게 했다.
+141 -67
View File
@@ -30,14 +30,24 @@ 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 _desired := 0.0
## 기본으로 삼는 축. 0이면 좌우, 1이면 위아래.
var _vertical := 0.0
var _spread := 0.35
## 나아가는/쉬는 시간의 (최소, 최대).
var _cruise := Vector2(1.6, 3.8)
var _rest := Vector2(0.25, 1.1)
var _rest_odds := 0.5
var _accel := 2.5
var _turn_rate := 2.2
## 지금 쉬는 중인지, 그리고 다음 전환까지 남은 시간.
var _resting := false
var _move_timer := 0.0
## 0에서 1로 오가는 속도 배수. 서서히 붙고 서서히 죽는다.
var _throttle := 0.0
var _t := 0.0
var _phase := 0.0
@@ -85,49 +95,85 @@ func set_area(area: Rect2, cell: Vector2, height_tiles: float) -> void:
## 층 전체를 자유롭게 돌아다니게 한다.
## 집은 정원을 세는 단위일 뿐이고, 헤엄치는 자리는 층 전체다.
##
## 종류마다 다니는 방식이 다르다. 물고기는 크게 돌고, 게는 바닥을 짚고 걷고,
## 해마는 거의 제자리에서 오르내리고, 새우는 튀었다 멈춘다.
## 같은 으로 움직이면 여러 마리가 한 화면에 있을 때 무늬가 되어 버린다.
## 움직임은 두 상태를 오간다 — 한동안 나아가다가(CRUISE) 잠시 멈춘다(REST).
## 실제 물고기가 그렇고, 무엇보다 쉬는 틈이 있어야 여러 마리가 한 화면에 있을 때
## 전부 같은 리듬으로 흐르지 않는다.
##
## 방향은 매 틀 흔들지 않는다. 나아갈 때마다 목표 각도를 하나 정하고 그쪽으로
## 부드럽게 돌아선다. 매 틀 흔들면 술 취한 것처럼 떨리고, 층이 가로로 길고
## 세로로 짧아서 위아래 벽에 계속 부딪히며 통통 튀게 된다.
func set_roam(area: Rect2, rng: RandomNumberGenerator) -> void:
_roam = area
_roaming = true
# 종류별 성격. speed는 초당 픽셀, turn은 초당 라디안.
var h := _roam.size.y
# 종류별 성격.
# vertical: 0이면 좌우로, 1이면 위아래로 다니는 것을 기본으로 삼는다.
# spread : 그 축에서 얼마나 벗어날 수 있는지(라디안).
# rest_odds: 한 번 나아간 뒤 쉴 확률.
match shape:
"crab":
# 바닥을 짚고 좌우로만. 위아래로는 거의 안 움직인다.
_speed = _roam.size.y * 0.22
_turn = 0.5
# 바닥을 짚고 좌우로만. 자주 멈춰 선다.
_speed = h * 0.22
_vertical = 0.0
_spread = 0.06
_cruise = Vector2(0.8, 2.0)
_rest = Vector2(0.6, 2.2)
_rest_odds = 0.75
_turn_rate = 3.0
_accel = 6.0
_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
# 꼬리를 감고 선 채로 오르내린다. 오래 멈춰 있다.
_speed = h * 0.13
_vertical = 1.0
_spread = 0.5
_cruise = Vector2(1.0, 2.6)
_rest = Vector2(1.0, 3.0)
_rest_odds = 0.7
_turn_rate = 1.6
_accel = 2.0
_band = Vector2(0.15, 0.92)
"shrimp":
# 튀었다 멈춘다. 평소 속도는 느리고 가끔 확 나아간다.
_speed = _roam.size.y * 0.16
_turn = 1.1
# 거의 멈춰 있다가 짧게 튄다.
_speed = h * 0.6
_vertical = 0.0
_spread = 0.5
_cruise = Vector2(0.15, 0.4)
_rest = Vector2(0.9, 2.6)
_rest_odds = 0.9
_turn_rate = 5.0
_accel = 12.0
_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 = h * 0.14
_vertical = 1.0
_spread = 0.7
_cruise = Vector2(2.5, 5.0)
_rest = Vector2(0.8, 2.0)
_rest_odds = 0.4
_turn_rate = 0.8
_accel = 1.2
_band = Vector2(0.05, 0.78)
_:
_speed = _roam.size.y * 0.38
_turn = 0.8
_speed = h * 0.4
_vertical = 0.0
_spread = 0.35
_cruise = Vector2(1.6, 3.8)
_rest = Vector2(0.25, 1.1)
_rest_odds = 0.55
_turn_rate = 2.2
_accel = 2.5
_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)
_speed *= rng.randf_range(0.82, 1.2)
_heading = _axis_angle() + (PI if rng.randf() < 0.5 else 0.0)
_desired = _heading
# 시작 시각을 흩어 놓는다. 안 그러면 모두 같은 박자로 멈추고 나아간다.
_move_timer = rng.randf_range(0.0, _cruise.y)
_apply_depth()
@@ -135,6 +181,11 @@ func is_roaming() -> bool:
return _roaming and roam_enabled and _roam.size.x > 1.0
## 이 종류가 기본으로 삼는 축.
func _axis_angle() -> float:
return PI * 0.5 if _vertical > 0.5 else 0.0
## 헤엄칠 수 있는 세로 범위. 종류마다 물속에서 노는 높이가 다르다.
func _band_range() -> Vector2:
return Vector2(
@@ -142,43 +193,66 @@ func _band_range() -> Vector2:
_roam.position.y + _roam.size.y * _band.y)
## 다음에 나아갈 방향. 가던 쪽을 웬만하면 유지한다 --
## 매번 새로 뽑으면 제자리에서 좌우로 왔다 갔다 하는 것처럼 보인다.
func _new_heading() -> float:
var axis := _axis_angle()
var forward: float = cos(_heading) if _vertical < 0.5 else sin(_heading)
var same_way := forward >= 0.0
if randf() < 0.3:
same_way = not same_way
return axis + (0.0 if same_way else PI) + randf_range(-_spread, _spread)
func _roam_step(delta: float) -> void:
# 방향을 조금씩 튼다. 목표를 따로 두지 않고 각도만 흔들어도
# 충분히 어슬렁거리는 것처럼 보인다.
_heading += (randf() - 0.5) * _turn * delta * 6.0
_move_timer -= delta
if _move_timer <= 0.0:
if _resting:
_resting = false
_move_timer = randf_range(_cruise.x, _cruise.y)
_desired = _new_heading()
else:
_resting = randf() < _rest_odds
if _resting:
_move_timer = randf_range(_rest.x, _rest.y)
else:
# 쉬지 않고 이어 갈 때도 방향은 새로 잡는다.
_move_timer = randf_range(_cruise.x, _cruise.y)
_desired = _new_heading()
# 튀는 종류는 가끔 확 나아간다.
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)
var mx := _roam.size.y * 0.35
var my := (band.y - band.x) * 0.18
var away := Vector2.ZERO
if position.x < _roam.position.x + mx:
away.x += 1.0
elif position.x > _roam.end.x - mx:
away.x -= 1.0
if position.y < band.x + my:
away.y += 1.0
elif position.y > band.y - my:
away.y -= 1.0
if away != Vector2.ZERO:
_desired = away.angle()
_resting = false
# 목표 각도로 부드럽게 돌아선다.
_heading = rotate_toward(_heading, _desired, _turn_rate * delta)
# 속도는 서서히 붙고 서서히 죽는다. 딱 멈추고 딱 출발하면 기계처럼 보인다.
_throttle = move_toward(_throttle, 0.0 if _resting else 1.0, _accel * delta)
if _throttle > 0.001:
var step := Vector2.RIGHT.rotated(_heading) * _speed * _throttle * delta
position += step
# 제 범위를 벗어나지 않게 잡아 둔다. 위 steering이 대부분 막아 주고,
# 이건 크기가 갑자기 바뀌었을 때를 위한 마지막 그물이다.
position.x = clampf(position.x, _roam.position.x, _roam.end.x)
position.y = clampf(position.y, band.x, band.y)
if absf(step.x) > 0.02:
_facing = signf(step.x)
if absf(next.x - position.x) > 0.02:
_facing = signf(next.x - position.x)
position = next
_apply_depth()