class_name PlayerPickup extends Node ## 拾取组件:**按住左键拖拽,松手放下,按 Q 朝准心方向甩出去**。 ## 按下左键时用射线抓起正前方的 RigidBody3D,按住期间持续吸在 InteractionPos 前; ## 松开左键即原地放开,按 Q 则沿视线方向把手上的物体甩出去。 ## 只管「抓 / 拖 / 放 / 甩」,不碰移动与视角。 ## ## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 注入射线、持握点与玩家本体。 ## 拖拽力度:物体被拉向持握点的速度系数。越大跟手越紧,但过大容易来回抖。 ## 注意:它同时决定了拖拽时的稳态滞后 —— 滞后距离 = 移动速度 / pick_power。 @export var pick_power: float = 10.0 ## 投掷初速度(米/秒):按 Q 甩出去时给物体的速度大小。 @export var throw_speed: float = 12.0 ## 拖拽速度上限(米/秒)。防止物体离得远时被一股脑拽飞。 @export var max_drag_speed: float = 30.0 ## 交互状态变化时发出。 ## hovering = 准心正指着可拖拽的刚体(还没抓);holding = 正抓着东西。 ## Player 接住这个信号去切准心外观。只在两者之一变化时发,避免每帧刷 UI。 signal interact_state_changed(hovering: bool, holding: bool) var _current: RigidBody3D = null var _ray: RayCast3D var _hold_pos: Marker3D ## 玩家本体。用来给被拖物体加碰撞例外,以及做速度前馈。 var _body: CharacterBody3D ## 上一次发出的状态,用来只在「变化那一刻」发信号。 var _hovering: bool = false var _holding: bool = false ## 由 Player 在 _ready() 注入依赖。body 是玩家本体(CharacterBody3D)。 func setup(ray: RayCast3D, hold_pos: Marker3D, body: CharacterBody3D) -> void: _ray = ray _hold_pos = hold_pos _body = body func _unhandled_input(event: InputEvent) -> void: # 只有鼠标锁进游戏(真正在玩)时才响应抓 / 拖 / 放 / 甩。 # 否则「点一下把鼠标锁回来」的那次点击会顺带抓起一个物体。 if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: return # 长按拖拽:按下抓起,松开放下;拖拽途中按 Q 可以把手上的东西甩出去。 # 按住期间不需要任何输入处理 —— 物体靠 physics_step() 每物理帧持续吸住。 if event.is_action_pressed("carry"): _grab() elif event.is_action_released("carry"): _drop() elif event.is_action_pressed("throw_carry"): _throw() ## 每物理帧由 Player 调用一次,排在 move_and_slide() 之后。 ## 用速度把物体拽向持握点,而不是直接改 global_position —— ## 这样它仍会和场景里其他刚体正常碰撞,不会被硬塞进墙里。 func physics_step() -> void: _update_interact_state() # 用 is_instance_valid 而不是 `== null`:物体可能在拖拽途中被 destroy, # 那种情况下引用不是 null 而是「已被释放」,直接取 global_position 会报错。 if not is_instance_valid(_current) or not is_instance_valid(_hold_pos): return var to_hold := _hold_pos.global_position - _current.global_position var desired := to_hold * pick_power # 速度前馈:把玩家自身的速度加进去。 # # 只靠 to_hold * pick_power 的话,它是一个一阶跟随 —— 人匀速前进时物体必然 # 落后一个稳态距离,大小 = 移动速度 / pick_power。按 5 / 10 算就是 0.5 米, # 比持握点离身体的 0.44 米还远,于是走着走着物体就退进了玩家碰撞体里, # 被物理推出来、又被拽回去,两边打架 —— 看起来就是「一卡一卡」。 # 加上玩家速度后,匀速行走时滞后归零,物体稳稳待在身前。 if _body != null: desired += _body.velocity if desired.length() > max_drag_speed: desired = desired.normalized() * max_drag_speed _current.set_linear_velocity(desired) ## 检测「指着可拖拽物 / 正抓着东西」这两个状态,**只在变化时**发信号。 ## 每帧都发会让 UI 无谓重绘,所以这里自己记着上一次的结果。 func _update_interact_state() -> void: if _ray == null: return # 主动刷新一次:RayCast3D 的结果通常滞后于本帧的 move_and_slide, # 不强制更新的话,跑动中瞄到物体要慢一帧才亮。 _ray.force_raycast_update() var hovering := _ray.get_collider() is RigidBody3D var holding := is_instance_valid(_current) if hovering == _hovering and holding == _holding: return _hovering = hovering _holding = holding interact_state_changed.emit(_hovering, _holding) func _grab() -> void: if _ray == null or is_instance_valid(_current): return var collider := _ray.get_collider() if collider is RigidBody3D: _current = collider as RigidBody3D # 让被拖的物体和玩家本体**互不碰撞**。 # 否则物体一旦挨到身体就会被物理推出,而拖拽又立刻把它拉回来 —— # 两个解算器互相打架,表现就是持续的高频抖动。 # 只解除「物体 ↔ 玩家」这一对,物体和场景里其他东西照样正常碰撞。 if _body != null: _current.add_collision_exception_with(_body) func _drop() -> void: # 故意不清零线速度:松手时保留拖动惯性,甩出去的手感更自然。 # 想要「就地放下、不再滑出去」的话,这里补一句: # if is_instance_valid(_current): _current.set_linear_velocity(Vector3.ZERO) _forget(_current) _current = null ## 清掉抓取时留下的碰撞例外。物体可能已被销毁,所以先判有效性。 func _forget(body: RigidBody3D) -> void: if is_instance_valid(body) and _body != null: body.remove_collision_exception_with(_body) ## 按 Q 投掷:沿视线方向把物体甩出去,随后立刻松手。 func _throw() -> void: if not is_instance_valid(_current) or not is_instance_valid(_hold_pos): return # 持握点挂在摄像机下、且只有平移没有旋转,所以它的 basis 就是视线朝向, # 其 -Z 即「准心正前方」。这样不必再给组件注入一台摄像机。 var dir := -_hold_pos.global_transform.basis.z _current.set_linear_velocity(dir * throw_speed) # 想让手感更「物理」(重物飞得慢、轻物飞得远)可以换成给冲量: # _current.apply_central_impulse(dir * throw_speed) _forget(_current) _current = null ## 当前是否抓着东西。调试用,也方便以后加「搬运时不能开枪」这类规则。 func has_object() -> bool: return is_instance_valid(_current)