滑索
This commit is contained in:
+129
-3
@@ -14,9 +14,13 @@ extends CharacterBody3D
|
||||
## │ └─ SpringArm (SpringArm3D) ← 承担俯仰 pitch,自动避障
|
||||
## │ └─ Camera (Camera3D)
|
||||
## ├─ Sync (MultiplayerSynchronizer) ← 同步 position、Body.rotation、health、动画状态
|
||||
## └─ HealthLabel (Label3D) ← 头顶血条(节点配置,不是代码建的)
|
||||
## ├─ HealthLabel (Label3D) ← 头顶血条(节点配置,不是代码建的)
|
||||
## └─ InteractHint (Label3D) ← 靠近滑索时的「按 E」提示,同样在场景里配好
|
||||
##
|
||||
## 操作:WASD/方向键移动,鼠标转视角,Shift 疾跑,空格跳跃,Esc 释放/锁定鼠标
|
||||
## 滑索:走到缆下按 E 挂上,沿缆滑向低端,缆上按 S 刹车、空格跳离。
|
||||
## 手感参数(速度/刹车/吸附半径)全在 Zipline 组件上,本文件只读不定义 —— 见文件末尾。
|
||||
##
|
||||
## 操作:WASD/方向键移动,鼠标转视角,Shift 疾跑,空格跳跃,E 滑索,Esc 释放/锁定鼠标
|
||||
|
||||
## 跳跃输入缓冲帧数(输入事件在渲染帧到达,物理帧读取,避免漏跳)
|
||||
const JUMP_BUFFER_FRAMES: int = 6
|
||||
@@ -55,6 +59,8 @@ const JUMP_BUFFER_FRAMES: int = 6
|
||||
@onready var spring_arm: SpringArm3D = $CameraRig/SpringArm
|
||||
## 头顶血条是 player.tscn 里的 Label3D 节点(3D 世界里的文字,会跟着角色走)
|
||||
@onready var _health_label: Label3D = $HealthLabel
|
||||
## 滑索操作提示,同样在 player.tscn 里配好,默认隐藏
|
||||
@onready var _hint: Label3D = $InteractHint
|
||||
|
||||
## 动画状态机(挂在 Body 下)。只把「运动结果」交给它,也只知道它会自己播动画。
|
||||
@onready var animator: PlayerAnimator = $Body/PlayerAnimator
|
||||
@@ -66,6 +72,18 @@ var _jump_buffer: int = 0
|
||||
## 本节点是否由本机玩家操控。由世界节点在生成时设置 authority 决定,见 world/main.gd。
|
||||
var _is_local: bool = true
|
||||
|
||||
## 当前挂着的滑索。非 null 时 _physics_process 走缆上分支,常规移动/重力/贴地全部让位。
|
||||
## 只在本机权威端写;远端玩家的位置由 Sync 复制过来,跟着缆一起动。
|
||||
var _zipline: Zipline = null
|
||||
## 沿缆的进度参数 t∈[0,1]。缆的几何由 Zipline 负责,这里只记「我走到哪了」。
|
||||
var _zip_t: float = 0.0
|
||||
## 滑动方向:+1 往 AnchorB、-1 往 AnchorA。永远是往低的那一端
|
||||
var _zip_dir: int = 1
|
||||
## 沿缆速度(米/秒)
|
||||
var _zip_speed: float = 0.0
|
||||
## 挂上瞬间「人相对缆」的偏移,随后衰减到 0 —— 吸附是滑过去的,不是瞬移
|
||||
var _zip_grab_offset: Vector3 = Vector3.ZERO
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_report_identity()
|
||||
@@ -100,7 +118,16 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
camera_rig.rotation.y = _yaw
|
||||
spring_arm.rotation.x = _pitch
|
||||
elif event.is_action_pressed("jump"):
|
||||
_jump_buffer = JUMP_BUFFER_FRAMES
|
||||
# 缆上按跳 = 主动脱手,额外给一个向上的小跳,方便够到旁边的平台
|
||||
if _zipline != null:
|
||||
_detach_zipline(true)
|
||||
else:
|
||||
_jump_buffer = JUMP_BUFFER_FRAMES
|
||||
elif event.is_action_pressed("interact"):
|
||||
if _zipline != null:
|
||||
_detach_zipline()
|
||||
else:
|
||||
_attach_zipline()
|
||||
elif event.is_action_pressed("ui_cancel"):
|
||||
set_mouse_captured(Input.mouse_mode != Input.MOUSE_MODE_CAPTURED)
|
||||
elif event is InputEventMouseButton and event.pressed and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
|
||||
@@ -118,6 +145,18 @@ func set_mouse_captured(captured: bool) -> void:
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not _is_local:
|
||||
return
|
||||
|
||||
# 靠近缆时给个操作提示。判定与 _attach_zipline 共用 _find_zipline(),
|
||||
# 免得出现「提示亮了但按 E 挂不上」这种对不上的情况。
|
||||
_hint.visible = _zipline != null or _find_zipline() != null
|
||||
if _hint.visible:
|
||||
_hint.text = "空格 跳离 · E 松手" if _zipline != null else "按 E 挂上滑索"
|
||||
|
||||
# 挂在缆上时,下面的常规重力/输入/贴地全部让位给缆上运动
|
||||
if _zipline != null:
|
||||
_ride_zipline(delta)
|
||||
return
|
||||
|
||||
# --- 重力 ---
|
||||
if is_on_floor():
|
||||
if velocity.y < 0.0:
|
||||
@@ -161,6 +200,93 @@ func _physics_process(delta: float) -> void:
|
||||
animator.set_motion(velocity, is_on_floor(), Input.is_action_pressed("sprint"))
|
||||
|
||||
|
||||
# ---------------- 滑索 ----------------
|
||||
# 拆成三件事:找缆(_find_zipline)、挂上(_attach_zipline)、缆上运动(_ride_zipline)。
|
||||
# 缆的几何与手感参数都在 Zipline 组件里,这里只管「人」的状态,参数只读不定义。
|
||||
|
||||
## 找脚下能挂的缆,找不到返回 null。挂索与「按 E」提示共用同一套判定。
|
||||
func _find_zipline() -> Zipline:
|
||||
var best: Zipline = null
|
||||
var best_distance := INF
|
||||
for node in get_tree().get_nodes_in_group(Zipline.GROUP):
|
||||
var zip := node as Zipline
|
||||
if zip == null or zip.length <= 0.0:
|
||||
continue
|
||||
var info := zip.closest_point(global_position)
|
||||
# 两种情况都不挂:够不着,或者挂上就没几米可滑(后者会让人刚挂上就被弹下来)
|
||||
if info.distance > zip.attach_radius or info.remaining < zip.min_ride:
|
||||
continue
|
||||
if info.distance < best_distance:
|
||||
best_distance = info.distance
|
||||
best = zip
|
||||
return best
|
||||
|
||||
|
||||
## 挂上最近的一条缆,人就近吸附到缆上最近一点。
|
||||
func _attach_zipline() -> void:
|
||||
var zip := _find_zipline()
|
||||
if zip == null:
|
||||
return
|
||||
var info := zip.closest_point(global_position)
|
||||
_zipline = zip
|
||||
_zip_t = info.t
|
||||
_zip_dir = info.dir
|
||||
# 带上入缆时的水平速度,冲上去挂索不会突然一顿
|
||||
_zip_speed = maxf(Vector2(velocity.x, velocity.z).length() * 0.5, 2.0)
|
||||
# 把当前偏移记下来交给 _ride_zipline 逐帧衰减,避免瞬移到缆上
|
||||
_zip_grab_offset = global_position - zip.ride_point(_zip_t)
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
## 脱离缆索。hop=true 时额外给一个向上的小跳(主动跳离用)。
|
||||
func _detach_zipline(hop: bool = false) -> void:
|
||||
if _zipline == null:
|
||||
return
|
||||
# 惯性带走:沿缆方向的速度原样保留,脱手后就是一次抛物线
|
||||
velocity = _zipline.tangent(_zip_t) * float(_zip_dir) * _zip_speed
|
||||
if hop:
|
||||
velocity += Vector3.UP * 3.0
|
||||
_zipline = null
|
||||
_zip_speed = 0.0
|
||||
|
||||
|
||||
## 缆上运动:沿曲线加速下滑,S 刹车,到头自动脱手。
|
||||
##
|
||||
## 位置直接写 global_position,不走 move_and_slide():悬空缆段上没有需要解算的碰撞,
|
||||
## 让物理引擎去推反而会和「贴着曲线走」这个硬约束打架。
|
||||
## velocity 仍然维护 —— 一是给动画器读,二是脱手后能带着惯性飞出去。
|
||||
func _ride_zipline(delta: float) -> void:
|
||||
var tan := _zipline.tangent(_zip_t) * float(_zip_dir)
|
||||
# 重力沿缆方向的分量:缆越陡加速越猛,平缆上几乎不加速。
|
||||
# 下坡时 tan.y 是负的,所以前面要加负号。
|
||||
_zip_speed += -_gravity * _zipline.gravity_scale * tan.y * delta
|
||||
if Input.is_action_pressed("move_back"):
|
||||
_zip_speed -= _zipline.brake_accel * delta
|
||||
_zip_speed = clampf(_zip_speed, 0.0, _zipline.max_speed)
|
||||
|
||||
# t 是曲线参数而不是弧长,但 sag 相对缆长很小时两者差别可以忽略
|
||||
var next_t := _zip_t + float(_zip_dir) * _zip_speed / _zipline.length * delta
|
||||
if next_t <= 0.0 or next_t >= 1.0:
|
||||
# 到头了:先落到端点再脱手,免得停在离端点还差一截的地方
|
||||
_zip_t = clampf(next_t, 0.0, 1.0)
|
||||
global_position = _zipline.ride_point(_zip_t)
|
||||
_detach_zipline()
|
||||
return
|
||||
|
||||
_zip_t = next_t
|
||||
_zip_grab_offset = _zip_grab_offset.lerp(Vector3.ZERO, clampf(delta / 0.12, 0.0, 1.0))
|
||||
global_position = _zipline.ride_point(_zip_t) + _zip_grab_offset
|
||||
velocity = tan * _zip_speed
|
||||
var travel_yaw := atan2(-tan.x, -tan.z)
|
||||
body.rotation.y = lerp_angle(body.rotation.y, travel_yaw, clampf(turn_speed * delta, 0.0, 1.0))
|
||||
# 相机也跟着摆到行进方向背后 —— 否则人是横着飞的,看不见前面要去哪。
|
||||
# 速率刻意比转身慢:玩家用鼠标看的视角不会被立刻拽回去。
|
||||
_yaw = lerp_angle(_yaw, travel_yaw, clampf(3.0 * delta, 0.0, 1.0))
|
||||
camera_rig.rotation.y = _yaw
|
||||
# 缆上没有「落地」概念,交给动画器当下落姿态处理(模型里没有专门的滑索片段)
|
||||
animator.set_motion(velocity, false, false)
|
||||
|
||||
|
||||
func _refresh_health_label() -> void:
|
||||
# 血条节点在 player.tscn 里(Hud/HealthLabel)。
|
||||
# 同步可能在 _ready() 之前就把 health 写进来,那时 _health_label 还是 null,直接跳过。
|
||||
|
||||
@@ -2810,3 +2810,13 @@ no_depth_test = true
|
||||
text = "HP 100 / 100"
|
||||
font_size = 64
|
||||
outline_size = 8
|
||||
|
||||
[node name="InteractHint" type="Label3D" parent="." unique_id=1718788495]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.75, 0)
|
||||
visible = false
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
modulate = Color(1, 0.95, 0.6, 1)
|
||||
text = "按 E 挂上滑索"
|
||||
font_size = 48
|
||||
outline_size = 8
|
||||
|
||||
Reference in New Issue
Block a user