feat(rope): reel tether length to track player distance
Rope length was baked once at load, so it sagged when players closed in and the pin joints fought when they separated. Total length now eases toward clamp(gap * (1 + slack), min, max) at reel_speed. Joint3D only derives anchors from node transforms when the joint is configured on tree entry, so anchors are pushed straight to PhysicsServer3D.pin_joint_set_local_a/_b after resizing the capsules. Also instances the rope in 3dlevel and solves the spawn arc's bow height from the same target length, so the rope no longer lurches on frame one. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4818002bbd
commit
d66570169a
@@ -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
|
||||
|
||||
+83
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user