24 KiB
24 KiB
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.gd155 行,混了视角 / 移动跳跃 / 拾取 / 多人权限四类职责。 - 方案:子节点组件化(与既有
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 指 + 手掌 + 拇指的圆角矩形),双层 —— 底层深色#1a1a1astroke-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_power10(新滞后 3.0/10 = 0.3 m < 0.444 m 持握距离,且已有速度前馈,安全)、throw_speed12(被扔的物体本身没缩,投掷距离不该变)、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%(多出耳朵/尾巴)。
- 模型:glb 顶点原始高
- 缩放系数
k = 0.15 / 0.187313 = 0.800799。 player.tscn:Player.scale0.12 → 0.096096;Camera.scale8.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_power10、throw_speed12(被扔物体是世界尺寸,未缩)、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_speed0.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 毫米):
项 值 来源 摄像机 near0.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:38in_parse_node_tag,场景彻底打不开。 - 真实原因:不是文件语法错误。 我用 MCP
node_set_property设Camera.near = 0.01后scene_save,编辑器写出的内容与我手写的字节完全一致(同为 5106 字节),且scene_open(force_reload=true)立即reloaded_from_disk: true加载成功。 → 根因是外部直接写 .tscn 与编辑器文件监视及重载的竞态。 - 诊断过程中的有效手段(可复用):
editor_manage(op="state")看当前场景与就绪状态scene_open做对照实验(打开一个已知正常场景,若switched:true则说明工具可靠、目标文件真的坏了)logs_read(source="editor", include_details=true)拿准确报错函数与行号- 用
node_create在内存里加临时节点 +node_get_properties探测属性真名(不保存、不落盘,随后node_manage(op="delete")清掉)—— 这次用它确认了Camera3D.near是合法 float 属性(默认 0.05,far默认 4000) 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。