可拖拽判断(PickupItem 标记)

This commit is contained in:
2026-10-12 17:05:19 +08:00
parent 77fefd2505
commit 02c8d36f8d
6 changed files with 144 additions and 29 deletions
+54 -9
View File
@@ -5,6 +5,9 @@ extends Node
## 松开左键即原地放开,按 Q 则沿视线方向把手上的物体甩出去。
## 只管「抓 / 拖 / 放 / 甩」,不碰移动与视角。
##
## **能否被拖拽由 PickupItem 标记决定**(scripts/item.gd):
## 只有挂了该脚本、且 can_pickup 为 true 的刚体才抓得起来。判定见 can_grab()。
##
## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 注入射线、持握点与玩家本体。
## 拖拽力度:物体被拉向持握点的速度系数。越大跟手越紧,但过大容易来回抖。
@@ -14,6 +17,10 @@ extends Node
@export var throw_speed: float = 12.0
## 拖拽速度上限(米/秒)。防止物体离得远时被一股脑拽飞。
@export var max_drag_speed: float = 30.0
## 没挂 PickupItem 标记的刚体,是否也允许拖拽。
## false(默认)= 白名单:**只有挂了标记且 can_pickup 为 true 的才拖得动**。
## true = 黑名单:没挂标记的默认可拖,只有显式设成 false 的不能拖。
@export var allow_unmarked: bool = false
## 交互状态变化时发出。
## hovering = 准心正指着可拖拽的刚体(还没抓);holding = 正抓着东西。
@@ -78,6 +85,41 @@ func physics_step() -> void:
_current.set_linear_velocity(desired)
## 这个碰撞体现在能不能被拖拽。
## 硬条件:必须是 RigidBody3D(只有刚体才谈得上被拖)。
## 软条件:它身上的 PickupItem 标记允许 —— 没挂标记时怎么办由 allow_unmarked 决定。
func can_grab(collider: Object) -> bool:
if not (collider is RigidBody3D):
return false
var marker := _find_item_marker(collider as Node)
if marker == null:
return allow_unmarked
return marker.can_pickup
## 找挂在物体上的 PickupItem 标记,顺序:刚体自身 → 它的直接子节点 → owner 链向上。
##
## **为什么必须向上找**:像 SM_Bottle_1.glb 这类资源,实例化后是这个结构 ——
## SM_Bottle_12 (Node3D) ← 脚本通常挂在这一层(实例的根)
## └── SM_Bottle_1 (RigidBody3D) ← 但射线打中的是这一层
## 而 glb 内部节点的 owner 恰好指向实例根,所以沿 owner 链一步就能找到。
##
## 向上时**只检查节点自身、不检查它们的子节点** —— 否则一旦往上走到 World 那一层,
## 就会去遍历几百个子节点,而这是每物理帧都要跑一次的。
func _find_item_marker(body: Node) -> PickupItem:
if body is PickupItem:
return body as PickupItem
for child in body.get_children():
if child is PickupItem:
return child as PickupItem
var container: Node = body.owner
while container != null:
if container is PickupItem:
return container as PickupItem
container = container.owner
return null
## 检测「指着可拖拽物 / 正抓着东西」这两个状态,**只在变化时**发信号。
## 每帧都发会让 UI 无谓重绘,所以这里自己记着上一次的结果。
func _update_interact_state() -> void:
@@ -86,7 +128,7 @@ func _update_interact_state() -> void:
# 主动刷新一次:RayCast3D 的结果通常滞后于本帧的 move_and_slide,
# 不强制更新的话,跑动中瞄到物体要慢一帧才亮。
_ray.force_raycast_update()
var hovering := _ray.get_collider() is RigidBody3D
var hovering := can_grab(_ray.get_collider())
var holding := is_instance_valid(_current)
if hovering == _hovering and holding == _holding:
return
@@ -99,14 +141,17 @@ 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)
# 再判一次「能不能抓」,而不是直接信 hovering 状态 ——
# 悬停是上一物理帧算出来的,这中间 can_pickup 可能已经被改掉。
if not can_grab(collider):
return
_current = collider as RigidBody3D
# 让被拖的物体和玩家本体**互不碰撞**。
# 否则物体一旦挨到身体就会被物理推出,而拖拽又立刻把它拉回来 ——
# 两个解算器互相打架,表现就是持续的高频抖动。
# 只解除「物体 ↔ 玩家」这一对,物体和场景里其他东西照样正常碰撞。
if _body != null:
_current.add_collision_exception_with(_body)
func _drop() -> void: