Files
agame/.workbuddy/memory/2026-10-09.md
T
2026-10-09 16:36:17 +08:00

180 lines
24 KiB
Markdown
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.
# 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,**未写 `<title>`**(避免 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`。