Skip to content

Commit 6b1b500

Browse files
committed
Guard: Add and use PointsWalkBehavior
This walk behavior is based on the original guard's patrolling implementation.
1 parent 6d37841 commit 6b1b500

4 files changed

Lines changed: 205 additions & 18 deletions

File tree

scenes/game_elements/characters/enemies/guard/components/guard.gd

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ const DEFAULT_SPRITE_FRAMES = preload("uid://ovu5wqo15s5g")
5151
@export_tool_button("Add/Edit Patrol Path") var _edit_patrol_path: Callable = edit_patrol_path
5252
## The path the guard follows while patrolling.
5353
@export var patrol_path: Path2D:
54-
set(new_value):
55-
patrol_path = new_value
54+
set = _set_patrol_path
5655

5756
## The wait time at each patrol point.
5857
@export_range(0, 5, 0.1, "or_greater", "suffix:s") var wait_time: float = 1.0:
@@ -108,6 +107,9 @@ var _player: Node2D
108107
## Control to hold debug info that can be toggled on or off.
109108
@onready var debug_info: Label = %DebugInfo
110109

110+
## Behavior to use when patrolling.
111+
@onready var patrolling_behavior: PointsWalkBehavior = %PatrollingBehavior
112+
111113
## Reference to the node controlling the AnimationPlayer for walking / being idle,
112114
## so it can be disabled to play the alerted animation.
113115
@onready
@@ -149,6 +151,8 @@ func _ready() -> void:
149151
player_awareness.max_value = time_to_detect_player
150152
player_awareness.value = 0.0
151153

154+
patrolling_behavior.speeds.walk_speed = move_speed
155+
_set_patrol_path(patrol_path)
152156
_set_sprite_frames(sprite_frames)
153157

154158
if detection_area:
@@ -163,8 +167,9 @@ func _ready() -> void:
163167
guard_movement.path_blocked.connect(self._on_path_blocked)
164168

165169
# Wait 2 frames before starting to match backwards compatibility.
166-
await get_tree().process_frame
167-
await get_tree().process_frame
170+
if not Engine.is_editor_hint():
171+
await get_tree().process_frame
172+
await get_tree().process_frame
168173

169174
_advance_target_patrol_point()
170175
state = State.WAITING
@@ -177,8 +182,11 @@ func _process(delta: float) -> void:
177182
return
178183

179184
match state:
180-
State.PATROLLING, State.WAITING, State.DETECTING, State.INVESTIGATING, State.RETURNING:
185+
State.WAITING, State.INVESTIGATING, State.RETURNING:
181186
guard_movement.move()
187+
State.DETECTING:
188+
if _previous_state != State.PATROLLING:
189+
guard_movement.move()
182190

183191
if state != State.ALERTED:
184192
_update_player_awareness(delta)
@@ -224,6 +232,13 @@ func _update_debug_info() -> void:
224232
debug_info.text += "%s: %.2f\n" % ["time left", waiting_timer.time_left]
225233
State.DETECTING, State.INVESTIGATING, State.RETURNING:
226234
debug_info.text += "%s: %s\n" % ["breadcrumbs", breadcrumbs.size()]
235+
State.PATROLLING:
236+
debug_info.text += (
237+
"%s: %s\n" % ["previous_patrol_point_idx", patrolling_behavior.previous_point_index]
238+
)
239+
debug_info.text += (
240+
"%s: %s\n" % ["current_patrol_point_idx", patrolling_behavior.current_point_index]
241+
)
227242
_:
228243
debug_info.text += "%s: %s\n" % ["previous_patrol_point_idx", previous_patrol_point_idx]
229244
debug_info.text += "%s: %s\n" % ["current_patrol_point_idx", current_patrol_point_idx]
@@ -232,9 +247,6 @@ func _update_debug_info() -> void:
232247
## What happens when the guard reached the point it was walking towards
233248
func _on_destination_reached() -> void:
234249
match state:
235-
State.PATROLLING:
236-
state = State.WAITING
237-
_advance_target_patrol_point()
238250
State.INVESTIGATING:
239251
state = State.WAITING
240252
State.RETURNING:
@@ -250,14 +262,6 @@ func _on_destination_reached() -> void:
250262
## stuck with a collider.
251263
func _on_path_blocked() -> void:
252264
match state:
253-
State.PATROLLING:
254-
state = State.WAITING
255-
# This check makes sure that if the guard is blocked on start,
256-
# they won't try to set an invalid patrol point as destination.
257-
if previous_patrol_point_idx > -1:
258-
var new_patrol_point: int = previous_patrol_point_idx
259-
previous_patrol_point_idx = current_patrol_point_idx
260-
current_patrol_point_idx = new_patrol_point
261265
State.INVESTIGATING:
262266
state = State.RETURNING
263267
State.RETURNING:
@@ -277,12 +281,14 @@ func _set_state(new_state: State) -> void:
277281

