diff --git a/scenes/game_scene/levels/3dlevel.tscn b/scenes/game_scene/levels/3dlevel.tscn index 9e4b197..1c1f3ea 100644 --- a/scenes/game_scene/levels/3dlevel.tscn +++ b/scenes/game_scene/levels/3dlevel.tscn @@ -1,6 +1,7 @@ [gd_scene format=3 uid="uid://4ajccsn6o10n"] [ext_resource type="PackedScene" uid="uid://cbual4qbgv2fc" path="res://scenes/player.tscn" id="1_e2xum"] +[ext_resource type="PackedScene" uid="uid://x4xnc85khpl5" path="res://scenes/path_3d_rope.tscn" id="2_jetiq"] [sub_resource type="BoxShape3D" id="BoxShape3D_xcqlg"] size = Vector3(40, 2, 5) @@ -8,6 +9,15 @@ size = Vector3(40, 2, 5) [sub_resource type="BoxMesh" id="BoxMesh_xcqlg"] size = Vector3(40, 2, 5) +[sub_resource type="Curve3D" id="Curve3D_jetiq"] +resource_local_to_scene = true +bake_interval = 512.0 +_data = { +"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5), +"tilts": PackedFloat32Array(0, 0) +} +point_count = 2 + [node name="3Dlevel" type="Node3D" unique_id=1570360592] [node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=362192398] @@ -34,3 +44,11 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 1, 0) [node name="Player2" parent="." unique_id=432105976 instance=ExtResource("1_e2xum")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 1, 0) player_id = 2 + +[node name="Path3DRope" parent="." unique_id=301387047 instance=ExtResource("2_jetiq")] +curve = SubResource("Curve3D_jetiq") +attach_start_path = NodePath("../Player1") +attach_end_path = NodePath("../Player2") +number_of_segments = 32 +cable_thickness = 0.05 +fixed_start_point = false diff --git a/scripts/rope_tether.gd b/scripts/rope_tether.gd index 51f5280..2d3910f 100644 --- a/scripts/rope_tether.gd +++ b/scripts/rope_tether.gd @@ -20,9 +20,23 @@ var attach_start : PhysicsBody3D var attach_end : PhysicsBody3D ## Extra rope length as a fraction of the gap between the two endpoints. -## 0.0 spawns the rope taut (pin joints will fight); ~0.15 gives a natural sag. +## 0.0 is dead taut (pin joints will fight); ~0.15 gives a natural sag. @export_range(0.0, 1.0, 0.01) var slack := 0.15 +## Reel the rope in and out so its length keeps tracking the endpoint gap, +## instead of staying fixed at whatever it was when the level loaded. +@export var dynamic_length := true + +## Bounds on total rope length, in metres. min_length stops the rope collapsing +## into a stub when the players stand on top of each other; max_length is the +## leash that eventually drags them back together. +@export var min_length := 2.0 +@export var max_length := 20.0 + +## How fast total length changes, in metres per second. Low values feel like a +## winch, high values like the rope is weightless. +@export var reel_speed := 6.0 + ## Constrain segments to the X-Y plane (linear Z, angular X and Y). @export var plane_lock_z := true @@ -33,6 +47,9 @@ var attach_end : PhysicsBody3D # our own copy to place the end joint we add. var _origin_offset := Vector3.ZERO +# Total rope length right now, eased toward _target_length() at reel_speed. +var _rope_length := 0.0 + func _ready() -> void: attach_start = get_node_or_null(attach_start_path) as PhysicsBody3D @@ -42,6 +59,60 @@ func _ready() -> void: super() _apply_plane_lock() _wire_endpoints() + _rope_length = _target_length() + + +func _physics_process(delta: float) -> void: + if dynamic_length: + _reel(delta) + # Base script redraws the CSG curve from the segment transforms and capsule + # heights, so it has to run after the resize. + super(delta) + + +## Length the rope wants to be for the current endpoint gap. +func _target_length() -> float: + if attach_start == null or attach_end == null: + return maxf(distance, min_length) + var span := attach_start.global_position.distance_to(attach_end.global_position) + return clampf(span * (1.0 + slack), min_length, max_length) + + +func _reel(delta: float) -> void: + var previous := _rope_length + _rope_length = move_toward(_rope_length, _target_length(), reel_speed * delta) + if absf(_rope_length - previous) > 0.0001: + _set_segment_length(_rope_length / float(number_of_segments)) + + +# Resize every capsule and re-anchor the pin joints bracketing it. +# +# Joint3D only derives its anchor points from the node transforms when the joint +# is (re)configured on tree entry, so moving the joint nodes here would do +# nothing — the anchors go straight to the physics server instead. +# +# Capsules run along local Y, and the base script's curve update shows +Y is the +# end nearer the rope's start, so joint i sits at -Y on segment i-1 and +Y on +# segment i. +func _set_segment_length(segment_length: float) -> void: + # CapsuleShape3D silently clamps height to its diameter; clamp here too so + # the joint anchors match the shape the physics server actually has. + var clamped := maxf(segment_length, cable_thickness * 2.0) + var half := clamped * 0.5 + for segment in segments: + var shape := segment.get_child(0).shape as CapsuleShape3D + shape.height = clamped + for i in joints.size(): + var rid := joints[i].get_rid() + if i == 0: + # node_b is the attached body, whose anchor must not move. + PhysicsServer3D.pin_joint_set_local_a(rid, Vector3(0, half, 0)) + elif i < segments.size(): + PhysicsServer3D.pin_joint_set_local_a(rid, Vector3(0, -half, 0)) + PhysicsServer3D.pin_joint_set_local_b(rid, Vector3(0, half, 0)) + else: + # Tail joint: node_a is the last segment, node_b the attached body. + PhysicsServer3D.pin_joint_set_local_a(rid, Vector3(0, -half, 0)) # Rebuild the curve as a sagging arc between the two endpoints so the rope @@ -58,7 +129,14 @@ func _fit_curve_to_endpoints() -> void: # (a rope between two grounded players dips below the floor), and segments # that spawn embedded tunnel straight through it. Starting high is always # clear, and gravity drapes the rope into place within a few frames. - var mid := (a + b) * 0.5 + Vector3(0, span * slack, 0) + # + # Curve points get zero tangents, so the baked path is the straight pair + # a->mid->b: bowing by h gives a length of 2*sqrt((span/2)^2 + h^2). Solve + # that for the length the reel is going to ask for anyway, so the rope does + # not lurch on the first frame. + var target := _target_length() + var bow := 0.5 * sqrt(maxf(target * target - span * span, 0.0)) + var mid := (a + b) * 0.5 + Vector3(0, bow, 0) var fitted := Curve3D.new() # The scene's curve ships with a huge bake_interval, which would make @@ -72,7 +150,6 @@ func _fit_curve_to_endpoints() -> void: # the value is correct regardless of when that initializer runs. distance = curve.get_baked_length() - func _apply_plane_lock() -> void: for segment in segments: segment.collision_layer = segment_collision_layer @@ -102,3 +179,6 @@ func _wire_endpoints() -> void: end_joint.set_param(PinJoint3D.PARAM_BIAS, joint_bias_or_stiffness) end_joint.set_param(PinJoint3D.PARAM_IMPULSE_CLAMP, max_impulse) joints.append(end_joint) + +#func _physics_process(_delta: float) -> void: + #curve.bake_interval += 0.01