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

基于Character Controller实现FPS游戏滑铲功能的技术求助

问题描述

我正在开发一款FPS速通游戏,希望为玩家移动添加滑铲功能。目前已完成蹲伏功能的实现,但在滑铲功能开发上遇到瓶颈(使用Character Controller组件)。尝试过多个教程及论坛方案,但均未成功,多数情况下功能无响应。

当前PlayerMovement脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UIElements;

public class PlayerMovement : MonoBehaviour
{
    public CharacterController controller;
    private Rigidbody rb;
    public Transform cameraTransform;

    public HeadBobController headBobController;

    [Header("Movement")]
    private float speedMultiplier = 1f;
    public float speed = 12f;
    public float sprintSpeed = 20f;
    public float slopeSpeed = 0.01f;

    public float gravity = -9.81f;
    public float jumpHeight = 3f;
    public float sensitivity = 2f;
    public float cameraRotationLimit = 85f;

    [Header("Countermovement")]
    public float counterMovement = 0.175f;
    private float threshold = 0.01f;
    public float maxSlopeAngle = 35f;

    [Header("Sliding / Crouch")]
    public float slideSpeed;
    float originalHeight;
    public float reducedHeight;
    float targetHeight = 2f;
    public float timeToCrouch = 0.25f;
    float timeElapsed = 0;

    bool canStopCrouching;
    bool wantStopCrouch;

    bool isRunning = false;
    public bool isCrouching = false;
    bool canUseHeadbob = true;
    bool WillSlideOnSlopes = true;

    bool IsSliding
    {
        get
        {
            if (controller.isGrounded && Physics.Raycast(transform.position, Vector3.down, out RaycastHit slopeHit, 3f))
            {
                hitPointNormal = slopeHit.normal;
                return Vector3.Angle(hitPointNormal, Vector3.up) > controller.slopeLimit;

            } else
            {
                return false;
            }
        }
    }

    [Header("Headbob")]
    [SerializeField] private float walkBobSpeed = 14f;
    [SerializeField] private float walkBobAmount = 0.05f;
    [SerializeField] private float sprintBobSpeed = 18f;
    [SerializeField] private float sprintBobAmount = 0.11f;
    [SerializeField] private float crouchBobSpeed = 8f;
    [SerializeField] private float crouchBobAmount = 0.025f;
    private float defaultYPos = 0;
    private float timer;

    private Vector3 hitPointNormal;

    [Header("Ground Check")]
    public Transform groundCheck;
    public float groundDistance = 0.4f;
    public LayerMask groundMask;

    Vector3 velocity;
    public bool isGrounded;

    [Header("Camera")]
    float cameraRotationX = 0f;
    float cameraRotationY = 0f;

    public Camera camera;
    public Transform orientation;

    [Header("Other")]
    public GameObject speedlines;
    public ParticleSystem jumpParticle;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        controller = GetComponent<CharacterController>();
        originalHeight = controller.height;
        defaultYPos = camera.transform.localPosition.y;
        //Cursor.lockState = CursorLockMode.Locked;
        //Cursor.visible = false;
    }

    public void Update()
    {
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

        if (isGrounded && velocity.y < 0)
        {
            velocity.y = -2f;
        }

        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        Vector3 move = transform.right * x + transform.forward * z;
        move = Vector3.ClampMagnitude(move, 1f);

        if (Input.GetKey(KeyCode.LeftShift) && !isCrouching)
        {
            controller.Move(move * sprintSpeed * Time.deltaTime);
            isRunning = true;
            headBobController.amplitude = 0.1f;
            headBobController.frequency = 30f;
        } else
        {
            controller.Move(move * speed * speedMultiplier * Time.deltaTime);
            isRunning = false;
            headBobController.amplitude = 0.0426f;
            headBobController.frequency = 19.1f;
        }

        if (Physics.Raycast(camera.transform.position, Vector3.up, 1f))
        {
            canStopCrouching = false;
        } else
        {
            canStopCrouching = true;
        }

        if (canStopCrouching && wantStopCrouch && !Physics.Raycast(camera.transform.position, Vector3.up, 1f))
        {
            wantStopCrouch = false;
            StopCrouch();
        }

        if (Input.GetKeyDown(KeyCode.LeftControl) && isGrounded)
        {
            //controller.height = Mathf.Lerp(reducedHeight, originalHeight, timeElapsed);
            StartCrouch();
        } else if(Input.GetKeyUp(KeyCode.LeftControl) && isGrounded && !Physics.Raycast(camera.transform.position, Vector3.up, 1f))
        {
            //controller.height = Mathf.Lerp(originalHeight, targetHeight, timeElapsed);
            StopCrouch();
        } else if(Input.GetKeyUp(KeyCode.LeftControl) && isGrounded && Physics.Raycast(camera.transform.position, Vector3.up, 1f))
        {
            wantStopCrouch = true;
        }

        if (Input.GetKey(KeyCode.LeftControl) && isRunning && isGrounded)
        {
            //Sliding
        }

        if (canUseHeadbob && isGrounded)
        {
            if (Mathf.Abs(move.x) > 0.1f || Mathf.Abs(move.z) > 0.1f)
            {
                timer += Time.deltaTime * (isCrouching ? crouchBobSpeed : isRunning ? sprintBobSpeed : walkBobSpeed);
                camera.transform.localPosition = new Vector3(camera.transform.localPosition.x, defaultYPos + Mathf.Sin(timer) * (isRunning ? sprintBobAmount : walkBobAmount), camera.transform.localPosition.z);
            }
        }
        
        if(WillSlideOnSlopes && IsSliding)
        {
            move += new Vector3(hitPointNormal.x, -hitPointNormal.y, hitPointNormal.z) * slopeSpeed;
        }

        if (isRunning)
        {
            speedlines.active = true;
        } else {
            speedlines.active = false;
        }

        controller.Move(move * speed * speedMultiplier * Time.deltaTime);

        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            controller.Move(-transform.forward * 0.1f);
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
            jumpParticle.Play();
        }

        velocity.y += gravity * Time.deltaTime;

        controller.Move(velocity * Time.deltaTime);

        // Rotate the player based on mouse input
        float mouseX = Input.GetAxis("Mouse X") * sensitivity;
        float mouseY = Input.GetAxis("Mouse Y") * sensitivity;

        cameraRotationY += mouseX;

        cameraRotationX -= mouseY;
        cameraRotationX = Mathf.Clamp(cameraRotationX, -cameraRotationLimit, cameraRotationLimit);

        cameraTransform.localEulerAngles = new Vector3(cameraRotationX, cameraRotationY, 0f);
        transform.eulerAngles = new Vector3(0f, cameraRotationY, 0f);
    }

    private void StartCrouch()
    {
        controller.height = reducedHeight;
        speedMultiplier = 0.5f;
        isCrouching = true;
    }

    private void StopCrouch()
    {
        controller.height = originalHeight;
        speedMultiplier = 1f;
        isCrouching = false;
    }
}