278282
match state:
279283
State.PATROLLING:
280-
var target_position: Vector2 = _patrol_point_position(current_patrol_point_idx)
281-
guard_movement.set_destination(target_position)
284+
patrolling_behavior.process_mode = Node.PROCESS_MODE_INHERIT
282285
State.DETECTING:
286+
if _previous_state != State.PATROLLING:
287+
patrolling_behavior.process_mode = Node.PROCESS_MODE_DISABLED
283288
if not _alert_sound.playing:
284289
_alert_sound.play()
285290
State.ALERTED:
291+
patrolling_behavior.process_mode = Node.PROCESS_MODE_DISABLED
286292
character_animation_player_behavior.process_mode = Node.PROCESS_MODE_DISABLED
287293
if not _alert_sound.playing:
288294
_alert_sound.play()
@@ -292,9 +298,13 @@ func _set_state(new_state: State) -> void:
292298
player_awareness.visible = true
293299
guard_movement.stop_moving()
294300
State.INVESTIGATING:
301+
patrolling_behavior.process_mode = Node.PROCESS_MODE_DISABLED
295302
breadcrumbs.push_back(global_position)
296303
State.WAITING:
304+
patrolling_behavior.process_mode = Node.PROCESS_MODE_DISABLED
297305
waiting_timer.start()
306+
State.RETURNING:
307+
patrolling_behavior.process_mode = Node.PROCESS_MODE_DISABLED
298308

299309

300310
## Calculate and set the next point in the patrol path.
@@ -452,6 +462,12 @@ func _set_alert_other_sound_stream(new_value: AudioStream) -> void:
452462
_torch_hit_sound.stream = new_value
453463

454464

465+
func _set_patrol_path(new_patrol_path: Path2D) -> void:
466+
patrol_path = new_patrol_path
467+
if patrolling_behavior:
468+
patrolling_behavior.walking_path = patrol_path
469+
470+
455471
func _set_wait_time(new_wait_time: float) -> void:
456472
wait_time = new_wait_time
457473
waiting_timer.wait_time = wait_time
@@ -489,3 +505,11 @@ func _on_waiting_timer_timeout() -> void:
489505
state = State.PATROLLING
490506
State.INVESTIGATING:
491507
state = State.RETURNING
508+
509+
510+
func _on_patrolling_behavior_point_reached() -> void:
511+
state = State.WAITING
512+
513+
514+
func _on_patrolling_behavior_got_stuck() -> void:
515+
state = State.WAITING

scenes/game_elements/characters/enemies/guard/guard.tscn

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@
55
[ext_resource type="Texture2D" uid="uid://8t6ihmesvghq" path="res://scenes/game_elements/characters/enemies/guard/components/field_of_view.png" id="3_0hjcv"]
66
[ext_resource type="Script" uid="uid://cu7ujf83r7yh2" path="res://scenes/game_logic/walk_behaviors/points_walk_behavior.gd" id="3_mswbt"]
77
[ext_resource type="Texture2D" uid="uid://c75ofhy3swhj3" path="res://scenes/game_elements/characters/enemies/guard/components/exclamation_mark.png" id="3_pnkgp"]
8+
[ext_resource type="Script" uid="uid://csev4hv57utxv" path="res://scenes/game_logic/walk_behaviors/character_speeds.gd" id="4_innil"]
89
[ext_resource type="Script" uid="uid://cxsi2xqcdyw7g" path="res://scenes/game_elements/characters/enemies/guard/components/light.gd" id="4_lptvm"]
910
[ext_resource type="Script" uid="uid://bnbt0iw1a1w6" path="res://scenes/game_elements/characters/enemies/guard/components/detection_area.gd" id="4_mswbt"]
1011
[ext_resource type="SpriteFrames" uid="uid://ovu5wqo15s5g" path="res://scenes/quests/template_quests/NO_EDIT/1_NO_EDIT_stealth/NO_EDIT_stealth_components/NO_EDIT_guard_enemy.tres" id="5_mswbt"]
1112
[ext_resource type="Script" uid="uid://dy68p7gf07pi3" path="res://scenes/game_logic/sprite_behaviors/character_sprite_behavior.gd" id="7_klpct"]
1213
[ext_resource type="Script" uid="uid://b3hx1n2yl88qr" path="res://scenes/game_logic/character_animation_player_behavior.gd" id="9_8vt0k"]
1314

