extends CharacterBody3D ## 跳跃输入缓冲(秒):按下跳跃后这段时间内若落地,自动补上一次起跳 const JUMP_BUFFER_TIME: float = 0.1 @export_group("视角") ## 鼠标灵敏度(弧度/像素) @export var mouse_sensitivity: float = 0.0025 ## 最低俯角(向下看) @export var pitch_min_deg: float = -89.0 ## 最高仰角(向上看) @export var pitch_max_deg: float = 89.0 @export_group("移动") ## 常规移动速度(米/秒) @export var walk_speed: float = 5.0 ## 疾跑速度(米/秒) @export var sprint_speed: float = 8.5 ## 地面加速度(米/秒²) @export var acceleration: float = 45.0 ## 空中加速度(米/秒²) @export var air_acceleration: float = 12.0 ## 起跳初速度(米/秒) @export var jump_velocity: float = 5.0 @onready var body: Node3D = $Body ## 眼睛分两层转:CameraRig 绕竖直轴转(yaw),Camera 原地抬头低头(pitch)。 ## 分开是为了让 pitch 不掺进移动方向 —— 移动只用 rig 的 basis,低头看地也不会走歪。 @onready var camera_rig: Node3D = $CameraRig @onready var camera: Camera3D = $CameraRig/Camera ## 动画状态机。控制器只把「运动结果」交给它,它也只管自己播动画。 @onready var animator: PlayerAnimator = $Body/AnimationPlayer @onready var interaction_pos: Marker3D = $CameraRig/Camera/InteractionPos @onready var interaction_ray: RayCast3D = $CameraRig/Camera/InteractionRay var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8)) var _yaw: float = 0.0 var _pitch: float = 0.0 var _jump_buffer: float = 0.0 # 拾取 var _pickObj = null var _pickPower = 10 func _ready() -> void: # 远端玩家只是一具「回放身体」: # 拆掉相机 —— 否则每多一个玩家就多一台 Camera3D 抢渲染; # 关掉输入与物理 —— 否则与权威端双重模拟,表现为抖动、漂移、穿地。 # 它的 position 与 Body.rotation 由 Sync 子节点同步;血条由 health 同步。 if not is_multiplayer_authority(): camera_rig.queue_free() set_physics_process(false) set_process_unhandled_input(false) return _yaw = camera_rig.rotation.y _pitch = clampf(camera.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg)) _apply_look() set_mouse_captured(true) func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: _yaw -= event.relative.x * mouse_sensitivity _pitch = clampf( _pitch - event.relative.y * mouse_sensitivity, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg) ) _apply_look() elif event.is_action_pressed("jump"): _jump_buffer = JUMP_BUFFER_TIME elif event.is_action_pressed("ui_cancel"): set_mouse_captured(Input.mouse_mode != Input.MOUSE_MODE_CAPTURED) elif event is InputEventMouseButton and event.pressed and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: set_mouse_captured(true) if event is InputEventMouseButton and event.is_action_pressed("carry"): if _pickObj == null: _pickObject() else: _removePickObject() func set_mouse_captured(captured: bool) -> void: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE ## 把 _yaw / _pitch 落到节点上。鼠标一动就调一次(渲染帧),所以视角跟手、不等物理帧。 ## 身体跟着一起转 —— 第一人称里「你看哪,身体就朝哪」,别人看你也是这个朝向。 func _apply_look() -> void: camera_rig.rotation.y = _yaw camera.rotation.x = _pitch body.rotation.y = _yaw func _physics_process(delta: float) -> void: # --- 重力 --- if is_on_floor(): if velocity.y < 0.0: velocity.y = 0.0 else: velocity.y -= _gravity * delta # --- 跳跃(读取输入缓冲)--- if _jump_buffer > 0.0: _jump_buffer -= delta if is_on_floor(): velocity.y = jump_velocity _jump_buffer = 0.0 # --- 相对视线方向的水平输入 --- # 只用 rig 的 basis(只有 yaw),低头看地时前进方向不会跟着往下扎。 var input_dir := Input.get_vector("move_left", "move_right", "move_back", "move_forward") var look_basis := camera_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() # 只有真正按了方向键才允许疾跑(避免原地"疾跑"触发跑动动画) var sprinting := Input.is_action_pressed("sprint") and input_dir.length_squared() > 0.000001 var target_speed := sprint_speed if sprinting else walk_speed var accel := acceleration if is_on_floor() else air_acceleration var horizontal := Vector2(velocity.x, velocity.z) horizontal = horizontal.move_toward(Vector2(wish.x, wish.z) * target_speed, accel * delta) velocity.x = horizontal.x velocity.z = horizontal.y move_and_slide() # 动画只读运动结果。控制器不认识任何片段名,动画器也不认识输入。 animator.set_motion( velocity, is_on_floor(), sprinting, Input.is_action_pressed("crouch"), ) # 物体拖动 if _pickObj != null: var pickObjPos = _pickObj.global_position var pickPlacePos = interaction_pos.global_position _pickObj.set_linear_velocity((pickPlacePos - pickObjPos) * _pickPower) func _pickObject() -> void: var pickCollider = interaction_ray.get_collider() print(pickCollider) if pickCollider != null and pickCollider is RigidBody3D: print(pickCollider) _pickObj = pickCollider func _removePickObject() -> void: _pickObj = null