# 2026-10-09 ## 简易准心(Crosshair) - 新增 `ui/crosshair.gd`:`@tool extends Control`,用 `_draw()` 画十字(gap/length/thickness/dot 可导出调参),`mouse_filter = IGNORE`,监听 `NOTIFICATION_RESIZED` 重绘。 - 挂载约定:`CanvasLayer > Crosshair(Control)`,Control 锚点为 Full Rect,中心即屏幕中心;推荐挂在 `CameraRig` 之下 —— 因为 `player.gd _ready()` 对非 authority 会 `camera_rig.queue_free()`,远端玩家准心随之一并释放,无需额外代码。 - 项目 stretch 模式为 `canvas_items` + `expand`,CanvasLayer 会自动跟随缩放。 ### 后续改动:只保留「点」 - 最终形态:准心 = 屏幕中心一个点,无十字、无样式开关(`enum Style` 已移除)。 - 导出参数:`dot_color` / `dot_radius`(默认 2.5) / `outline` / `outline_color` / `outline_width`。 - `_draw()` 先画描边圆再画主体圆,保证浅背景可见。 ## player.gd 拆解方案(待执行) - 现状:`character/player/player.gd` 155 行,混了视角 / 移动跳跃 / 拾取 / 多人权限四类职责。 - 方案:**子节点组件化**(与既有 `PlayerAnimator` 同模式)。根节点降为协调器(~40 行),新增 `Look` / `Motor` / `Pickup` 三个 Node 子节点,各自挂 `player_look.gd` / `player_motor.gd` / `player_pickup.gd`。 - 三条硬约束:① `move_and_slide()` 留在根节点,Motor 只写 `body.velocity`;② 物理顺序由根节点显式编排(motor.step → move_and_slide → animator → pickup),不让子节点各写 `_physics_process`;③ 非 authority 时三个组件的输入/物理必须一并关闭。 - 范式定位:组合优于继承 + 组件模式(Component Pattern);协调器部分带 Facade/Mediator 色彩;`setup()` 注入属手工依赖注入。**不是 ECS**(Godot 节点是「带行为的对象」,非纯数据组件)。 - 状态:**已落地**(2026-10-09)。新增 `player_look.gd` / `player_motor.gd` / `player_pickup.gd`(均带 `class_name`:PlayerLook / PlayerMotor / PlayerPickup);`player.gd` 重写为协调器(~65 行);`player.tscn` 追加 Look / Motor / Pickup 三个 Node 子节点(挂 FaceRig 之后,ext_resource id 13_look / 14_motor / 15_pickup,未写 uid 由 Godot 补)。 - 场景格式注意:本项目的 .tscn 节点带非标准 `unique_id=`(Godot 4.7 特性),`unique_id` 与 `load_steps` 均可省略;`paint_panel.tscn` 就完全没写,可作参照。 - 已确认无外部脚本引用被搬走的属性(仅 `world/main.tscn` 通过场景 uid 引用玩家场景)。 - 顺手去掉了原 `_pickObject` 里的两行 `print()` 调试输出。 ### 拾取改为长按拖拽 - `player_pickup.gd`:由「按一下切换」改为「按下抓起、松开放下」——`is_action_pressed("carry")` → `_grab()`,`is_action_released("carry")` → `_drop()`。 - `carry` 动作确认绑定鼠标左键(`button_index: 1`);项目里另有未被任何脚本使用的 `throw_carry`(绑 Q 键),可作投掷功能的预留。 - 加了 `Input.mouse_mode != MOUSE_MODE_CAPTURED` 早退,并在 `player.gd` 重新锁定鼠标那一支补 `get_viewport().set_input_as_handled()`,避免「点一下重新锁定鼠标」顺带抓起物体(两种分支执行顺序下都成立)。 - `physics_step()` 与 `has_object()` 改用 `is_instance_valid()` —— 防止拖拽途中物体被 destroy 时取属性报错。 - `_drop()` 保留线速度(松手有惯性 / 可甩出);想要「就地放下」需补 `set_linear_velocity(Vector3.ZERO)`。 ### 按 Q 投掷 - `player_pickup.gd` 接入 `throw_carry`(Q 键)→ `_throw()`:取 `-_hold_pos.global_transform.basis.z` 作投掷方向(持握点挂在摄像机下且仅有平移,其 basis 即视线朝向,故无需再注入摄像机),`set_linear_velocity(dir * throw_speed)` 后置 `_current = null`。 - 新增导出参数 `throw_speed: float = 12.0`。 - 备选手感(注释里留了行):改用 `apply_central_impulse` 则重物飞得慢。 ### 可拖拽时准心变手型 - 新增素材 `character/assets/hand_cursor.svg`(新建了 `character/assets/` 目录):简笔手(4 指 + 手掌 + 拇指的圆角矩形),双层 —— 底层深色 `#1a1a1a` stroke-width 4 当描边,上层白色 stroke-width 1.5 覆盖内部接缝。32×32,**未写 ``**(避免 ThorVG 解析风险)。 - `ui/crosshair.gd`:加 `class_name Crosshair`;新增 `hand_texture`(自动加载上面的 svg) / `hand_size`(=26)、`set_grabbable(bool)`;`_draw()` 在 `_grabbable && hand_texture != null` 时走 `_draw_hand()`,否则走 `_draw_dot()`。 - `player_pickup.gd`:新增 `signal hover_changed(grabbable: bool)`,`physics_step()` 里先调 `_update_hover()`——用 `_ray.force_raycast_update()` 强制刷新(否则结果滞后于本帧 move_and_slide,跑动中瞄到物体慢一帧),**只在状态变化时**发信号。 - `player.gd`:`@onready var crosshair: Crosshair = $CameraRig/HUD/Crosshair`;`_ready()` 权威分支里 `pickup.hover_changed.connect(_on_pickup_hover_changed)`;handler 里既切准心,也 `Input.set_custom_mouse_cursor()` 换系统指针(含指尖热点),鼠标未捕获时该行无副作用。 - **注意**:用户侧已把 `AnimationPlayer` 从 `Body/AnimationPlayer` 提到 Player 根节点(`$AnimationPlayer`),并把 `animator` 变量改名为 `animation_player`;相机 scale 改为 5。 ### 换用真实光标素材(grab / grab_hover) - 素材来源:`C:\Users\yvmou\Downloads\cursors\light\`(light 主题 = 深色手型,32×32 RGBA)。 - 已复制到 `character/assets/grab.png`(指着可拖拽物)与 `character/assets/grab_hover.png`(正抓着),Godot 已生成 `.import`。 - **删除了临时的 `character/assets/hand_cursor.svg` 及其 `.import`**(已被真实素材取代)。 - `ui/crosshair.gd` 改为三态:`enum GrabState { NONE, HOVER, HOLDING }`;导出 `hover_texture` / `hold_texture` / `hand_size`(=32,1:1 不缩放) / `icon_modulate` / `sync_os_cursor`;公开 `texture_for_state(state)` 供系统指针复用素材;`_sync_os_cursor()` 内部完成 `Input.set_custom_mouse_cursor`(`Engine.is_editor_hint()` 下跳过),热点在图标高度 12% 处 —— **系统指针逻辑已从 player.gd 移入准心**。 - `player_pickup.gd`:信号由 `hover_changed(bool)` 改为 `interact_state_changed(hovering, holding)`,`_update_hover()` 改名 `_update_interact_state()` 并同时跟踪 `_holding`。 - `player.gd`:handler 改名 `_on_pickup_interact_state_changed(false)`,只负责把两个布尔量收敛成 `Crosshair.GrabState`(优先级 抓着 > 指着 > 没有)。 - 待用户确认:像素手在非整数倍分辨率缩放下发糊的话,需把 Crosshair 的 `texture_filter` 设为 Nearest。 ### 拖拽时物体一卡一卡(已修) - **根因(几何 + 控制律)**:`physics_step()` 用 `v = to_hold * pick_power` 是一阶跟随,人匀速前进时物体必有稳态滞后,滞后距离 = 移动速度 / pick_power。当时为 `walk_speed 5 ÷ pick_power 10 = 0.5 m`,而持握点离身体仅 `0.44 m`(InteractionPos local z = -0.4439,Player 缩放 0.2 × Camera 缩放 5 = 净 1.0,故世界距离就是 0.444 m)→ 物体退进玩家胶囊体 → 物理推出 / 拖拽拉回互相打架 → 高频抖动。 - **修复 1(前馈)**:`desired = to_hold * pick_power + _body.velocity` —— 匀速行走时滞后归零。这是根治。 - **修复 2(碰撞例外)**:`_grab()` 里 `_current.add_collision_exception_with(_body)`,`_drop()` / `_throw()` 经 `_forget()` 用 `remove_collision_exception_with` 解除。只解除「物体 ↔ 玩家」这一对,物体与场景其他碰撞体照常交互。 - 另加 `@export var max_drag_speed: float = 30.0` 速度上限,物体离得远时不会被一股脑拽飞。 - **接口变更**:`PlayerPickup.setup(ray, hold_pos, body)` 多了第三个参数(玩家本体),`player.gd` 里传 `self`。`_grab()` 加了 `is_instance_valid(_current)` 早退,避免重复抓取时漏掉碰撞例外。 ### 玩家缩放层级(查明,供后续改尺寸参考) - `character/player/player.tscn` 的缩放链:`Player ×0.2` → 子节点 Collision(胶囊 r0.545/h1.406,本地 y0.703) / CameraRig(眼高 y1.6) / Body/mouse(×0.1824) 累计 ×0.2。 - `Camera ×5` **是补偿值**,用来把相机子树抵消回世界缩放 1.0(0.2×5=1.0),因此 InteractionRay 世界长度 0.5 m、InteractionPos 世界距离 0.444 m。 - 推论:**改 `Player.scale` 必须同步 `Camera.scale = 1 / Player.scale`**,否则交互距离/持握距离会跟着缩。另外 PlayerMotor 的 walk/sprint/jump/accl 是世界单位,不会自动缩放,改尺寸时需手动同倍调整。 - 世界尺寸现值:碰撞胶囊半径 0.109 / 高 0.281,眼高 0.32 m。 ### 整体人物缩到 0.6 倍(已实施) - `player.tscn`:`Player` 缩放 0.2 → **0.12**;`Camera` 缩放 5 → **8.333333**(= 1/0.12,保持相机子树净缩放 1.0,**交互距离/持握距离不变**)。Camera 的位置偏移与 CameraRig 的 y=1.6 都自动随父级缩放,未改。 - `player_motor.gd`:walk 5.0→**3.0**、sprint 8.5→**5.1**、acceleration 45→**27**、air_acceleration 12→**7.2**,均为 ×0.6;jump_velocity 5.0→**3.87**(×√0.6,因跳跃高度 = v²/2g,直接乘 0.6 会跳得偏矮)。 - `player_animator.gd`:**基准速度也必须 ×0.6** —— walk 5.0→3.0、run 8.5→5.1、crouch 2.5→1.5、sneak 1.6→0.96。原因:模型缩到 0.6 倍后,同一片段一个循环跨出的世界距离也缩到 0.6 倍,不改基准速度脚步会明显打滑。同时修正了 `_resolve_state()` 里那句「jump 片段 0.5s ≈ 5/9.8 上升时间」的过时注释。 - 未改动:`pick_power` 10(新滞后 3.0/10 = 0.3 m < 0.444 m 持握距离,且已有速度前馈,安全)、`throw_speed` 12(被扔的物体本身没缩,投掷距离不该变)、`mouse_sensitivity`(纯角度)。 - 缩放后世界尺寸:胶囊半径 0.0654 / 高 0.169,眼高 0.192 m;跳跃绝对高度 1.276 → 0.766 m(相对身高观感不变)。 ### 再缩到「视觉高度 0.15 m」(已实施) - **口径确认(重要)**:用户选的是**视觉模型高度**,不是碰撞胶囊。两者不等: - 模型:glb 顶点原始高 `8.557729`(min y -4.7746 / max y 3.7832)× mouse 节点缩放 `0.18240134` = 本地 1.560941;世界 = ×0.12 = **0.187313 m**(当时) - 胶囊:本地 1.4063476 × 0.12 = **0.1687617 m** - 模型底部在本地 y ≈ -0.0007(脚踩原点,与胶囊底部对齐),故测量可信。模型比胶囊高 11%(多出耳朵/尾巴)。 - 缩放系数 `k = 0.15 / 0.187313 = 0.800799`。 - `player.tscn`:`Player.scale` 0.12 → **0.096096**;`Camera.scale` 8.333333 → **10.406274**(= 1/0.096096)。 - `player_motor.gd`:walk 3.0→**2.4024**、sprint 5.1→**4.0841**、accel 27→**21.6216**、air_accel 7.2→**5.7658**、jump 3.87→**3.4632**(×√k)。 - `player_animator.gd`:walk_ref 3.0→**2.4024**、run_ref 5.1→**4.0841**、crouch_ref 1.5→**1.2012**、sneak_ref 0.96→**0.7688**。 - 注释已改成「按同一系数同步缩放」的通用表述,不再写死 0.6,避免下次再失效。 - 结果:胶囊世界 半径 0.05241 / 高 0.13514,眼高 0.08729 m,跳跃绝对高度 ≈ 0.612 m;拖拽滞后 = 2.4024/10 = 0.24 m < 0.444 m 持握距离,安全。 - **可复用公式**:`Player.scale = 目标世界高 / (模型本地总高)`,`Camera.scale = 1 / Player.scale`,速度与动画基准速度均 ×k(jump 用 ×√k)。 ### 按 0.15 m 身高重新设计移动参数(已实施) - 用户明确:「不用管之前,按 0.15 的身高重新设计」→ 采取**以人类为参照的等比设计**,不再依赖旧基准。 - 设计基准:人 1.75 m,游戏惯例 walk 5.0 / sprint 8.5 / accel 45 / air_accel 12 / jump 5.0(跳跃高度 1.28 m ≈ 0.73 身高)。系数 `k = 0.15 / 1.75 ≈ 0.0857143`。 - `player_motor.gd`:walk 2.4024→**0.4286**、sprint 4.0841→**0.7286**、accel 21.6216→**3.8571**、air_accel 5.7658→**1.0286**、jump 3.4632→**1.4639**(×√k)。 - `player_animator.gd`:walk_ref 2.4024→**0.4286**、run_ref 4.0841→**0.7286**、crouch_ref 1.2012→**0.2143**、sneak_ref 0.7688→**0.1371**。 - 校验:两边都是「走路 = 2.86 身长/秒」「跳跃 = 0.73 身高」,比例一致。加速度给出约 0.11 秒加到全速;跳跃高度 0.109 m。 - 拖拽滞后 = 0.4286/10 = 0.043 m ≪ 0.444 m 持握距离,安全。 - **未改动**(有意保持):交互距离/持握距离(射线 0.5 m、持握 0.444 m —— 若按 k 缩到 0.038 m 会够不到任何东西)、`pick_power` 10、`throw_speed` 12(被扔物体是世界尺寸,未缩)、`mouse_sensitivity`(纯角度)。 - **世界尺度查不到可靠线索**:`world/main2.tscn` 有 295 个 `Transform3D`,但混入了嵌套实例的局部变换,位置跨度(X33/Z57)不可信;198 个实例缩放为 1.0,资产疑似米制。若实际关卡跨度大、用户觉得慢,把 walk/sprint 等线性量乘同一倍数即可。 ### 蹲伏真正生效(已实施) - 背景:**蹲下改速度、改摄像机高度这两件事从来没实现过**(原 `player.gd` 也只把 `crouching` 传给动画器切 CROUCH/SNEAK 片段),不是回归。 - `player_motor.gd`:新增 `@export var crouch_speed: float = 0.2143`(= walk 的一半 = 2.5×k)。`step()` 里改为**蹲伏优先于疾跑**:`sprinting = wants_sprint and not crouching`,速度按 `crouch_speed > sprint_speed` 优先级取。 - `player_look.gd`:新增蹲伏组 `crouch_height_ratio`(=0.6) / `crouch_transition_speed`(=10);`setup()` 从场景读 `_stand_height = rig.position.y`(不写死 1.6);新增 `set_crouching(bool)` 设目标高度;`_process()` 用 `1 - exp(-k·delta)` 指数收敛平滑升降(**渲染帧**,物理帧会一顿一顿);`_ready()` 里 `set_process(false)`,`setup()` 才开 —— 远端玩家不调 setup 就不会跑。 - `player.gd`:`_physics_process` 里 `motor.step(delta)` 之后加 `look.set_crouching(motor.crouching)` —— **蹲伏状态由 Motor 单点判定**,不让 Look 自己读蹲键,避免两处各判一次不同步。 - `player_animator.gd`:`sneak_ref_speed` 0.1371 → **0.2143**,与 `crouch_speed` 对齐 —— 蹲着走播的就是 SNEAK 片段,两边不等会打滑(原来会以 1.56× 播放,逼近 1.6 的 clamp 上限)。注释里写明了这条配对关系。 - 世界高度:站立 `1.6 × 0.096096 = 0.1538 m` → 蹲下 `0.0923 m`。 - **未做(可以后续补)**:蹲下时没有缩小碰撞胶囊 —— 所以现在还钻不过低矮缝隙。要做需要额外处理「头顶有障碍时不许起身」的检测。 ### 关于「重力是否该随角色缩小」(已分析,待用户定夺) - **物理结论**:不该变。重力加速度是行星属性,与物体大小无关。 - **但设计上是真问题**:我们的速度按 k=0.0857 等比缩了,重力却没缩,导致**落体相对过快**。 - 落「自身身高」用时:人类 (1.75 m, g=9.8) = 0.598 s;本角色 (0.15 m, g=9.8) = 0.175 s,**快 3.4 倍**。 - 若 `g = 9.8 × k = 0.84`:本角色也是 0.598 s,与人类完全一致(缩小版人类)。 - **顺带暴露的问题**:`jump` 片段长 0.5 s。现在 g=9.8、jump_velocity=1.4639 → 上升仅 **0.149 s**,只播得完片段的 30%。若 `g=0.84` 且 `jump_velocity = walk × 1.0 = 0.4286`,上升 = **0.51 s**,正好等于片段长度 —— 回到原始设计(原来 jump 5.0 / g 9.8 = 0.51 s)里那种吻合。 - **实现方式(已落地为旋钮)**:`PlayerMotor` 新增 `@export_range(0.0, 2.0, 0.01) var gravity_scale: float = 1.0`,`step()` 里 `_body.velocity.y -= _gravity * gravity_scale * delta`(**每帧现读**,方便运行中拖 Inspector 试手感)。默认 1.0 = 行为完全不变。**绝不要改 `ProjectSettings physics/3d/default_gravity`** —— 那是全局的,会让场景里所有 RigidBody 一起变飘,包括拖拽/投掷的物体。 - 三个候选:`9.8`(物理真实,落体快、跳跃动画对不上)/ `2.87`(×√k,折中)/ `0.84`(×k,缩小版人类,跳跃动画吻合,但比月球 1.62 还飘)。 - **⚠ 配对规律**:重力 × s 时 `jump_velocity` 也要 × s,否则跳跃高度按 1/s 暴涨。现在是 `g=9.8 + v=1.4639 → 高 0.109 m`;若 `g=0.84` 而 v 不变,跳跃高会变成 **1.28 m(8.5 个身高)**;正确配对是 `g=0.84 + v=0.4286`。这条已写进 `gravity_scale` 的代码注释。 - 用户尚未选定最终值。 ### 跳跃「高度过低、落地太快」的调法(已给方案,待用户选) - 问题:现状 g=9.8 + jump_velocity=1.4639 → 跳高 **0.109 m**(0.73 身高)、**滞空仅 0.30 s**,跳一下几乎立刻落地。 - **核心关系**:`h = v²/2g`、`T = 2v/g` → 合起来 `h = g·T²/8`。 → **固定重力下,滞空翻倍 = 高度涨 4 倍**。所以「既要跳得久、又不跳太高」只能靠**降重力**,光提 `jump_velocity` 做不到。 - **反解公式**(定目标高度 h 与滞空 T):`g = 8h/T²`(→ gravity_scale = g/9.8),`jump_velocity = 4h/T`。 - 三个候选(角色 0.15 m,1 身高 = 0.15 m): | 方案 | gravity_scale | jump_velocity | 跳高 | 身高倍数 | 滞空 | 落地时长 | |---|---|---|---|---|---|---| | 现状 | 1.0 | 1.4639 | 0.109 m | 0.73 | 0.30 s | — | | A 只提跳跃 | 1.0 | 2.10 | 0.225 m | 1.50 | 0.43 s | 不变 | | B 只降重力 | 0.5 | 1.4639 | 0.219 m | 1.46 | 0.60 s | ×1.41 | | **C 组合(推荐)** | 0.5 | 1.50 | 0.230 m | 1.53 | 0.61 s | ×1.41 | → A 和 B 高度几乎一样,但 B 的滞空长 40% —— 这正是「降重力」的价值。 - 副作用:g=4.9 时从 1 m 高落下要 0.639 s(原 0.452 s)。仍远好于等比 g=0.84(比月球还飘)。 - jump 动画片段 0.5 s:现状只播得完 30%,C 方案升到 61%。要让上升时间完全等于 0.5 s 需 T=1.0 s → g 低到 1.84(接近月球),不建议。 - **可选改进(未实施)**:把 `jump_velocity` 换成「跳跃高度 `jump_height`(米)」—— 设计上更直观,且改重力时高度自动保持,不会出现「改重力后跳高暴涨」的坑。 ### 跳跃参数定案:方案 C(已实施) - 用户问「推荐哪个方案」→ 定 **C**:`gravity_scale = 0.5`(有效 g = 4.9)、`jump_velocity = 1.5`。 - 结果:跳高 **0.230 m(1.53 身高)**、滞空 **0.612 s**、上升 0.306 s;从 1 m 高落下 0.639 s(原 0.452 s,×1.41)。 - 选 C 不选 A(只提 jump_velocity、保持 g=9.8)的理由:同样 0.23 m 的高度,A 只有 0.43 s 滞空,落地仍嫌仓促;C 多出 0.18 s。而且 C 让 jump 片段能播完约六成(A 只有四成)。 - 没选等比方案(`gravity_scale = 0.0857`):那比月球(1.62)还飘。 - 代码注释已整理:`jump_velocity` 与 `gravity_scale` 标注为**配对参数**,并写入反解公式 `g = 8h/T²`、`v = 4h/T`;`player_animator.gd` 里那句引用旧值的上升时间注释也改准了(1.5 / 4.9 ≈ 0.31 s)。 ### 贴墙时摄像机穿模(已修) - **根因(纯几何,差 8 毫米)**: | 项 | 值 | 来源 | |---|---|---| | 摄像机 `near` | **0.05 m** | Godot 默认,`player.tscn` 未覆盖 | | 摄像机前移量 | **0.01074 m** | `Camera` 本地 Z = −0.1118 × Player 缩放 0.096096 | | 贴墙时身体离墙 | **0.05241 m** | 胶囊局部半径 0.5454 × 0.096096 | | **摄像机到墙** | **0.04167 m** | 0.05241 − 0.01074 | → `0.0417 < 0.05`,墙面落在近裁剪面**内部**被整个裁掉,于是看到墙的背面/内部。 - **修复 1**:`player.tscn` 的 `Camera` 加 `near = 0.01`(留 4 倍余量)。项目是 Forward+(反向 Z 深度),near 缩小对深度精度影响很小。`far` 保持默认未动。 - **修复 2**:`player.gd` 新增 `@onready var model: Node3D = get_node_or_null("Body/mouse") as Node3D`,在权威分支里 `model.visible = false` —— 原来正是靠 near=0.05 把自己的身体裁掉才看不见,near 调小后身体会露出来。只影响本地玩家,远端玩家各自的模型实例不受影响。 - 代价:自己也看不到自己的影子(`visible=false` 会关掉阴影)。想保留影子可改用「本地玩家的 MeshInstance3D 放到独立视觉层 + 本地摄像机 cull_mask 排除该层」,但要注意该层不能和远端玩家共用,否则本地相机也看不到别人。 - **备选方案(未采用)**:把胶囊半径加大到 > 0.06 m 让摄像机永远离墙更远 —— 会改变玩法(体型变胖);或把 `Camera` 的前移量改小 —— 会动视角位置。 ### 附带发现的 bug:复制配置里的节点路径过期(已修) - 用户此前把 `AnimationPlayer` 从 `Body/AnimationPlayer` 移到 Player 根节点(`$AnimationPlayer`),但 `player_sync.tres` 里两条路径仍写着 `Body/AnimationPlayer:anim_state` / `Body/AnimationPlayer:anim_speed` → 这两个属性根本没被复制,**远端玩家的动画状态不会同步**。 - 已改为 `AnimationPlayer:anim_state` / `AnimationPlayer:anim_speed`。 ## ⚠ 项目约定(用户明确要求) **改 Godot 的 .tscn / .tres / .gd,一律走 godot-ai MCP,不要直接写文件。** 原因见下:外部直接写 .tscn 会和编辑器的文件监视重载(`_reload_modified_scenes`)竞态,导致编辑器读到中间状态、报出**假的解析错误**。 - 可用工具:`scene_open` / `scene_save` / `node_set_property` / `node_get_properties` / `node_create` / `node_manage` / `script_manage(find_symbols 可当解析校验)` / `script_create`(带诊断) / `filesystem_manage(read_text/write_text/scan/reimport)` / `resource_manage` / `logs_read` / `editor_manage(state)` / `project_run`。 - 调用形式:多数工具是「扁平参数」(如 `scene_open {path}`),管理类工具是 `{op, params}` 双层(如 `editor_manage {op:"state"}`)。`node_*` 的 path 是**相对当前编辑场景根**(如 `/Player/CameraRig/Camera`),不是 `/root/...`。 ## player.tscn 解析错误事件(已解决) - 现象:`Parse Error ... player.tscn:38` in `_parse_node_tag`,场景彻底打不开。 - **真实原因:不是文件语法错误。** 我用 MCP `node_set_property` 设 `Camera.near = 0.01` 后 `scene_save`,编辑器写出的内容与我手写的**字节完全一致**(同为 5106 字节),且 `scene_open(force_reload=true)` 立即 `reloaded_from_disk: true` 加载成功。 → 根因是**外部直接写 .tscn 与编辑器文件监视及重载的竞态**。 - 诊断过程中的有效手段(可复用): 1. `editor_manage(op="state")` 看当前场景与就绪状态 2. `scene_open` 做**对照实验**(打开一个已知正常场景,若 `switched:true` 则说明工具可靠、目标文件真的坏了) 3. `logs_read(source="editor", include_details=true)` 拿准确报错函数与行号 4. 用 `node_create` 在**内存里**加临时节点 + `node_get_properties` 探测属性真名(**不保存、不落盘**,随后 `node_manage(op="delete")` 清掉)—— 这次用它确认了 `Camera3D.near` 是合法 float 属性(默认 0.05,`far` 默认 4000) 5. `script_manage(op="find_symbols")` 可当脚本**解析校验**器;`filesystem_manage(op="scan")` 重建 `class_name` 全局表 - 修复结果:`player.tscn` 的 `Camera` 现有 `near = 0.01`(MCP 设置并保存),场景可正常加载。 ### 日志里发现的其它线索(多为历史记录,非当前问题) - `Could not find type "PlayerLook/PlayerMotor/PlayerPickup/Crosshair"`、`Identifier "animator" not declared` —— 都是**旧状态的历史日志**;`find_symbols` 证明 player.gd 现已正常,且 `filesystem_manage(scan)` 返回 `global_classes_registered_delta: 0`(类名表本是全的,共 75 个)。 - `crosshair.gd:133 _draw 里 texture_for_state 收到 Nil` —— `@tool` 脚本热重载时旧实例被新代码调用的一次性产物,非当前 bug。 - `player.tscn` 的 **Crosshair 纹理疑似接反**:`hover_texture = grab_hover.png`、`hold_texture = grab.png`。按我的语义(hover=指着可拖拽物 / hold=正抓着)应互换;但若按光标包命名(`_hover` 后缀表示悬停态)则现状正确。**未擅自改动,已向用户指出。** - 遗留警告(非本次引入):`face_painter.gd:153` 整数除法、`face_rig.gd:190` 局部变量 `name` 遮蔽 Node.name、`paint_panel.gd:69` 局部变量 `scale` 遮蔽 Control.scale、`Cannot load shader: res://shader/stylized.gdshader`。