20230118-【UNITY 学习】增加攀登系统

发布时间:2024年01月18日

替换脚本PlayerMovement_04.cs

在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement_05 : MonoBehaviour
{
    private float moveSpeed; // 玩家移动速度
    public float walkSpeed = 7; // 行走速度
    public float sprintSpeed = 10; // 冲刺速度
    public float slideSpeed = 30; // 滑动速度
    public float wallrunSpeed = 8.5f;

    public float climbSpeed = 3;

    private float desiredMoveSpeed; // 期望的移动速度
    private float lastDesiredMoveSpeed; // 上一次的期望移动速度

    public float speedIncreaseMultiplier = 1.5f; // 速度增加倍数
    public float slopeIncreaseMultiplier = 2.5f; // 斜坡增加倍数

    public float groundDrag = 5; // 地面时的阻力

    public float playerHeight = 2; // 玩家身高
    public LayerMask whatIsGround; // 地面的LayerMask
    public bool grounded; // 是否在地面上

    public float jumpForce = 6; // 跳跃力度
    public float jumpCooldown = 0.25f; // 跳跃冷却时间
    public float airMultiplier = 0.4f; // 空中移动速度衰减
    private bool readyToJump = true; // 是否可以跳跃

    public float crouchSpeed = 3.5f; // 蹲伏时的移动速度
    public float crouchYScale = 0.5f; // 蹲伏时的Y轴缩放比例
    private float startYScale; // 初始Y轴缩放比例

    public float maxSlopAngle = 40; // 最大坡度角度
    private RaycastHit slopeHit; // 坡度检测的射线信息
    private bool exitingSlope = true; // 是否正在离开坡度

    public KeyCode jumpKey = KeyCode.Space; // 跳跃键
    public KeyCode sprintKey = KeyCode.LeftShift; // 冲刺键
    public KeyCode crouchKey = KeyCode.LeftControl; // 下蹲键

    public Climbing climbingScript;

    public Transform orientation; // 玩家朝向的Transform

    private float h; // 水平输入
    private float v; // 垂直输入

    private Vector3 moveDirection; // 移动方向

    private Rigidbody rb; // 玩家刚体

    public MovementState state; // 当前玩家的移动状态
    public enum MovementState
    {
        walking,    // 行走
        sprinting,  // 冲刺
        wallrunning,//墙跑
        climbing,
        crouching,  // 蹲伏
        sliding,    // 滑动
        air         // 空中
    }

    public bool sliding; // 是否正在滑动
    public bool wallrunning;
    public bool climbing;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.freezeRotation = true; // 防止刚体旋转

        startYScale = transform.localScale.y;  // 记录初始的Y轴缩放
    }

    private void Update()
    {
        grounded = Physics.Raycast(transform.position, Vector3.down, playerHeight * 0.5f + 0.2f, whatIsGround);

        MyInput();

        SpeedControl();

        StateHandler();

        if (grounded)
            rb.drag = groundDrag;
        else
            rb.drag = 0;
    }

    private void FixedUpdate()
    {
        MovePlayer();
    }

    private void MyInput()
    {
        // 获取水平和垂直输入
        h = Input.GetAxisRaw("Horizontal");
        v = Input.GetAxisRaw("Vertical");

        // 如果按下跳跃键且准备好跳,并且在地面上
        if (Input.GetKey(jumpKey) && readyToJump && grounded)
        {
            readyToJump = false;

            Jump();

            Invoke(nameof(ResetJump), jumpCooldown);
        }

        if (Input.GetKeyDown(crouchKey))
        {
            // 调整玩家缩放以模拟蹲下效果
            transform.localScale = new Vector3(transform.localScale.x, crouchYScale, transform.localScale.z);

            rb.AddForce(Vector3.down * 5f, ForceMode.Impulse);
        }

        // 如果释放下蹲键
        if (Input.GetKeyUp(crouchKey))
        {
            // 恢复到原始Y轴缩放
            transform.localScale = new Vector3(transform.localScale.x, startYScale, transform.localScale.z);
        }
    }

    private void MovePlayer()
    {
        if (climbingScript.exitingWall) return;

        // 根据朝向计算移动方向
        moveDirection = orientation.forward * v + orientation.right * h;

        // 如果在斜坡上并且不是即将离开斜坡
        if (OnSlope() && !exitingSlope)
        {
            // 在斜坡上施加力,以便更好地移动
            rb.AddForce(GetSlopeMoveDirection(moveDirection) * moveSpeed * 20f, ForceMode.Force);

            // 如果垂直速度为正(上升),则额外施加向下的力,以克服斜坡引起的垂直速度变慢
            if (rb.velocity.y > 0)
            {
                rb.AddForce(Vector3.down * 80f, ForceMode.Force);
            }
        }
        else if (grounded) // 如果在地面上
        {
            rb.AddForce(moveDirection.normalized * moveSpeed * 10f, ForceMode.Force); // 在地面上施加移动力
        }
        else if (!grounded) // 如果在空中
        {
            // 在空中施加移动力,乘以空中移动速度衰减系数
            rb.AddForce(moveDirection.normalized * moveSpeed * 10f * airMultiplier, ForceMode.Force);
        }

        // 根据是否在斜坡上决定是否启用重力
        if (!wallrunning)
            rb.useGravity = !OnSlope();
    }

    private void SpeedControl()
    {
        // 如果在斜坡上并且不是即将离开斜坡
        if (OnSlope() && !exitingSlope)
        {
            // 如果速度的大小超过了设定的移动速度
            if (rb.velocity.magnitude > moveSpeed)
            {
                // 将速度归一化,并乘以设定的移动速度,以限制速度在设定范围内
                rb.velocity = rb.velocity.normalized * moveSpeed;
            }
        }
        // 如果不在斜坡上
        else
        {
            // 获取水平方向的速度
            Vector3 flatVel = new Vector3(rb.velocity.x, 0f, rb.velocity.z);

            // 如果水平速度的大小超过了设定的移动速度
            if (flatVel.magnitude > moveSpeed)
            {
                // 限制水平速度在设定范围内
                Vector3 limitedVel = flatVel.normalized * moveSpeed;

                // 更新刚体的速度,保持垂直速度不变
                rb.velocity = new Vector3(limitedVel.x, rb.velocity.y, limitedVel.z);
            }
        }
    }

    private void Jump()
    {
        exitingSlope = true;

        //rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z);
        rb.velocity = Vector3.zero;

        // 添加向上的力以实现跳跃
        rb.AddForce(transform.up * jumpForce, ForceMode.Impulse);
    }

    private void ResetJump()
    {
        readyToJump = true;

        exitingSlope = false;
    }

    private void StateHandler()
    {
        if (climbing)
        {
            state = MovementState.climbing;
            desiredMoveSpeed = climbSpeed;
        }
        else if (wallrunning)
        {
            state = MovementState.wallrunning;
            desiredMoveSpeed = wallrunSpeed;
        }
        else if (sliding)
        {
            state = MovementState.sliding;  // 设置当前状态为滑动状态

            if (OnSlope() && rb.velocity.y < 0.1f)
            {
                desiredMoveSpeed = slideSpeed;  // 如果在斜坡上并且垂直速度小于0.1,则设置期望移动速度为滑动速度
            }
            else
            {
                desiredMoveSpeed = sprintSpeed;  // 否则,设置期望移动速度为冲刺速度
            }
        }
        // 如果按住蹲伏键
        else if (Input.GetKey(crouchKey))
        {
            // 设置当前状态为蹲伏状态
            state = MovementState.crouching;
            // 设置移动速度为蹲伏速度
            desiredMoveSpeed = crouchSpeed;
        }
        // 如果在地面上并且按住冲刺键
        else if (grounded && Input.GetKey(sprintKey))
        {
            // 设置当前状态为冲刺状态
            state = MovementState.sprinting;
            // 设置移动速度为冲刺速度
            desiredMoveSpeed = sprintSpeed;
        }
        // 如果在地面上但没有按住冲刺键
        else if (grounded)
        {
            // 设置当前状态为行走状态
            state = MovementState.walking;
            // 设置移动速度为行走速度
            desiredMoveSpeed = walkSpeed;
        }
        // 如果不在地面上
        else
        {
            // 设置当前状态为空中状态
            state = MovementState.air;
        }

        if (Mathf.Abs(desiredMoveSpeed - lastDesiredMoveSpeed) > 4f && moveSpeed != 0)
        {
            StopAllCoroutines();  // 停止所有协程
            StartCoroutine(SmoothlyLerpMoveSpeed());  // 启动平滑插值移动速度的协程
        }
        else
        {
            moveSpeed = desiredMoveSpeed;  // 否则,直接将移动速度设置为期望移动速度
        }

        lastDesiredMoveSpeed = desiredMoveSpeed;  // 更新上一次的期望移动速度
    }

    public bool OnSlope()
    {
        // 使用射线检测当前位置向下,获取击中信息存储在slopeHit中
        if (Physics.Raycast(transform.position, Vector3.down, out slopeHit, playerHeight * 0.5f + 0.3f))
        {
            // 计算斜坡的角度
            float angle = Vector3.Angle(Vector3.up, slopeHit.normal);

            // 如果角度小于最大允许斜坡角度且不等于0,表示在斜坡上
            return angle < maxSlopAngle && angle != 0;
        }

        // 如果没有击中信息,或者角度不符合条件,表示不在斜坡上
        return false;

    }

    public Vector3 GetSlopeMoveDirection(Vector3 direction)
    {
        // 使用Vector3.ProjectOnPlane将移动方向投影到斜坡法线上,然后进行归一化
        return Vector3.ProjectOnPlane(direction, slopeHit.normal).normalized;
    }

    private IEnumerator SmoothlyLerpMoveSpeed()
    {
        float time = 0;  // 记录经过的时间
        float difference = Mathf.Abs(desiredMoveSpeed - moveSpeed);  // 计算期望移动速度与当前移动速度的差值
        float startValue = moveSpeed;  // 记录开始时的移动速度

        while (time < difference)
        {
            moveSpeed = Mathf.Lerp(startValue, desiredMoveSpeed, time / difference);  // 使用插值平滑地改变移动速度

            if (OnSlope())
            {
                float slopeAngle = Vector3.Angle(Vector3.up, slopeHit.normal);  // 计算当前坡度的角度
                float slopeAngleIncrease = 1 + (slopeAngle / 90f);  // 根据坡度角度增加速度

                // 根据时间、速度增加倍数、坡度增加倍数进行平滑插值
                time += Time.deltaTime * speedIncreaseMultiplier * slopeIncreaseMultiplier * slopeAngleIncrease;
            }
            else
            {
                // 在平地上,只考虑时间和速度增加倍数
                time += Time.deltaTime * speedIncreaseMultiplier;
            }

            yield return null;  // 等待下一帧
        }

        moveSpeed = desiredMoveSpeed;  // 最终将移动速度设置为期望移动速度
    }
}

