class_name PlayerMotor extends Node ## 移动组件:重力、跳跃缓冲、水平加减速。 ## ## 只算速度写回 body.velocity,**不调 move_and_slide()** —— 推进由 Player 统一做。 ## 这样「算速度 → 推进 → 播动画」的顺序永远固定,不受节点树顺序影响。 ## ## 「走多快、蹲不蹲、跑不跑」由 hsm/ 的 LimboHSM(LimboAI)决定: ## 状态在 _enter 时写 target_speed / crouching / sprinting, ## 本组件只负责把这份「意图」用物理方式变成速度。 ## ## 设计基准:**小人国玩法,角色身高约 0.2 m**。速度不按「人类等比缩放」推导, ## 直接按鼠类疾走的手感定标:走 ≈ 12 身长/秒、疾跑 ≈ 18 身长/秒、 ## 跳高 ≈ 1.15 身高。速度/加速度按手感单独调即可,不必成套换算; ## 唯独 jump_velocity 与 gravity_scale 是**配对**的(反解公式见重力一节)。 ## 跳跃输入缓冲(秒):按下跳跃后这段时间内若落地,自动补上一次起跳 const JUMP_BUFFER_TIME: float = 0.1 @export_group("移动") ## 常规移动速度(米/秒)—— ≈ 12 身长/秒(0.2 m 身高),小鼠疾走的步频感 @export var walk_speed: float = 2.419 ## 疾跑速度(米/秒)—— ≈ 18 身长/秒 @export var sprint_speed: float = 3.624 ## 蹲行速度(米/秒)—— 走速的一半。蹲下时按住 Shift 也不疾跑(蹲伏优先)。 @export var crouch_speed: float = 1.21 ## 地面加速度(米/秒²)—— 约 0.11 秒加到走速全速,起步干脆(鼠类的一窜一动) @export var acceleration: float = 22.0 ## 空中加速度(米/秒²)—— 约地面的 1/4:空中能修正方向,但不至于完全失控 @export var air_acceleration: float = 5.9 ## 起跳初速度(米/秒)—— 配 gravity_scale 0.5(有效 g = 4.9)时, ## 跳高 ≈ 0.23 m(约 1.15 身高)、滞空 ≈ 0.61 s。**与 gravity_scale 配对**,见下。 @export var jump_velocity: float = 1.5 @export_group("重力") ## 重力缩放系数。0.5 = 有效重力 4.9 m/s²。 ## ## 小个子角色若用真实 9.8,同样的起跳速度滞空短一半、跳起来像被拍回去 —— ## 降重力换滞空,跳跃才有「腾空感」。0.5 时:跳高 0.23 m、滞空 0.61 s。 ## ## ⚠ 与 jump_velocity **配对**:改一个必须按反解公式重算另一个(跳跃高度 = v²/2g)。 ## 反解公式 —— 定目标高度 h(米)与滞空 T(秒): ## g = 8h / T² → gravity_scale = g / 9.8 ## jump_velocity = 4h / T ## ## 只影响本角色。**不要**去改 ProjectSettings 的 physics/3d/default_gravity —— ## 那是全局的,会连带把拖拽/投掷的刚体一起变飘。 @export_range(0.0, 2.0, 0.01) var gravity_scale: float = 0.5 ## 当前目标水平速度(米/秒)—— 由 HSM 状态在 _enter 时写入,step() 朝它加减速。 ## Idle / Crouch 写 0(自然减速到停),Walk / Sprint / Sneak 写各自速度。 var target_speed: float = 0.0 ## 是否蹲着 / 是否疾跑 —— 由 HSM 状态写入的**事实镜像**,本组件不再自己读输入推断: ## crouching 给 Look 压低摄像机,两者都给 DebugPanel 显示。 var sprinting: bool = false var crouching: bool = false var _body: CharacterBody3D var _look_rig: Node3D ## 项目设置里的默认重力(9.8)。实际用的是它 × gravity_scale。 var _gravity: float = 9.8 var _jump_buffer: float = 0.0 ## 由 Player 在 _ready() 注入依赖。look_rig 用来把输入转成「相对视线」的方向。 func setup(body: CharacterBody3D, look_rig: Node3D) -> void: _body = body _look_rig = look_rig _gravity = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8)) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("jump"): _jump_buffer = JUMP_BUFFER_TIME ## 本帧是否按着方向键。HSM 状态(Idle/Walk 等的转换判定)与 step() 共用这个口径。 func has_move_input() -> bool: return Input.get_vector("move_left", "move_right", "move_back", "move_forward") \ .length_squared() > 0.000001 ## 由 HSM 地面状态每帧调用:跳跃缓冲还有效且脚下有地板,就当场起跳并返回 true。 ## 起跳「动作」放在状态判定处执行(而不是 step() 里), ## 状态机同一帧就能拿到 vy > 0 这个事实、转进 Jump,动画不迟到一帧。 func try_jump() -> bool: if _jump_buffer > 0.0 and _body != null and _body.is_on_floor(): _body.velocity.y = jump_velocity _jump_buffer = 0.0 return true return false ## 每物理帧由 Player 调用一次,必须排在 move_and_slide() 之前。 ## 只写 _body.velocity,不做位移推进。 func step(delta: float) -> void: if _body == null or _look_rig == null: return # --- 重力 --- if _body.is_on_floor(): if _body.velocity.y < 0.0: _body.velocity.y = 0.0 else: # gravity_scale 每帧现读,方便运行中拖着试手感(1.0 = 物理真实的 9.8) _body.velocity.y -= _gravity * gravity_scale * delta # --- 跳跃缓冲倒计时(执行挪到 try_jump(),由 HSM 地面状态调用)--- if _jump_buffer > 0.0: _jump_buffer -= delta # --- 相对视线方向的水平输入 --- # 只用 rig 的 basis(只有 yaw),低头看地时前进方向不会跟着往下扎。 var input_dir := Input.get_vector("move_left", "move_right", "move_back", "move_forward") var look_basis := _look_rig.global_transform.basis var wish := look_basis.x * input_dir.x - look_basis.z * input_dir.y wish.y = 0.0 if wish.length_squared() > 0.000001: wish = wish.normalized() # 目标速度来自 HSM 状态(target_speed),不在本组件里按蹲键/Shift 分档。 var accel := acceleration if _body.is_on_floor() else air_acceleration var horizontal := Vector2(_body.velocity.x, _body.velocity.z) horizontal = horizontal.move_toward(Vector2(wish.x, wish.z) * target_speed, accel * delta) _body.velocity.x = horizontal.x _body.velocity.z = horizontal.y