You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity玩家控制器跳跃丢失水平速度问题及优化需求

玩家跳跃丢失水平速度问题及优化方案

问题描述

开发Unity玩家控制器时,跳跃动作会导致玩家丢失之前的水平速度,产生撞隐形墙的违和感。尝试调整drag参数、斜坡控制逻辑、跳跃实现方式均无效,需要修复该问题并获得更高效的玩家移动实现方案。

问题根源

  1. 跳跃方法的水平速度处理错误:Jump()方法中构造水平速度向量时参数顺序错误,导致水平速度被错误重置。
  2. 地面移动方式与物理引擎冲突:地面移动使用rb.MovePosition结合Lerp,这种直接修改位置的方式会干扰Rigidbody的速度状态,导致跳跃时速度丢失。
  3. 斜坡移动的法线引用不一致:GetSlopeMoveDirection()使用未更新的slopeHit.normal,但地面检测时更新的是slopeNormal,导致斜坡移动方向计算错误。
  4. 着陆逻辑冗余: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);
        }
    }
}

优化说明

  1. 修复跳跃速度丢失问题:修正Jump()方法中水平速度的构造逻辑,正确保留x和z轴速度,仅重置垂直速度。
  2. 统一物理移动逻辑:地面移动改用AddForce,与空中、斜坡移动逻辑保持一致,避免直接修改位置导致的物理冲突。
  3. 修复斜坡移动计算:将斜坡移动方向计算整合到MovePlayer()中,使用正确的slopeNormal确保方向准确。
  4. 简化着陆逻辑:移除冗余的速度恢复和drag延迟重置,直接切换drag状态,避免速度异常。
  5. 优化速度限制:仅限制水平方向速度,不影响垂直方向的跳跃下落速度。
  6. 简化蹲伏逻辑:合并蹲伏的缩放控制,避免重复的条件判断。

内容的提问来源于stack exchange,提问作者KingKhong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 20:50:29