From 01ee8f051fcaabbd89d3827bc86721df59323c32 Mon Sep 17 00:00:00 2001 From: RealMelwei Date: Sun, 12 Oct 2025 17:59:43 +0200 Subject: [PATCH] Refactored and documented leech and segment code. --- enemies/leech/giant_leech.tscn | 8 ++-- enemies/leech/leech.gd | 74 +++++++++++++++++++++++++--------- enemies/leech/leech.tscn | 8 ++-- enemies/leech/segment.gd | 4 +- 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/enemies/leech/giant_leech.tscn b/enemies/leech/giant_leech.tscn index c1e4da2..477e5a5 100644 --- a/enemies/leech/giant_leech.tscn +++ b/enemies/leech/giant_leech.tscn @@ -49,20 +49,20 @@ scale = Vector2(2, 2) [node name="EarthAligner" parent="." instance=ExtResource("3_vk62e")] -[node name="RayCast2D" type="Area2D" parent="."] +[node name="StepChecker" type="Area2D" parent="."] position = Vector2(248, 31.2) collision_layer = 0 collision_mask = 8 -[node name="CollisionShape2D" type="CollisionShape2D" parent="RayCast2D"] +[node name="CollisionShape2D" type="CollisionShape2D" parent="StepChecker"] position = Vector2(0, 25) shape = SubResource("RectangleShape2D_cq6dk") -[node name="RayCast2D2" type="Area2D" parent="."] +[node name="GroundSensor" type="Area2D" parent="."] collision_layer = 0 collision_mask = 8 -[node name="CollisionShape2D" type="CollisionShape2D" parent="RayCast2D2"] +[node name="CollisionShape2D" type="CollisionShape2D" parent="GroundSensor"] position = Vector2(0, 25) shape = SubResource("RectangleShape2D_cq6dk") diff --git a/enemies/leech/leech.gd b/enemies/leech/leech.gd index 819756b..5fd6fa9 100644 --- a/enemies/leech/leech.gd +++ b/enemies/leech/leech.gd @@ -1,7 +1,10 @@ extends EnemyHurtbox +# The distance from one end to the other while standing @export var broadth = 250 -@export var move_dir = -1 -@export var angular_speed = 0.25 +var move_dir = [-1, 1].pick_random() +const angular_speed = 0.25 + +# The angle from the base to the moving end var angle = 0.0 if move_dir == 1 else PI @onready var segments : Array[Node] = $Segments.get_children() @@ -10,25 +13,37 @@ var dead = false func _process(delta: float) -> void: if dead: return - if not $RayCast2D2.has_overlapping_bodies(): + # Fall slowly while not grounded + if not $GroundSensor.has_overlapping_bodies(): position += 200 * delta * $EarthAligner.global_from_local(Vector2.DOWN) + var y = position.length() var ratio = - move_dir * broadth / (2 * y) + + # Due to the curvature of the ground, the leech can not just prescribe a semicircle. + # 2 * rot_angle determines how much further than 180° the moving leg has to move. + # This agrees with the angle of the arc spanned by the leeches standing ends. var rot_angle = - 2 * asin(ratio) angle -= TAU * delta * angular_speed * move_dir + + # Once the rotation has finished, the other leg is used as the leeches center. + # The leeches position has to be updated accordingly. if(angle > PI + abs(rot_angle) / 2 or angle < - abs(rot_angle) / 2): angle = fmod(angle + PI, PI) position = position.rotated(rot_angle) - if dead: - return + + # StepChecker determines whether there is ground for the next step. + # Place the StepChecker on the side of the next step to be made. if(move_dir == 1 and angle < 1 or move_dir == -1 and angle > PI - 1): - $RayCast2D.global_position = position.rotated(rot_angle) - $RayCast2D.rotation = rot_angle + $StepChecker.global_position = position.rotated(rot_angle) + $StepChecker.rotation = rot_angle + + # At some point of the movement, check whether there is ground to step on, turn around otherwise. if(move_dir == 1 and angle < 0.5 or move_dir == -1 and angle > PI - 0.5): - if(not $RayCast2D.has_overlapping_bodies()): + if(not $StepChecker.has_overlapping_bodies()): move_dir *= -1 - if dead: - return + + # Update the position and rotation according to the end's position for i in range(segment_count): var segment_pos_data = calculate_segment_location_and_rotation(i) if not is_instance_valid(segments[i]): @@ -38,7 +53,11 @@ func _process(delta: float) -> void: func calculate_segment_location_and_rotation (i) -> Dictionary: var aerial_end_location = Vector2.from_angle(-angle) * broadth + + # Compute the gravicenter of the standing end, the moving end and the apex of the movement. var gravicenter = (aerial_end_location + Vector2(0, -broadth) + Vector2.ZERO) / 3 + + # Compute the circle through the above gravicenter, the standing end and the moving end. var ax = gravicenter.x var ay = gravicenter.y var bx = aerial_end_location.x @@ -48,31 +67,46 @@ func calculate_segment_location_and_rotation (i) -> Dictionary: var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) var ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d var uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d - var center = Vector2(ux, uy) - var radius = center.length() + var circum_center = Vector2(ux, uy) + var radius = circum_center.length() + + # Determine the direction of the correct arc between the standing and the moving end var switch_arc_dir = false + # When the moving end crosses above the standing end, the circumcenter jumps + # from one side of the leech to the other, reverting the direction of the arc if ux < 0: switch_arc_dir = !switch_arc_dir - if center.angle() > 1: + + # For sufficiently large size of circum_center.angle() it can happen that + # the sign of the (circum_center - aerial_end_location).angle() flips while + # that of circum_center.angle() doesn't, which has to be counteracted. + if circum_center.angle() > 1: switch_arc_dir = !switch_arc_dir - var angle1 = - PI + center.angle() - var angle2 = - PI + (center-aerial_end_location).angle() + var angle1 = - PI + circum_center.angle() + var angle2 = - PI + (circum_center - aerial_end_location).angle() if(switch_arc_dir): angle1 += TAU - if radius < 10000000: - return {"position": center + radius * Vector2.from_angle((i * angle1 + (segment_count - 1 - i) * angle2)/(segment_count - 1)), - "rotation": (i * angle1 + (segment_count - 1 - i) * angle2)/ (segment_count - 1) + sign(ux) * PI/2} - - else: + # In the edge case where the leech is almost a straight line, the circum_center + # and radius approach infty, leading to numerical errors producing flickering. + # This is ruled out by treating these cases as actual straight lines. + if radius > 1000000: return {"position" : Vector2.UP * broadth * i / (segment_count - 1), "rotation" : 3 * PI / 2} + + # Otherwise, the segments are distributed regularly along the arc. + else: + var arc_angle = (i * angle1 + (segment_count - 1 - i) * angle2)/(segment_count - 1) + return {"position": circum_center + radius * Vector2.from_angle(arc_angle), + "rotation": arc_angle + sign(ux) * PI/2} + func _on_damage_taken(_damage, _dir, _id): $AudioStreamPlayer2D.play() func _on_death(): + # Free all other children while waiting for the death sound to play. dead = true for child in get_children(): if not child is AudioStreamPlayer2D: diff --git a/enemies/leech/leech.tscn b/enemies/leech/leech.tscn index bacf79a..9a4f6b1 100644 --- a/enemies/leech/leech.tscn +++ b/enemies/leech/leech.tscn @@ -39,20 +39,20 @@ scale = Vector2(-1, -1) [node name="EarthAligner" parent="." instance=ExtResource("3_0r7dp")] -[node name="RayCast2D" type="Area2D" parent="."] +[node name="StepChecker" type="Area2D" parent="."] position = Vector2(248, 31.2) collision_layer = 0 collision_mask = 8 -[node name="CollisionShape2D" type="CollisionShape2D" parent="RayCast2D"] +[node name="CollisionShape2D" type="CollisionShape2D" parent="StepChecker"] position = Vector2(0, 14.8) shape = SubResource("RectangleShape2D_cq6dk") -[node name="RayCast2D2" type="Area2D" parent="."] +[node name="GroundSensor" type="Area2D" parent="."] collision_layer = 0 collision_mask = 8 -[node name="CollisionShape2D" type="CollisionShape2D" parent="RayCast2D2"] +[node name="CollisionShape2D" type="CollisionShape2D" parent="GroundSensor"] position = Vector2(0, 8.8) shape = SubResource("RectangleShape2D_cq6dk") diff --git a/enemies/leech/segment.gd b/enemies/leech/segment.gd index dd66406..12fae72 100644 --- a/enemies/leech/segment.gd +++ b/enemies/leech/segment.gd @@ -2,12 +2,10 @@ extends Area2D @onready var player = get_tree().get_root().get_node_or_null("main/Player") var damage = 1 -#signal segment_damaged - func _process(_delta: float) -> void: if (player != null and overlaps_body(player)): player.hurt(damage, self.global_position-player.global_position) +# Forward taken damage to the parent leech. func _on_hurtbox_damaged(dmg : int, dir : Vector2, id): - #segment_damaged.emit(dmg, dir) get_parent().get_parent().hurt(dmg, dir, id)