移除绳索

This commit is contained in:
2026-10-08 17:21:44 +08:00
parent 1bd447a56e
commit 8e463d9247
3 changed files with 0 additions and 193 deletions
-180
View File
@@ -1,180 +0,0 @@
@tool
extends Node3D
class_name Zipline
## 滑索(Zipline):两点之间一条会下垂的钢缆。玩家走到缆下按 E 挂上,
## 沿缆滑向较低的那一端,中途可按 S 刹车、按空格主动跳离。
##
## 场景结构(res://world/zipline/zipline.tscn):
## Zipline (Node3D, 本脚本)
## ├─ AnchorA (Marker3D) ← 高处起点,在视口里直接拖
## └─ AnchorB (Marker3D) ← 低处终点
## 缆绳网格是本脚本在 _ready() 里生成的一个 MeshInstance3D 子节点,
## 故意不给 owner,所以不会被写进 .tscn —— 它完全由两个锚点推导出来,
## 不存在「文件里的网格和锚点对不上」这种脏数据。
##
## 手感参数(速度/刹车/吸附半径)放在「缆」这一侧而不是玩家身上:
## 同一张关卡里可以有快缆和慢缆,玩家只负责读参数,不负责定义手感。
##
## 曲线模型:两点连线再减去一个抛物线形下垂量
## P(t) = lerp(A, B, t) - UP * sag * 4t(1-t)
## 权重 4t(1-t) 在 t=0/1 处为 0、t=0.5 处为 1,所以锚点高度就是真实挂点高度、
## sag 就是中点下垂深度 —— 调参所见即所得,不用去解悬链线方程。
##
## 约束:本节点不要旋转/缩放,靠拖动两个锚点摆位。
## 所有滑索都进这个组,玩家靠它找缆
const GROUP: StringName = &"zipline"
## 缆绳截面的边数。缆很细,6 边足够,还省顶点
const RING_SEGMENTS: int = 6
@export_group("缆绳")
## 中点下垂深度(米)。0 = 绷直。真实滑索是绷紧的,所以别调太大
@export_range(0.0, 12.0, 0.05) var sag: float = 1.2
## 沿缆采样段数。越高越圆滑,也越费顶点
@export_range(8, 128, 1) var segments: int = 48
## 缆绳半径(米)
@export_range(0.01, 0.3, 0.005) var radius: float = 0.05
## 缆绳材质。留空则用内置的深色金属
@export var cable_material: Material
@export_group("乘坐")
## 人挂在缆下方多深(米)。约等于「双手举过头顶抓住缆」的高度
@export var hang_offset: float = 1.15
## 玩家离缆多近能按 E 挂上(米)。给得宽一点:缆悬在头顶时也算「够得着」
@export var attach_radius: float = 4.0
## 沿缆最高速度(米/秒)
@export var max_speed: float = 18.0
## 重力沿缆方向的缩放。1 = 完全按坡度加速,调小则「黏」一些
@export var gravity_scale: float = 1.0
## 按住后退键时的刹车减速度(米/秒²)
@export var brake_accel: float = 14.0
## 剩余行程短于此值就不让挂(米)。否则刚挂上就到头,像被弹开
@export var min_ride: float = 3.0
@onready var anchor_a: Marker3D = $AnchorA
@onready var anchor_b: Marker3D = $AnchorB
## 缆绳网格节点,_ready() 里现造
var _cable: MeshInstance3D = null
## 缆绳总长(米)。生成网格时顺带算出来,玩家用它把速度换算成 t 的推进量
var length: float = 0.0
## 编辑器里用来比对「锚点/参数变了没有」,避免每帧重建网格
var _editor_sig: String = ""
func _ready() -> void:
add_to_group(GROUP)
_cable = MeshInstance3D.new()
_cable.name = "Cable"
add_child(_cable)
_rebuild_cable()
# 编辑器下开着 _process,拖动锚点能立刻看到缆绳跟着变;运行时不需要
set_process(Engine.is_editor_hint())
func _process(_delta: float) -> void:
if anchor_a == null or anchor_b == null:
return
# 只在签名变化时重建 —— SurfaceTool 生成网格不便宜,不能每帧做
var sig := "%s|%s|%.3f|%d|%.4f" % [anchor_a.position, anchor_b.position, sag, segments, radius]
if sig == _editor_sig:
return
_editor_sig = sig
_rebuild_cable()
# ---------------- 对外接口(玩家控制器只读这几个) ----------------
## 缆上参数 t∈[0,1] 处的世界坐标。
## 曲线先在**本地空间**算完再转世界:sag 是本地单位,父节点一有缩放整条缆要跟着一起缩,
## 形状才不走样(混用 global 锚点 + 未缩放的 sag,下垂比例会随父节点缩放而变)。
func sample(t: float) -> Vector3:
return to_global(_curve_local(t))
## 缆上参数 t 处的世界方向(单位向量,指向 B)。
## 用解析导数而不是前后差分:差分在 t=0/1 处要越界取样,端点会偏。
func tangent(t: float) -> Vector3:
var d := (anchor_b.position - anchor_a.position) - Vector3.UP * (sag * 4.0 * (1.0 - 2.0 * t))
return (global_transform.basis * d).normalized()
## 乘坐时人应该待的世界坐标:缆点再往下挂 hang_offset。
func ride_point(t: float) -> Vector3:
return to_global(_curve_local(t) - Vector3.UP * hang_offset)
## 找出世界坐标 pos 在缆上最近的一点,供玩家「就近吸附」。
## 返回 { t, point, distance, dir, remaining }:
## dir +1 表示往 B 滑、-1 表示往 A 滑(永远是往低的那端)
## remaining 沿 dir 方向还剩多少米
func closest_point(pos: Vector3) -> Dictionary:
var best_t := 0.0
var best_d := INF
for i in range(segments + 1):
var t := float(i) / float(segments)
var d := sample(t).distance_squared_to(pos)
if d < best_d:
best_d = d
best_t = t
var dir := _downhill_dir()
return {
"t": best_t,
"point": sample(best_t),
"distance": sqrt(best_d),
"dir": dir,
"remaining": absf((1.0 if dir > 0 else 0.0) - best_t) * length,
}
# ---------------- 内部 ----------------
## 低的那一端才是出口 —— 滑索只会往下滑。两端等高时退回 A→B。
func _downhill_dir() -> int:
return 1 if anchor_b.global_position.y <= anchor_a.global_position.y else -1
## 下垂曲线在**本节点局部空间**的点。sag 是本地单位,所以父节点缩放时整条缆一起缩、形状不变。
func _curve_local(t: float) -> Vector3:
return anchor_a.position.lerp(anchor_b.position, t) - Vector3.UP * (sag * 4.0 * t * (1.0 - t))
## 把下垂曲线扫成一根管子。用三角形带:每个截面 RING_SEGMENTS+1 个顶点,
## 多出来的那个和首顶点重合,接缝处法线才连续、不会裂开一条缝。
func _rebuild_cable() -> void:
if _cable == null or anchor_a == null or anchor_b == null:
return
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
var prev := _curve_local(0.0)
length = 0.0
for i in range(segments + 1):
var t := float(i) / float(segments)
var p := _curve_local(t)
if i > 0:
length += p.distance_to(prev)
prev = p
# 截面参考轴取「切线 × 世界上方向」。切线接近竖直时叉乘会退化成零向量,
# 这时换 RIGHT 兜底,否则 normalize() 会吐 NaN 把整根缆搅烂。
var tan_dir := (_curve_local(minf(t + 0.01, 1.0)) - _curve_local(maxf(t - 0.01, 0.0))).normalized()
var n := tan_dir.cross(Vector3.UP)
if n.length_squared() < 0.0001:
n = tan_dir.cross(Vector3.RIGHT)
n = n.normalized()
var binormal := n.cross(tan_dir).normalized()
for j in range(RING_SEGMENTS + 1):
var ang := TAU * float(j) / float(RING_SEGMENTS)
var off := n * cos(ang) + binormal * sin(ang)
st.set_normal(off)
st.add_vertex(p + off * radius)
_cable.mesh = st.commit()
var mat: Material = cable_material
if mat == null:
var std := StandardMaterial3D.new()
std.albedo_color = Color(0.14, 0.15, 0.18)
std.metallic = 0.85
std.roughness = 0.3
# 三角形带的绕序不保证,直接双面,省得算错朝向
std.cull_mode = BaseMaterial3D.CULL_DISABLED
mat = std
_cable.material_override = mat
-1
View File
@@ -1 +0,0 @@
uid://ddh4pqdknx8yf
-12
View File
@@ -1,12 +0,0 @@
[gd_scene format=3 uid="uid://12qonfnowpas"]
[ext_resource type="Script" uid="uid://ddh4pqdknx8yf" path="res://world/zipline/zipline.gd" id="1_pj3rq"]
[node name="Zipline" type="Node3D" unique_id=982324784]
script = ExtResource("1_pj3rq")
[node name="AnchorA" type="Marker3D" parent="." unique_id=267373356]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 8, -24)
[node name="AnchorB" type="Marker3D" parent="." unique_id=1805183028]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 18)