可拖拽判断(PickupItem 标记)
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
# 2026-10-12
|
||||||
|
|
||||||
|
## 可拖拽判断(PickupItem 标记)
|
||||||
|
|
||||||
|
**需求**:`scripts/item.gd` 提供 `can_pickup`,只有为 true 时才能拖拽。
|
||||||
|
|
||||||
|
**item.gd 本次改动**:加了 `class_name PickupItem` 和说明注释,功能不变。
|
||||||
|
```gdscript
|
||||||
|
class_name PickupItem
|
||||||
|
extends Node
|
||||||
|
@export var can_pickup: bool = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠ 关键发现:脚本挂载位置 ≠ 射线打中的节点
|
||||||
|
|
||||||
|
用户在 `world/main.tscn` 第 155 行把 item.gd 挂在了 `SM_Bottle_12` 上,但那个节点**不是刚体**。
|
||||||
|
实例来源 `res://assets/Furniture_FREE-af040129/SM_Bottle_1.glb`,实际结构是:
|
||||||
|
|
||||||
|
```
|
||||||
|
SM_Bottle_12 (Node3D) ← item.gd 挂在这一层(glb 实例的根)
|
||||||
|
└── SM_Bottle_1 (RigidBody3D) ← 射线实际打中的是这一层
|
||||||
|
├── SM_Bottle_1 (MeshInstance3D)
|
||||||
|
└── CollisionShape3D
|
||||||
|
```
|
||||||
|
|
||||||
|
**实测确认**:glb 内部节点的 `owner` 指向实例根(`SM_Bottle_1.owner == SM_Bottle_12`),
|
||||||
|
再往上为 null。所以「沿 owner 链向上找标记」正好一步命中。
|
||||||
|
|
||||||
|
### 实现(`player_pickup.gd`)
|
||||||
|
|
||||||
|
- 新增 `@export var allow_unmarked: bool = false`
|
||||||
|
- **false(默认)= 白名单**:只有挂了 `PickupItem` 且 `can_pickup=true` 的才拖得动
|
||||||
|
- true = 黑名单:没挂标记的默认可拖,只有显式设 false 的不能拖
|
||||||
|
- 新增 `can_grab(collider) -> bool`:先判 `is RigidBody3D`,再查标记
|
||||||
|
- 新增 `_find_item_marker(body)`:顺序 **刚体自身 → 直接子节点 → owner 链向上**
|
||||||
|
- 向上时**只查节点自身、不查子节点** —— 否则走到 World 层会遍历几百个子节点,而这是每物理帧都要跑的
|
||||||
|
- `_update_interact_state()` 的 hovering 判定改用 `can_grab()`
|
||||||
|
- `_grab()` 里再判一次(悬停状态来自上一物理帧,其间 can_pickup 可能已被改掉)
|
||||||
|
|
||||||
|
### 实测验证(headless,全部符合预期)
|
||||||
|
```
|
||||||
|
can_grab(SM_Bottle_1 RigidBody3D) = true ✓
|
||||||
|
can_grab(SM_Bottle_12 Node3D) = false ✓(不是刚体)
|
||||||
|
can_grab(Can2 内部刚体) = false ✓(严格/白名单模式)
|
||||||
|
allow_unmarked=true 后 Can2 = true ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠ 副作用(需告知用户)
|
||||||
|
默认白名单模式下,场景里**所有没挂 item.gd 的刚体都拖不动了**
|
||||||
|
(`Can2`、饭盒、水果等)。要么逐个挂标记,要么把 `allow_unmarked` 勾上。
|
||||||
|
|
||||||
|
### MCP 备注
|
||||||
|
4 次 `script_patch` 均返回 `replacements: 1` 但伴随 `gdscript_reload_failed (code 43)`
|
||||||
|
假报错(`fallback_line: true`)—— 与 10-10 记录的是同一现象。
|
||||||
|
离线 `--check-only` 两个脚本均通过;`PickupItem` 已进 `global_script_class_cache.cfg`。
|
||||||
@@ -5,6 +5,9 @@ extends Node
|
|||||||
## 松开左键即原地放开,按 Q 则沿视线方向把手上的物体甩出去。
|
## 松开左键即原地放开,按 Q 则沿视线方向把手上的物体甩出去。
|
||||||
## 只管「抓 / 拖 / 放 / 甩」,不碰移动与视角。
|
## 只管「抓 / 拖 / 放 / 甩」,不碰移动与视角。
|
||||||
##
|
##
|
||||||
|
## **能否被拖拽由 PickupItem 标记决定**(scripts/item.gd):
|
||||||
|
## 只有挂了该脚本、且 can_pickup 为 true 的刚体才抓得起来。判定见 can_grab()。
|
||||||
|
##
|
||||||
## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 注入射线、持握点与玩家本体。
|
## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 注入射线、持握点与玩家本体。
|
||||||
|
|
||||||
## 拖拽力度:物体被拉向持握点的速度系数。越大跟手越紧,但过大容易来回抖。
|
## 拖拽力度:物体被拉向持握点的速度系数。越大跟手越紧,但过大容易来回抖。
|
||||||
@@ -14,6 +17,10 @@ extends Node
|
|||||||
@export var throw_speed: float = 12.0
|
@export var throw_speed: float = 12.0
|
||||||
## 拖拽速度上限(米/秒)。防止物体离得远时被一股脑拽飞。
|
## 拖拽速度上限(米/秒)。防止物体离得远时被一股脑拽飞。
|
||||||
@export var max_drag_speed: float = 30.0
|
@export var max_drag_speed: float = 30.0
|
||||||
|
## 没挂 PickupItem 标记的刚体,是否也允许拖拽。
|
||||||
|
## false(默认)= 白名单:**只有挂了标记且 can_pickup 为 true 的才拖得动**。
|
||||||
|
## true = 黑名单:没挂标记的默认可拖,只有显式设成 false 的不能拖。
|
||||||
|
@export var allow_unmarked: bool = false
|
||||||
|
|
||||||
## 交互状态变化时发出。
|
## 交互状态变化时发出。
|
||||||
## hovering = 准心正指着可拖拽的刚体(还没抓);holding = 正抓着东西。
|
## hovering = 准心正指着可拖拽的刚体(还没抓);holding = 正抓着东西。
|
||||||
@@ -78,6 +85,41 @@ func physics_step() -> void:
|
|||||||
_current.set_linear_velocity(desired)
|
_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 无谓重绘,所以这里自己记着上一次的结果。
|
## 每帧都发会让 UI 无谓重绘,所以这里自己记着上一次的结果。
|
||||||
func _update_interact_state() -> void:
|
func _update_interact_state() -> void:
|
||||||
@@ -86,7 +128,7 @@ func _update_interact_state() -> void:
|
|||||||
# 主动刷新一次:RayCast3D 的结果通常滞后于本帧的 move_and_slide,
|
# 主动刷新一次:RayCast3D 的结果通常滞后于本帧的 move_and_slide,
|
||||||
# 不强制更新的话,跑动中瞄到物体要慢一帧才亮。
|
# 不强制更新的话,跑动中瞄到物体要慢一帧才亮。
|
||||||
_ray.force_raycast_update()
|
_ray.force_raycast_update()
|
||||||
var hovering := _ray.get_collider() is RigidBody3D
|
var hovering := can_grab(_ray.get_collider())
|
||||||
var holding := is_instance_valid(_current)
|
var holding := is_instance_valid(_current)
|
||||||
if hovering == _hovering and holding == _holding:
|
if hovering == _hovering and holding == _holding:
|
||||||
return
|
return
|
||||||
@@ -99,14 +141,17 @@ func _grab() -> void:
|
|||||||
if _ray == null or is_instance_valid(_current):
|
if _ray == null or is_instance_valid(_current):
|
||||||
return
|
return
|
||||||
var collider := _ray.get_collider()
|
var collider := _ray.get_collider()
|
||||||
if collider is RigidBody3D:
|
# 再判一次「能不能抓」,而不是直接信 hovering 状态 ——
|
||||||
_current = collider as RigidBody3D
|
# 悬停是上一物理帧算出来的,这中间 can_pickup 可能已经被改掉。
|
||||||
# 让被拖的物体和玩家本体**互不碰撞**。
|
if not can_grab(collider):
|
||||||
# 否则物体一旦挨到身体就会被物理推出,而拖拽又立刻把它拉回来 ——
|
return
|
||||||
# 两个解算器互相打架,表现就是持续的高频抖动。
|
_current = collider as RigidBody3D
|
||||||
# 只解除「物体 ↔ 玩家」这一对,物体和场景里其他东西照样正常碰撞。
|
# 让被拖的物体和玩家本体**互不碰撞**。
|
||||||
if _body != null:
|
# 否则物体一旦挨到身体就会被物理推出,而拖拽又立刻把它拉回来 ——
|
||||||
_current.add_collision_exception_with(_body)
|
# 两个解算器互相打架,表现就是持续的高频抖动。
|
||||||
|
# 只解除「物体 ↔ 玩家」这一对,物体和场景里其他东西照样正常碰撞。
|
||||||
|
if _body != null:
|
||||||
|
_current.add_collision_exception_with(_body)
|
||||||
|
|
||||||
|
|
||||||
func _drop() -> void:
|
func _drop() -> void:
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ project/assembly_name="agame"
|
|||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/mesh_collider/plugin.cfg", "res://addons/sky_3d/plugin.cfg")
|
enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/sky_3d/plugin.cfg")
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class_name PickupItem
|
||||||
|
extends Node
|
||||||
|
## 可拖拽物标记:挂在你想让玩家能拖动的物体上。
|
||||||
|
##
|
||||||
|
## 挂载位置很自由 —— 拾取组件按「刚体自身 → 它的直接子节点 → owner 链向上」的顺序找,
|
||||||
|
## 所以下面两种挂法都行:
|
||||||
|
##
|
||||||
|
## SM_Bottle_12 (Node3D) ← 挂在实例的根节点上(最常见)
|
||||||
|
## └── SM_Bottle_1 (RigidBody3D) ← 射线打中的是这一层,靠 owner 链向上找到标记
|
||||||
|
##
|
||||||
|
## SM_Bottle_1 (RigidBody3D) ← 直接挂在刚体本身上
|
||||||
|
## └── CollisionShape3D
|
||||||
|
|
||||||
|
## 是否允许被玩家拖拽。
|
||||||
|
## false 时:准心指着它不会变成手型,按下左键也抓不起来。
|
||||||
|
## 适合做「暂时不能拿」的物体 —— 比如需要先完成某个条件才解锁。
|
||||||
|
@export var can_pickup: bool = true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b80cx1d3r8w8r
|
||||||
+16
-19
@@ -12,11 +12,10 @@
|
|||||||
[ext_resource type="Texture2D" uid="uid://c1vwcdcdvb74a" path="res://addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg" id="11_nikgi"]
|
[ext_resource type="Texture2D" uid="uid://c1vwcdcdvb74a" path="res://addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg" id="11_nikgi"]
|
||||||
[ext_resource type="Script" uid="uid://deo0gxkmk2ahd" path="res://addons/sky_3d/src/Sky3D.gd" id="12_d6btn"]
|
[ext_resource type="Script" uid="uid://deo0gxkmk2ahd" path="res://addons/sky_3d/src/Sky3D.gd" id="12_d6btn"]
|
||||||
[ext_resource type="Script" uid="uid://b4tkplvh7fq40" path="res://addons/sky_3d/src/SkyDome.gd" id="13_wc2es"]
|
[ext_resource type="Script" uid="uid://b4tkplvh7fq40" path="res://addons/sky_3d/src/SkyDome.gd" id="13_wc2es"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dh5ej8olhdv5x" path="res://assets/Furniture_FREE-af040129/SM_Bottle_1.glb" id="14_60vd7"]
|
||||||
[ext_resource type="Script" uid="uid://cjmkcj8hwm7ts" path="res://addons/sky_3d/src/TimeOfDay.gd" id="14_gk3u8"]
|
[ext_resource type="Script" uid="uid://cjmkcj8hwm7ts" path="res://addons/sky_3d/src/TimeOfDay.gd" id="14_gk3u8"]
|
||||||
|
[ext_resource type="Script" uid="uid://b80cx1d3r8w8r" path="res://scripts/item.gd" id="15_60vd7"]
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_cmo1i"]
|
[ext_resource type="PackedScene" uid="uid://j3cjykus603q" path="res://assets/Food Pack - Low Poly Assets/Can.glb" id="16_e63cu"]
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_i1eo7"]
|
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_sgblq"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_sgblq"]
|
||||||
shader = ExtResource("5_8q2wj")
|
shader = ExtResource("5_8q2wj")
|
||||||
@@ -71,8 +70,8 @@ shader_parameter/cirrus_absorption = 2.0
|
|||||||
shader_parameter/cirrus_sky_tint_fade = 0.5
|
shader_parameter/cirrus_sky_tint_fade = 0.5
|
||||||
shader_parameter/cirrus_size = 1.0
|
shader_parameter/cirrus_size = 1.0
|
||||||
shader_parameter/cirrus_uv = Vector2(0.16, 0.11)
|
shader_parameter/cirrus_uv = Vector2(0.16, 0.11)
|
||||||
shader_parameter/cirrus_position1 = Vector2(0.043840162, 0.043840162)
|
shader_parameter/cirrus_position1 = Vector2(0.7657499, 0.7657499)
|
||||||
shader_parameter/cirrus_position2 = Vector2(0.043840162, 0.043840162)
|
shader_parameter/cirrus_position2 = Vector2(0.7657499, 0.7657499)
|
||||||
shader_parameter/cirrus_texture = ExtResource("6_60vd7")
|
shader_parameter/cirrus_texture = ExtResource("6_60vd7")
|
||||||
shader_parameter/cumulus_visible = true
|
shader_parameter/cumulus_visible = true
|
||||||
shader_parameter/cumulus_intensity = 0.6
|
shader_parameter/cumulus_intensity = 0.6
|
||||||
@@ -82,7 +81,7 @@ shader_parameter/cumulus_absorption = 2.0
|
|||||||
shader_parameter/cumulus_noise_freq = 2.7
|
shader_parameter/cumulus_noise_freq = 2.7
|
||||||
shader_parameter/cumulus_sky_tint_fade = 0.0
|
shader_parameter/cumulus_sky_tint_fade = 0.0
|
||||||
shader_parameter/cumulus_size = 0.5
|
shader_parameter/cumulus_size = 0.5
|
||||||
shader_parameter/cumulus_position = Vector2(0.2192056, 0.2192056)
|
shader_parameter/cumulus_position = Vector2(3.8291218, 3.8291218)
|
||||||
shader_parameter/cumulus_texture = ExtResource("7_e63cu")
|
shader_parameter/cumulus_texture = ExtResource("7_e63cu")
|
||||||
shader_parameter/cumulus_partial_mie_phase = Vector3(0.957564, 1.042436, 0.412)
|
shader_parameter/cumulus_partial_mie_phase = Vector3(0.957564, 1.042436, 0.412)
|
||||||
shader_parameter/cumulus_mie_intensity = 1.0
|
shader_parameter/cumulus_mie_intensity = 1.0
|
||||||
@@ -115,18 +114,6 @@ player_scene = ExtResource("2_4f7fj")
|
|||||||
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=1020023513]
|
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=1020023513]
|
||||||
spawn_path = NodePath("..")
|
spawn_path = NodePath("..")
|
||||||
|
|
||||||
[node name="World" parent="." unique_id=332495265 instance=ExtResource("7_gu74f")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -3.970745, 0)
|
|
||||||
|
|
||||||
[node name="RigidBody3D" type="RigidBody3D" parent="." unique_id=1008810927]
|
|
||||||
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="RigidBody3D" unique_id=1173685673]
|
|
||||||
shape = SubResource("BoxShape3D_cmo1i")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="RigidBody3D" unique_id=751871190]
|
|
||||||
mesh = SubResource("BoxMesh_i1eo7")
|
|
||||||
|
|
||||||
[node name="Sky3D" type="WorldEnvironment" parent="." unique_id=1566118792]
|
[node name="Sky3D" type="WorldEnvironment" parent="." unique_id=1566118792]
|
||||||
environment = SubResource("Environment_8l5dh")
|
environment = SubResource("Environment_8l5dh")
|
||||||
camera_attributes = SubResource("CameraAttributesPractical_6lvsd")
|
camera_attributes = SubResource("CameraAttributesPractical_6lvsd")
|
||||||
@@ -161,3 +148,13 @@ game_time_enabled = false
|
|||||||
dome_path = NodePath("../SkyDome")
|
dome_path = NodePath("../SkyDome")
|
||||||
minutes_per_day = 0.0
|
minutes_per_day = 0.0
|
||||||
current_time = 8.64
|
current_time = 8.64
|
||||||
|
|
||||||
|
[node name="World" parent="." unique_id=332495265 instance=ExtResource("7_gu74f")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -4.8994102, 0)
|
||||||
|
|
||||||
|
[node name="SM_Bottle_12" parent="." unique_id=98618243 instance=ExtResource("14_60vd7")]
|
||||||
|
transform = Transform3D(0.475, 0, 0, 0, 0.475, 0, 0, 0, 0.475, 0, 0, 0)
|
||||||
|
script = ExtResource("15_60vd7")
|
||||||
|
|
||||||
|
[node name="Can2" parent="." unique_id=907661218 instance=ExtResource("16_e63cu")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3182813, -1.2704358, 0.5117185)
|
||||||
|
|||||||
Reference in New Issue
Block a user