@tool class_name CarryArms extends Node ## 抱东西时把两条胳膊伸出去抱住物品,走路时人和东西一起晃。 ## ## 模型里没有「抱」这个动作片段,所以这里在动画**之后**叠加一段手臂姿态: ## 只动 arm_l_* / arm_r_* 这几根骨骼,其余骨骼照旧交给 AnimationPlayer。 ## process_priority 设得比播放器大,保证在它写完骨骼之后才覆盖 —— 否则会被动画冲掉。 ## ## 晃动同时作用在两条胳膊和「抱持点」上:物品每帧跟住抱持点,于是东西和人一起晃, ## 不会出现「胳膊在晃、东西钉在手上」的穿帮。 ## ## 控制器只负责说「抱没抱、走多快」,具体摆成什么样全由这里的参数决定。 ## ## 标了 @tool 是为了能在编辑器里跑测试(非 @tool 脚本在编辑器里是 placeholder,方法调不动)。 @export_group("接线") ## 骨架。本节点挂在 Body 下,所以要先 .. 回到 Body @export var skeleton_path: NodePath = ^"../mouse/MouseRig/Skeleton3D" ## 抱持点(Marker3D)。物品就摆在它上面:Body/CarryArms → ../../CameraRig/CarrySocket @export var socket_path: NodePath = ^"../../CameraRig/CarrySocket" @export_group("抱持姿势") ## 大臂前摆(度):把手伸到身前 @export var shoulder_swing_deg: float = 40.0 ## 大臂内收(度):两只手往中间合,抱住东西 @export var shoulder_tuck_deg: float = 50.0 ## 大臂侧收(度):绕身体前后轴转,把胳膊从上往下收到身前 @export var shoulder_roll_deg: float = 0.0 ## 小臂弯曲(度) @export var elbow_deg: float = 15.0 ## 胳膊沿自身方向拉长的倍数(>1 就是「延伸」) @export var stretch: float = 1.25 @export_group("走路晃动") ## 胳膊晃动幅度(度) @export var wobble_deg: float = 8.0 ## 抱持点晃动幅度(米)。物品跟着它晃,所以别太大 @export var wobble_socket: float = 0.035 ## 每秒晃几下 @export var wobble_hz: float = 1.7 ## 姿态过渡速度(越大越跟手) @export var blend_speed: float = 8.0 var _skeleton: Skeleton3D var _socket: Node3D var _socket_rest := Transform3D.IDENTITY ## 手臂骨骼索引,按 [左大臂, 左小臂, 左手, 右大臂, 右小臂, 右手] 的顺序 var _bones: Array[int] = [] var _carrying := false ## 姿态权重 0~1:抱起来时涨到 1,放下时退回 0(退回 0 之后就完全交给动画) var _weight := 0.0 var _phase := 0.0 ## 当前速度 / 常规走速,用来决定晃多厉害 var _speed_ratio := 0.0 func _ready() -> void: # 跑在 AnimationPlayer 之后,否则我们摆的骨骼会被动画覆盖 process_priority = 100 _skeleton = get_node_or_null(skeleton_path) as Skeleton3D if _skeleton == null: push_warning("%s 找不到骨架(%s),胳膊不会动" % [get_path(), skeleton_path]) _socket = get_node_or_null(socket_path) as Node3D if _socket != null: _socket_rest = _socket.transform for bone_name in ["arm_l_01", "arm_l_02", "arm_l_03", "arm_r_01", "arm_r_02", "arm_r_03"]: if _skeleton == null: break var index := _skeleton.find_bone(bone_name) if index < 0: push_warning("%s 里没有骨骼 %s,胳膊姿势会不完整" % [_skeleton.get_path(), bone_name]) _bones.append(index) ## 控制器每帧告诉它「抱没抱、走多快」。speed_ratio 用「当前水平速度 / 常规走速」。 func set_carrying(carrying: bool, speed_ratio: float) -> void: _carrying = carrying _speed_ratio = speed_ratio func _process(delta: float) -> void: var target := 1.0 if _carrying else 0.0 _weight = move_toward(_weight, target, blend_speed * delta) if _weight <= 0.0: # 没抱东西:不碰骨骼也不碰抱持点,全部交回给动画 return # 走得越快晃得越猛;站着不动时 phase 不推进,姿态是静的 var wobble_scale := clampf(_speed_ratio, 0.0, 1.5) _phase += delta * TAU * wobble_hz * wobble_scale var wobble := sin(_phase) * wobble_deg * wobble_scale * _weight if _socket != null: # 抱持点跟着晃,物品每帧跟住它 —— 东西和胳膊就同步了 var bob := sin(_phase) * wobble_socket * wobble_scale * _weight _socket.transform = _socket_rest.translated_local(Vector3(0.0, bob, 0.0)) if _skeleton == null or _bones.size() < 6: return # 左右两组,同相位(抱着重物是两只手一起上下),内收方向相反 for side in 2: var shoulder := _bones[side * 3] var elbow := _bones[side * 3 + 1] var wrist := _bones[side * 3 + 2] if shoulder < 0: continue var mirror := -1.0 if side == 0 else 1.0 # 参数是**世界方向**:X 负向 = 往前摆,Y = 左右收,Z = 侧收(左右手相反) _apply_bone(shoulder, Vector3( -(shoulder_swing_deg + wobble), shoulder_tuck_deg * mirror, shoulder_roll_deg * mirror, )) # 胳膊沿自身拉长 —— 这就是「可延伸」 _skeleton.set_bone_pose_scale(shoulder, Vector3(1.0, lerpf(1.0, stretch, _weight), 1.0)) if elbow >= 0: _apply_bone(elbow, Vector3(-elbow_deg, 0.0, 0.0)) if wrist >= 0: _apply_bone(wrist, Vector3(-elbow_deg * 0.4, 0.0, 0.0)) ## 在动画写好的姿态上再叠一个旋转。 ## ## 传进来的角度按**世界方向**理解(往前摆 = 绕 X 轴负向),内部用骨骼自身的静止姿态 ## 把它换算到骨骼本地空间 —— 否则得先猜出每根骨骼本地轴朝哪,换个模型就全错。 ## 叠在动画姿态上是乘法,所以走路原本的摆臂不会被抹掉。 func _apply_bone(index: int, offset_deg: Vector3) -> void: var rest_basis := _skeleton.get_bone_rest(index).basis var world := Basis.from_euler(offset_deg * PI / 180.0) var local := rest_basis.inverse() * world * rest_basis var posed := _skeleton.get_bone_pose_rotation(index) _skeleton.set_bone_pose_rotation(index, posed * local.get_rotation_quaternion())