15+
[sub_resource type="Resource" id="Resource_2e3xr"]
16+
script = ExtResource("4_innil")
17+
walk_speed = 200.0
18+
metadata/_custom_type_script = "uid://csev4hv57utxv"
19+
20+
[sub_resource type="Resource" id="Resource_innil"]
21+
script = ExtResource("4_innil")
22+
1423
[sub_resource type="CircleShape2D" id="CircleShape2D_g173s"]
1524
radius = 35.0
1625

@@ -219,13 +228,15 @@ one_shot = true
219228
unique_name_in_owner = true
220229
process_mode = 4
221230
script = ExtResource("3_mswbt")
231+
speeds = SubResource("Resource_2e3xr")
222232
character = NodePath("..")
223233
metadata/_custom_type_script = "uid://cu7ujf83r7yh2"
224234

225235
[node name="ReturningBehavior" type="Node2D" parent="." unique_id=641161332 node_paths=PackedStringArray("character")]
226236
unique_name_in_owner = true
227237
process_mode = 4
228238
script = ExtResource("3_mswbt")
239+
speeds = SubResource("Resource_innil")
229240
character = NodePath("..")
230241
metadata/_custom_type_script = "uid://cu7ujf83r7yh2"
231242

@@ -356,6 +367,8 @@ max_distance = 500.0
356367
bus = &"SFX"
357368