期望玩家在**奔跑(isRunning)且处于地面(isGrounded)**状态时,按下左Ctrl触发滑铲,恳请指导如何将该功能整合至现有代码中!


解决方案

1. 添加滑铲状态变量

在Sliding / Crouch区域下补充滑铲相关变量:

[Header("Sliding / Crouch")]
public float slideSpeed;
public float slideDuration = 0.5f; // 滑铲持续时长
public float slideDecayRate = 1.5f; // 滑铲速度衰减系数
float originalHeight;
public float reducedHeight;
float targetHeight = 2f;
public float timeToCrouch = 0.25f;
float timeElapsed = 0;

bool canStopCrouching;
bool wantStopCrouch;
bool isSliding = false; // 滑铲状态标记
float slideTimer = 0f; // 滑铲计时器

2. 修改输入检测逻辑

替换原左Ctrl输入判断,优先触发滑铲条件:

// 替换原左Ctrl输入代码块
if (Input.GetKeyDown(KeyCode.LeftControl) && isGrounded)
{
    if (isRunning)
    {
        StartSlide();
    }
    else
    {
        StartCrouch();
    }
}
else if(Input.GetKeyUp(KeyCode.LeftControl) && isGrounded && !Physics.Raycast(camera.transform.position, Vector3.up, 1f))
{
    if (isSliding)
    {
        StopSlide();
    }
    else
    {
        StopCrouch();
    }
}
else if(Input.GetKeyUp(KeyCode.LeftControl) && isGrounded && Physics.Raycast(camera.transform.position, Vector3.up, 1f))
{
    wantStopCrouch = true;
}

3. 实现滑铲核心方法

添加滑铲启动和停止的处理方法:

private void StartSlide()
{
    isSliding = true;
    isCrouching = true;
    controller.height = reducedHeight;
    speedMultiplier = 1f; // 滑铲使用独立速度,不受蹲伏减速影响
    slideTimer = slideDuration;

    // 调整滑铲时的头部晃动参数
    headBobController.amplitude = 0.08f;
    headBobController.frequency = 25f;
}

private void StopSlide()
{
    isSliding = false;
    // 检查头顶无障碍物再恢复站立
    if (canStopCrouching)
    {
        StopCrouch();
    }
    else
    {
        wantStopCrouch = true;
    }
}

4. 处理滑铲运动逻辑

替换原代码中//Sliding注释区域,添加滑铲帧更新逻辑:

// 替换原滑铲注释代码块
if (isSliding)
{
    slideTimer -= Time.deltaTime;
    // 沿移动方向施加衰减的滑铲速度
    Vector3 slideDir = transform.forward * Input.GetAxis("Vertical") + transform.right * Input.GetAxis("Horizontal");
    slideDir = Vector3.ClampMagnitude(slideDir, 1f);
    float currentSlideSpeed = slideSpeed * Mathf.Lerp(1f, 0f, 1 - (slideTimer / slideDuration));
    controller.Move(slideDir * currentSlideSpeed * Time.deltaTime);

    // 滑铲时长结束自动停止
    if (slideTimer <= 0f)
    {
        StopSlide();
    }

    // 滑铲时强制取消奔跑状态
    isRunning = false;
    speedlines.SetActive(false);
}

5. 修复移动逻辑冲突

原代码存在重复的controller.Move调用,导致移动速度异常,删除以下重复行:

controller.Move(move * speed * speedMultiplier * Time.deltaTime);

6. 适配滑铲头部晃动

修改头部晃动逻辑,添加滑铲状态判断:

if (canUseHeadbob && isGrounded)
{
    if (Mathf.Abs(move.x) > 0.1f || Mathf.Abs(move.z) > 0.1f)
    {
        float bobSpeed = isCrouching ? crouchBobSpeed : isRunning ? sprintBobSpeed : (isSliding ? 22f : walkBobSpeed);
        float bobAmount = isCrouching ? crouchBobAmount : isRunning ? sprintBobAmount : (isSliding ? 0.07f : walkBobAmount);
        timer += Time.deltaTime * bobSpeed;
        camera.transform.localPosition = new Vector3(camera.transform.localPosition.x, defaultYPos + Mathf.Sin(timer) * bobAmount, camera.transform.localPosition.z);
    }
}

7. 参数配置建议

在Unity Inspector面板中设置:

  • slideSpeed:25-30(略高于奔跑速度)
  • slideDuration:0.4-0.6秒
  • slideDecayRate:保持1.5即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 21:25:14