134 lines
5.7 KiB
GDScript
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())
|