Files
agame/character/player/player_animator.gd
T
2026-10-02 09:04:37 +08:00

129 lines
4.5 KiB
GDScript
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.
extends Node
enum AnimState { IDLE, WALK, CROUCH, RUN, FALL, SNEAK, JUMP }
const ANIM_CLIPS: Dictionary = {
AnimState.IDLE: "idle",
AnimState.WALK: "walk",
AnimState.CROUCH: "crouch",
AnimState.RUN: "run",
AnimState.SNEAK: "SNEAK",
AnimState.JUMP: "jump",
AnimState.FALL: "fall",
}
## 需要循环的状态。
const ANIM_LOOPED: Array[int] = [AnimState.IDLE, AnimState.WALK, AnimState.RUN]
@export_group("接线")
## 要驱动的 AnimationPlayer。留空则在本节点的兄弟子树里自动找一个。
## 换模型后这里重新指一下即可 —— 这也是它能脱离具体模型的原因。
@export var animation_player: AnimationPlayer
@export_group("播放")
## 动作切换的交叉淡入时间(秒)。0 为硬切
@export var blend_time: float = 0.15
## 低于此水平速度就视为站立(米/秒)
@export var idle_threshold: float = 0.2
## 播放倍率下限:被墙抵住、起步那一瞬,脚步不该慢到像在滑冰
@export var speed_scale_min: float = 0.65
## 播放倍率上限:防止速度被外力拉高时脚步快到抽搐
@export var speed_scale_max: float = 1.6
@export_group("片段基准速度")
## walk 片段在多少米/秒下按 1.0 倍播放。这是片段自身的属性,不是控制器的手感参数:
## 控制器把跑速调到 6 m/s 时,这段动画自然播成 1.2 倍,脚步自动跟上,无需同步改两边。
@export var walk_ref_speed: float = 5.0
## sprint 片段的基准速度(米/秒)
@export var sprint_ref_speed: float = 8.5
## 当前动作状态(AnimState)。权威端每物理帧写一次,Sync 复制给远端。
var anim_state: int = AnimState.IDLE:
set(value):
if anim_state == value:
return
anim_state = value
_play(ANIM_CLIPS.get(anim_state, ""), blend_time)
## 当前播放倍率(1.0 = 片段原速)。
## 同样要复制:远端只有 position,算不出真实速度,只能照抄权威端的倍率。
var anim_speed: float = 1.0:
set(value):
value = clampf(value, speed_scale_min, speed_scale_max)
anim_speed = value
if animation_player != null:
animation_player.speed_scale = value
func _ready() -> void:
if animation_player == null:
push_warning("%s 没绑定 AnimationPlayer,角色不会有动画" % get_path())
return
_patch_loop_modes()
# 同步可能在 _ready() 之前就把 anim_state 写进来了 —— 那时 animation_player 还是 null,
# setter 空跑了一趟。所以这里按「当前状态」补播一遍,不能写死 idle。
_play(ANIM_CLIPS.get(anim_state, ""), 0.0)
# ---------------- 对外接口 ----------------
## 控制器每物理帧调用一次(放在 move_and_slide() 之后)。
## 只读入运动快照,不回写任何物理量 —— 这就是「单向耦合」的边界。
func set_motion(velocity: Vector3, on_floor: bool, sprinting: bool = false) -> void:
var state := _resolve_state(velocity, on_floor, sprinting)
# 先写倍率再切片段 —— 顺序反了会让新片段用一帧旧倍率。
anim_speed = _speed_scale_for(state, velocity)
anim_state = state # setter 负责切片段
## 当前正在播的片段名。调试与测试用。
func current_clip() -> String:
if animation_player == null:
return ""
return str(animation_player.current_animation)
# ---------------- 内部 ----------------
func _resolve_state(velocity: Vector3, on_floor: bool, sprinting: bool) -> int:
if not on_floor:
# 上升用 jump、越过顶点用 fall。jump 片段长 0.5s,
# 恰好约等于 jump_velocity 5 / 重力 9.8 的上升时间。
return AnimState.JUMP if velocity.y > 0.0 else AnimState.FALL
if Vector2(velocity.x, velocity.z).length() < idle_threshold:
return AnimState.IDLE
return AnimState.SPRINT if sprinting else AnimState.WALK
## 播放倍率 = 实际水平速度 / 该片段的基准速度。
## 只有循环的移动动作参与缩放,跳跃/下落/站立一律原速。
func _speed_scale_for(state: int, velocity: Vector3) -> float:
var ref := 0.0
match state:
AnimState.WALK:
ref = walk_ref_speed
AnimState.SPRINT:
ref = sprint_ref_speed
_:
return 1.0
if ref <= 0.0:
return 1.0
return Vector2(velocity.x, velocity.z).length() / ref
func _play(clip: String, blend: float) -> void:
if animation_player == null or clip.is_empty():
return
if not animation_player.has_animation(clip):
return
animation_player.play(clip, blend)
func _patch_loop_modes() -> void:
for state in ANIM_LOOPED:
var clip: String = ANIM_CLIPS[state]
if not animation_player.has_animation(clip):
continue
animation_player.get_animation(clip).loop_mode = Animation.LOOP_LINEAR