358369
[connection signal="timeout" from="WaitingTimer" to="." method="_on_waiting_timer_timeout"]
370+
[connection signal="got_stuck" from="PatrollingBehavior" to="." method="_on_patrolling_behavior_got_stuck"]
371+
[connection signal="point_reached" from="PatrollingBehavior" to="." method="_on_patrolling_behavior_point_reached"]
359372
[connection signal="body_entered" from="InstantDetectionArea" to="." method="_on_instant_detection_area_body_entered"]
360373
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"]
361374
[connection signal="body_exited" from="DetectionArea" to="." method="_on_detection_area_body_exited"]
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# SPDX-FileCopyrightText: The Threadbare Authors
2+
# SPDX-License-Identifier: MPL-2.0
3+
@tool
4+
class_name PointsWalkBehavior
5+
extends BaseCharacterBehavior
6+
## @experimental
7+
##
8+
## Make the character walk through the points of a path.
9+
##
10+
## If the path is closed the character walks in circles. If not, they walk back and forth turning
11+
## around in endings.
12+
## [br][br]
13+
## If the character gets stuck while walking the path, they turn around.
14+
15+
## Emitted when a point of the path is reached.
16+
## This could be used to wait standing for a bit in these points.
17+
signal point_reached
18+
19+
## Emitted when [member character] got stuck while walking the path.
20+
signal got_stuck
21+
22+
## Parameters controlling the speed at which this character walks. If unset, the default values of
23+
## [CharacterSpeeds] are used.
24+
@export var speeds: CharacterSpeeds
25+
26+
## The walking path.
27+
@export var walking_path: Path2D:
28+
set = _set_walking_path
29+
30+
## Index of the previous patrol point, -1 means that there isn't a previous
31+
## point yet.
32+
var previous_point_index: int = -1
33+
34+
## Index of the current patrol point.
35+
var current_point_index: int = 0
36+
37+
## The walking target position.
38+
var target_position: Vector2
39+
40+
## True if the [member walking_path] is closed, in which case the character will walk in
41+
## circles.
42+
var is_path_closed: bool
43+
44+
45+
func _set_walking_path(new_walking_path: Path2D) -> void:
46+
walking_path = new_walking_path
47+
update_configuration_warnings()
48+
if walking_path:
49+
_advance_target_patrol_point()
50+
var local_point_position: Vector2 = walking_path.curve.get_point_position(
51+
current_point_index
52+
)
53+
target_position = walking_path.to_global(local_point_position)
54+
55+
56+
func _get_configuration_warnings() -> PackedStringArray:
57+
var warnings := super._get_configuration_warnings()
58+
if not walking_path:
59+
warnings.append("Walking Path property must be set.")
60+
return warnings
61+
62+
63+
func _ready() -> void:
64+
super._ready()
65+
66+
if not speeds:
67+
speeds = CharacterSpeeds.new()
68+
69+
_set_walking_path(walking_path)
70+
71+
72+
func _physics_process(delta: float) -> void:
73+
character.velocity = character.global_position.direction_to(target_position) * speeds.walk_speed
74+
character.move_and_slide()
75+
76+
var collision: KinematicCollision2D = character.get_last_slide_collision()
77+
78+
# If the distance it was able to travel is a lot lower than the remainder,
79+
# it's stuck and we can emit the path_blocked signal so the guard can
80+
# handle that case
81+
if collision and collision.get_travel().length() < collision.get_remainder().length() / 20.0:
82+
if previous_point_index > -1:
83+
var new_patrol_point: int = previous_point_index
84+
previous_point_index = current_point_index
85+
current_point_index = new_patrol_point
86+
got_stuck.emit()
87+
88+
if (
89+
(
90+
character.global_position.distance_to(target_position)
91+
<= character.velocity.length() * delta
92+
)
93+
and walking_path
94+
):
95+
_advance_target_patrol_point()
96+
var local_point_position: Vector2 = walking_path.curve.get_point_position(
97+
current_point_index
98+
)
99+
target_position = walking_path.to_global(local_point_position)
100+
point_reached.emit()
101+
102+
103+
## Return true if the end of the path is the same point as the beginning.
104+
func _is_path_closed() -> bool:
105+
if walking_path.curve.point_count < 3:
106+
return false
107+
108+
var first_point_position: Vector2 = walking_path.curve.get_point_position(0)
109+
var last_point_position: Vector2 = walking_path.curve.get_point_position(
110+
walking_path.curve.point_count - 1
111+
)
112+
113+
return first_point_position.is_equal_approx(last_point_position)
114+
115+
116+
## Calculate and set the next point in the patrol path.
117+
## The guard would circle back if the path is open, and go in rounds if the
118+
## path is closed.
119+
func _advance_target_patrol_point() -> void:
120+
# TODO: Assume as existing and add an editor warning instead.
121+
if not walking_path or not walking_path.curve or walking_path.curve.point_count < 2:
122+
return
123+
124+
var new_patrol_point_idx: int
125+
126+
if _is_path_closed():
127+
# amount of points - 1 is used here because in a closed path, the
128+
# last and first patrol points are the same. So, this lets us skip
129+
# that repeated point and go for the first one that is different
130+
new_patrol_point_idx = (current_point_index + 1) % (walking_path.curve.point_count - 1)
131+
else:
132+
var at_last_point: bool = current_point_index == (walking_path.curve.point_count - 1)
133+
var at_first_point: bool = current_point_index == 0
134+
var going_backwards_in_path: bool = previous_point_index > current_point_index
135+
if at_last_point:
136+
# When reaching the end of the path, it starts walking back
137+
new_patrol_point_idx = current_point_index - 1
138+
elif at_first_point:
139+
# If it's at first point is either because it was walking back
140+
# or because it's the first time it will move, in any case, it moves
141+
# forward
142+
new_patrol_point_idx = current_point_index + 1
143+
elif going_backwards_in_path:
144+
new_patrol_point_idx = current_point_index - 1
145+
else:
146+
new_patrol_point_idx = current_point_index + 1
147+
148+
previous_point_index = current_point_index
149+
current_point_index = new_patrol_point_idx
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://cu7ujf83r7yh2

0 commit comments

Comments
 (0)