Files
agame/character/player/carry_arms.gd
T
2026-10-03 18:04:07 +08:00

134 lines
5.7 KiB
GDScript

@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())