Files
agame/character/player/player_pickup.gd
T
2026-10-09 15:28:00 +08:00

110 lines
4.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name PlayerPickup
extends Node
## 拾取组件:**按住左键拖拽,松手放下,按 Q 朝准心方向甩出去**。
## 按下左键时用射线抓起正前方的 RigidBody3D,按住期间持续吸在 InteractionPos 前;
## 松开左键即原地放开,按 Q 则沿视线方向把手上的物体甩出去。
## 只管「抓 / 拖 / 放 / 甩」,不碰移动与视角。
##
## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 注入射线与持握点。
## 拖拽力度:物体被拉向持握点的速度系数。越大跟手越紧,但过大容易来回抖。
@export var pick_power: float = 10.0
## 投掷初速度(米/秒):按 Q 甩出去时给物体的速度大小。
@export var throw_speed: float = 12.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 _hovering: bool = false
var _holding: bool = false
## 由 Player 在 _ready() 注入依赖。
func setup(ray: RayCast3D, hold_pos: Marker3D) -> void:
_ray = ray
_hold_pos = hold_pos
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
_current.set_linear_velocity(to_hold * pick_power)
## 检测「指着可拖拽物 / 正抓着东西」这两个状态,**只在变化时**发信号。
## 每帧都发会让 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:
return
var collider := _ray.get_collider()
if collider is RigidBody3D:
_current = collider as RigidBody3D
func _drop() -> void:
# 故意不清零线速度:松手时保留拖动惯性,甩出去的手感更自然。
# 想要「就地放下、不再滑出去」的话,这里补一句:
# if is_instance_valid(_current): _current.set_linear_velocity(Vector3.ZERO)
_current = null
## 按 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)
_current = null
## 当前是否抓着东西。调试用,也方便以后加「搬运时不能开枪」这类规则。
func has_object() -> bool:
return is_instance_valid(_current)