新增脚本Climbing.cs

在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Climbing : MonoBehaviour
{
    public Transform orientation; // 角色朝向
    public Rigidbody rb; // 角色刚体
    public PlayerMovement_05 pm_05; // 角色基本运动脚本
    public LayerMask whatIsWall; // 定义哪些是墙的图层

    public float climbSpeed = 10; // 攀爬速度
    public float maxClimbTime = 0.75f; // 最大攀爬时间
    private float climbTimer; // 攀爬计时器

    private bool climbing; // 是否正在攀爬

    public float climbJumpUpForce = 14; // 攀爬跳跃向上的力
    public float climbJumpBackForce = 12; // 攀爬跳跃向后的力

    public KeyCode jumpKey = KeyCode.Space; // 跳跃键
    public int climbJumps = 1; // 攀爬跳跃次数
    private int climbJumpsLeft; // 剩余可用的攀爬跳跃次数

    public float detectionLength = 0.7f; // 检测墙的射线长度
    public float sphereCastRadius = 0.25f; // 射线球的半径
    public float maxWallLookAngle = 30; // 最大墙面角度
    private float wallLookAngle; // 当前墙面角度

    private RaycastHit frontWallHit; // 射线检测到的前方墙面信息
    private bool wallFront; // 是否在墙面前方

    private Transform lastWall; // 上一次攀爬的墙面
    private Vector3 lastWallNormal; // 上一次攀爬的墙面法线方向
    public float minWallNormalAngleChange = 5; // 最小墙面法线方向变化角度

    public bool exitingWall; // 是否正在退出墙面
    public float exitWallTime = 0.2f; // 退出墙面的时间
    private float exitWallTimer; // 退出墙面计时器

    private void Update()
    {
        WallCheck();
        StateMachine();

        if (climbing && !exitingWall)
        {
            ClimbingMovement();
        }
    }

    private void StateMachine()
    {
        // 检查是否在墙面前方,按下攀爬键,墙面角度小于最大角度,且正在退出墙面
        if (wallFront && Input.GetKey(KeyCode.W) && wallLookAngle < maxWallLookAngle && !exitingWall)
        {
            // 如果尚未开始攀爬且攀爬时间尚未用完
            if (!climbing && climbTimer > 0)
            {
                Debug.Log("开始攀爬");
                StartClimbing(); // 开始攀爬
            }
            // 如果攀爬时间尚未用完
            if (climbTimer > 0)
            {
                climbTimer -= Time.deltaTime;
            }
            // 如果攀爬时间用完
            if (climbTimer < 0)
            {
                Debug.Log("攀爬时间用完,停止攀爬!");
                StopClimbing(); // 停止攀爬
            }
        }
        // 如果正在退出墙面
        else if (exitingWall)
        {
            // 如果正在攀爬
            if (climbing)
            {
                StopClimbing(); // 停止攀爬
            }
            // 如果退出墙面计时尚未用完
            if (exitWallTimer > 0)
                exitWallTimer -= Time.deltaTime;
            // 如果退出墙面计时用完
            if (exitWallTimer < 0)
                exitingWall = false;
        }
        else
        {
            // 如果正在攀爬
            if (climbing)
            {
                StopClimbing(); // 停止攀爬
            }
        }

        // 如果在墙面前方,按下跳跃键,且剩余可用的攀爬跳跃次数大于0
        if (wallFront && Input.GetKeyDown(jumpKey) && climbJumpsLeft > 0)
        {
            Debug.Log("进行攀爬跳跃");
            ClimbJump(); // 进行攀爬跳跃
        }
    }

    private void WallCheck()
    {
        // 发出前方射线,检测是否有墙
        wallFront = Physics.SphereCast(transform.position, sphereCastRadius, orientation.forward, out frontWallHit, detectionLength, whatIsWall);

        // 计算墙面角度
        wallLookAngle = Vector3.Angle(orientation.forward, -frontWallHit.normal);

        // 判断是否是新的墙面,或者墙面法线方向变化大于最小变化角度,或者角色在地面上
        bool newWall = frontWallHit.transform != lastWall || Mathf.Abs(Vector3.Angle(lastWallNormal, frontWallHit.normal)) > minWallNormalAngleChange;

        // 如果在墙面前方且是新的墙面
        if ((wallFront && newWall) || pm_05.grounded)
        {
            climbTimer = maxClimbTime; // 重置攀爬时间
            climbJumpsLeft = climbJumps; // 重置可用的攀爬跳跃次数
        }
    }

    private void StartClimbing()
    {
        climbing = true; // 设置正在攀爬
        pm_05.climbing = true; // 触发攀爬状态

        lastWall = frontWallHit.transform; // 记录上一次攀爬的墙面
        lastWallNormal = frontWallHit.normal; // 记录上一次攀爬的墙面法线方向
    }

    private void ClimbingMovement()
    {
        rb.velocity = new Vector3(rb.velocity.x, climbSpeed, rb.velocity.z); // 应用攀爬速度
    }

    private void StopClimbing()
    {
        climbing = false; // 设置停止攀爬
        pm_05.climbing = false; // 触发停止攀爬状态
    }

    private void ClimbJump()
    {
        exitingWall = true; // 设置正在退出墙面

        exitWallTimer = exitWallTime; // 重置退出墙面计时器

        // 计算施加到角色身上的力,包括向上的力和沿墙的后退力
        Vector3 forceToApply = transform.up * climbJumpUpForce + frontWallHit.normal * climbJumpBackForce;

        rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z); // 将垂直速度置为0,确保在墙面上的水平移动

        rb.AddForce(forceToApply, ForceMode.Impulse); // 应用力到刚体,实现攀爬跳跃效果

        climbJumpsLeft--; // 减少可用的攀爬跳跃次数
    }
}
文章来源:https://blog.csdn.net/qq_44887198/article/details/135677011
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。