Unity玩家控制器跳跃丢失水平速度问题及优化需求
玩家跳跃丢失水平速度问题及优化方案
问题描述
开发Unity玩家控制器时,跳跃动作会导致玩家丢失之前的水平速度,产生撞隐形墙的违和感。尝试调整drag参数、斜坡控制逻辑、跳跃实现方式均无效,需要修复该问题并获得更高效的玩家移动实现方案。
问题根源
- 跳跃方法的水平速度处理错误:
Jump()方法中构造水平速度向量时参数顺序错误,导致水平速度被错误重置。 - 地面移动方式与物理引擎冲突:地面移动使用
rb.MovePosition结合Lerp,这种直接修改位置的方式会干扰Rigidbody的速度状态,导致跳跃时速度丢失。 - 斜坡移动的法线引用不一致:
GetSlopeMoveDirection()使用未更新的slopeHit.normal,但地面检测时更新的是slopeNormal,导致斜坡移动方向计算错误。 - 着陆逻辑冗余:
OnLanding()中的Invoke恢复速度和drag的逻辑会导致速度状态异常。
修复后的完整代码
using UnityEngine; using System.Collections; public class PlayerMovement : MonoBehaviour { [Header("Movement Settings")] public float walkSpeed = 5f; public float sprintSpeed = 10f; public float crouchSpeed = 2f; public Transform orientation; public float groundDrag = 5f; public float airDrag = 0f; public float moveForce = 100f; public float maxSpeed = 10f; [Header("Ground Check")] public float groundCheckRadius = 0.5f; public LayerMask groundLayer; private bool grounded; private bool wasGrounded; [Header("Slope Handling")] public float maxSlopeAngle = 45f; private bool onSlope; private Vector3 slopeNormal; private float slopeRayLength = 2f; private float groundRayOffset = 0.5f; [Header("Player Jumping")] public float jumpForce = 10f; public float jumpCooldown = 1f; public float airMultiplier = 0.25f; [Header("Crouching")] public float crouchYScale = 0.5f; private float startYScale; [Header("Keybinds")] public KeyCode jumpKey = KeyCode.Space; public KeyCode sprintKey = KeyCode.LeftShift; public KeyCode crouchKey = KeyCode.LeftControl; private Rigidbody rb; private float moveSpeed; private float horizontalInput; private float verticalInput; private bool readyToJump = true; private Vector3 moveDirection; public MovementState state; public enum MovementState { walking, sprinting, crouching, air } private void Start() { rb = GetComponent<Rigidbody>(); rb.freezeRotation = true; rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY | RigidbodyConstraints.FreezeRotationZ; startYScale = transform.localScale.y; rb.drag = groundDrag; } private bool IsGrounded() { bool isGrounded = false; Vector3 rayOrigin = transform.position + Vector3.up * groundRayOffset; for (int i = -1; i <= 1; i++) { Vector3 offset = Vector3.right * i * groundCheckRadius; Vector3 rayPosition = rayOrigin + offset; if (Physics.Raycast(rayPosition, Vector3.down, out RaycastHit hit, slopeRayLength, groundLayer)) { isGrounded = true; slopeNormal = hit.normal; float angle = Vector3.Angle(Vector3.up, slopeNormal); onSlope = angle > 0 && angle <= maxSlopeAngle; } } return isGrounded; } private void Update() { wasGrounded = grounded; grounded = IsGrounded(); MyInput(); StateHandler(); // 切换drag rb.drag = grounded ? groundDrag : airDrag; if (grounded && !wasGrounded) { OnLanding(); } } private void MyInput() { horizontalInput = Input.GetAxisRaw("Horizontal"); verticalInput = Input.GetAxisRaw("Vertical"); if (Input.GetKeyDown(jumpKey) && readyToJump && grounded) { readyToJump = false; Jump(); StartCoroutine(JumpCooldown()); } // 蹲伏缩放 if (Input.GetKey(crouchKey)) { transform.localScale = new Vector3(transform.localScale.x, crouchYScale, transform.localScale.z); } else { transform.localScale = new Vector3(transform.localScale.x, startYScale, transform.localScale.z); } } private void FixedUpdate() { MovePlayer(); CapSpeed(); } private void StateHandler() { if (Input.GetKey(crouchKey)) { state = MovementState.crouching; moveSpeed = crouchSpeed; } else if(grounded && Input.GetKey(sprintKey)) { state = MovementState.sprinting; moveSpeed = sprintSpeed; } else if (grounded) { state = MovementState.walking; moveSpeed = walkSpeed; } else { state = MovementState.air; } } private void MovePlayer() { moveDirection = (orientation.forward * verticalInput + orientation.right * horizontalInput).normalized; if (onSlope) { // 斜坡移动:将方向投影到斜坡平面 Vector3 slopeMoveDir = Vector3.ProjectOnPlane(moveDirection, slopeNormal).normalized; rb.AddForce(slopeMoveDir * moveSpeed * moveForce, ForceMode.Force); // 防止斜坡上滑 if (rb.velocity.y > 0) rb.AddForce(Vector3.down * 80f, ForceMode.Force); } else if (grounded) { // 地面移动:使用AddForce保持物理一致性 rb.AddForce(moveDirection * moveSpeed * moveForce, ForceMode.Force); } else { // 空中移动 rb.AddForce(moveDirection * moveSpeed * moveForce * airMultiplier, ForceMode.Force); } } private void CapSpeed() { Vector3 flatVelocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z); // 限制水平速度 if (flatVelocity.magnitude > maxSpeed) { Vector3 limitedVelocity = flatVelocity.normalized * maxSpeed; rb.velocity = new Vector3(limitedVelocity.x, rb.velocity.y, limitedVelocity.z); } } private void Jump() { // 保留水平速度,仅重置垂直速度 Vector3 flatVelocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z); rb.velocity = flatVelocity; // 施加跳跃力 rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } private IEnumerator JumpCooldown() { yield return new WaitForSeconds(jumpCooldown); readyToJump = true; } private void OnLanding() { // 简化着陆逻辑,移除冗余的速度恢复 onSlope = false; } private void OnDrawGizmos() { Gizmos.color = Color.red; Vector3 rayOrigin = transform.position + Vector3.up * groundRayOffset; for (int i = -1; i <= 1; i++) { Vector3 offset = Vector3.right * i * groundCheckRadius; Gizmos.DrawRay(rayOrigin + offset, Vector3.down * slopeRayLength); } } }
优化说明
- 修复跳跃速度丢失问题:修正
Jump()方法中水平速度的构造逻辑,正确保留x和z轴速度,仅重置垂直速度。 - 统一物理移动逻辑:地面移动改用
AddForce,与空中、斜坡移动逻辑保持一致,避免直接修改位置导致的物理冲突。 - 修复斜坡移动计算:将斜坡移动方向计算整合到
MovePlayer()中,使用正确的slopeNormal确保方向准确。 - 简化着陆逻辑:移除冗余的速度恢复和drag延迟重置,直接切换drag状态,避免速度异常。
- 优化速度限制:仅限制水平方向速度,不影响垂直方向的跳跃下落速度。
- 简化蹲伏逻辑:合并蹲伏的缩放控制,避免重复的条件判断。
内容的提问来源于stack exchange,提问作者KingKhong
相关产品推荐
相关产品推荐

